fix(chv-muse-r12fix-5): UsageLedger.record() 读-改-写原子化(P11)
R12 真机:4 线程 × 10 条记账 0 写错,但 verify_chain 返回 (False, first_bad=11),链 G、链 H 两次复现同一断点;根因是 record() 先 SELECT 链头、再算摘要、再 INSERT 提交,无锁无 事务,两线程读到同一 prev_hash 各写一条,链分叉。修法: 同一实例用 threading.Lock 串行化;SELECT 前 BEGIN IMMEDIATE 持库级写锁(WAL + busy_timeout 下等待而非立即失败),使另一 实例/进程读到的链头必是已提交最新值。record_warning 委托 record(),同款覆盖。新增并发测试:同款 4×10 复测两轮, verify_chain 全真、40 条一条不丢(旧代码下该测试失败)。
This commit is contained in:
1 parent
0ebcdfb2a7
commit
a18a530bfd
2 files changed
+66
-20
No files matched your search
@@ -288,6 +288,36 @@ class LedgerTestCase(unittest.TestCase):
|
||||
self.assertNotIn("token", cols)
|
||||
self.assertGreater(row_id, 0)
|
||||
|
||||
def test_concurrent_records_keep_chain_intact(self):
|
||||
# round-12 修复轮(项 5,P11):R12 真机同款并发复测——
|
||||
# 4 线程 × 10 条,0 写错,但旧 record() 读-改-写无锁,
|
||||
# verify_chain 返回 (False, first_bad=11),链 G、链 H
|
||||
# 两次复现同一断点。修后两轮都须全真、40 条一条不丢。
|
||||
for _round in range(2):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
db_path = _make_db(tmp)
|
||||
ledger = UsageLedger(db_path)
|
||||
errors = []
|
||||
|
||||
def _worker():
|
||||
try:
|
||||
for _ in range(10):
|
||||
ledger.record("u1", "message", 1)
|
||||
except Exception as e: # noqa: BLE001 - 汇总后断言
|
||||
errors.append(e)
|
||||
|
||||
threads = [threading.Thread(target=_worker)
|
||||
for _ in range(4)]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
self.assertEqual(errors, [])
|
||||
self.assertEqual(ledger.sum_kind("u1", "message"), 40)
|
||||
ok, bad = ledger.verify_chain("u1")
|
||||
self.assertTrue(ok, bad)
|
||||
self.assertIsNone(bad)
|
||||
|
||||
|
||||
class WarningTestCase(unittest.TestCase):
|
||||
def test_once_per_window_at_80_percent(self):
|
||||
|
||||
Reference in new issue
Block a user