修复:补强使用并发幂等复核

This commit is contained in:
焦龙言
2026-07-26 21:43:55 +08:00
parent bc774c74e6
commit 33228faffc
4 changed files with 359 additions and 13 deletions
@@ -133,7 +133,12 @@ class TestDaoPublicContract(unittest.TestCase):
'site_ids',
),
'get_filling_by_idempotency_key': ('db', 'idempotency_key', 'warehouse_ids'),
'get_usage_by_idempotency_key': ('db', 'idempotency_key', 'site_ids'),
'get_usage_by_idempotency_key': (
'db',
'idempotency_key',
'site_ids',
'for_update',
),
'get_return_by_request_idempotency_key': ('db', 'idempotency_key'),
'get_return_by_confirm_idempotency_key': ('db', 'idempotency_key'),
'get_inbound_by_id': ('db', 'inbound_id', 'warehouse_ids'),
@@ -225,6 +230,13 @@ class TestDaoPublicContract(unittest.TestCase):
signature = inspect.signature(getattr(OperationDao, method_name))
self.assertIsNone(signature.parameters['site_ids'].default)
usage_key_signature = inspect.signature(OperationDao.get_usage_by_idempotency_key)
usage_key_lock = usage_key_signature.parameters.get('for_update')
if usage_key_lock is None:
self.fail('get_usage_by_idempotency_key 缺少 for_update 参数')
self.assertIs(inspect.Parameter.KEYWORD_ONLY, usage_key_lock.kind)
self.assertFalse(usage_key_lock.default)
return_list_signature = inspect.signature(OperationDao.get_return_list)
self.assertEqual(('db', 'query_object', 'is_page'), tuple(return_list_signature.parameters))
self.assertIs(True, return_list_signature.parameters['is_page'].default)
@@ -943,6 +955,54 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
self.assertNotIn('site_id IN', sql)
self.assertNotIn('WHERE false', sql)
async def test_usage_idempotency_current_read_locks_refreshes_and_preserves_scope(self) -> None:
self.assertIn(
'for_update',
inspect.signature(OperationDao.get_usage_by_idempotency_key).parameters,
)
scalar_result = MagicMock()
scalar_result.scalar_one_or_none.return_value = None
self.db.execute.return_value = scalar_result
cases = (
([11, 9, 11], 'biz_usage_record.site_id IN (9, 11)'),
([], None),
(None, None),
)
for site_ids, expected_scope in cases:
with self.subTest(site_ids=site_ids):
self.db.reset_mock()
self.db.execute.return_value = scalar_result
await OperationDao.get_usage_by_idempotency_key(
self.db,
'usage-key',
site_ids=site_ids,
for_update=True,
)
statement = self.db.execute.await_args.args[0]
self.assertTrue(statement.get_execution_options().get('populate_existing'))
for dialect in (mysql.dialect(), postgresql.dialect()):
sql = compile_sql(statement, dialect)
self.assertIn('FOR UPDATE', sql)
if expected_scope is not None:
self.assertIn(expected_scope, sql)
elif site_ids == []:
self.assertTrue('false' in sql.lower() or '0 = 1' in sql, sql)
else:
self.assertNotIn('site_id IN', sql)
self.db.reset_mock()
self.db.execute.return_value = scalar_result
await OperationDao.get_usage_by_idempotency_key(
self.db,
'usage-key',
site_ids=[9],
)
unlocked_statement = self.db.execute.await_args.args[0]
self.assertNotIn('FOR UPDATE', compile_sql(unlocked_statement, mysql.dialect()))
self.assertFalse(unlocked_statement.get_execution_options().get('populate_existing', False))
def test_time_conditions_include_fractional_last_second_with_exclusive_next_day(self) -> None:
lower, upper = OperationDao._time_conditions(
BizUsageRecord.occurred_at,