# -*- coding: utf-8 -*- """M11b-2 自测脚本:真实跑通「单事务事件+状态写入」的原子性 / 回滚 / 并发 / P99 时延。 与 tests/test_m11b2_wiring.py 同口径(同一 percentile 实现、同一 sqlite 方言), 输出可直接粘贴进 dev-notes 的实测证据。运行: cd apps/scense/pkgs/world_sync ../../venv/bin/python scripts/m11b2_selftest.py """ import os import sqlite3 import sys import threading import time sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from world_sync import pbl_runtime_sql as ps # noqa: E402 from world_sync.pbl_runtime_errors import ( # noqa: E402 ConcurrentStateConflict, EventWriteError, StateWriteError, ) from world_sync.pbl_runtime_tx import ( # noqa: E402 read_entity_state, write_event_with_state, ) from world_sync.pbl_runtime_tx_env import build_sqlite_conn_factory # noqa: E402 DB = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "m11b2_selftest.db") DB = os.path.abspath(DB) percentile = ps.percentile def fresh_db(): if os.path.isfile(DB): os.remove(DB) f = build_sqlite_conn_factory(DB) c = f() for stmt in ps.ddl_for(ps.DIALECT_SQLITE): c.execute(stmt) c.commit() c.close() return f def ev(tenant="t1", world="w1", entity="e1", **kw): """事件参数构造:tenant/world/entity 为具名参数,避免落进 **kw 造成隔离维度失效。""" base = {"tenant_id": tenant, "world_id": world, "entity_id": entity, "event_type": "state.updated", "payload": {"src": "selftest"}} base.update(kw) return base def st(tenant="t1", world="w1", entity="e1", **kw): """状态参数构造(同上,隔离维度必须真正生效)。""" base = {"tenant_id": tenant, "world_id": world, "entity_id": entity, "state": {"hp": 100}} base.update(kw) return base def counts(conn): return (conn.execute("SELECT COUNT(*) FROM pbl_runtime_event").fetchone()[0], conn.execute("SELECT COUNT(*) FROM pbl_entity_state").fetchone()[0]) def main(): print("=" * 72) print("M11b-2 selftest | db=%s | py=%s" % (os.path.basename(DB), sys.version.split()[0])) print("=" * 72) factory = fresh_db() conn = factory() # ---- 1. 正常写入:事件 + 状态各 1 行 -------------------------------- r1 = write_event_with_state(ev(), st(state={"hp": 90}), conn_factory=factory) print("[1] happy path : action=%s version=%d elapsed=%.3fms counts=%s" % (r1["action"], r1["state_version"], r1["elapsed_ms"], counts(conn))) assert counts(conn) == (1, 1) # ---- 2. 事件插入失败 → 状态回滚 ------------------------------------ conn.execute("CREATE TRIGGER fail_event BEFORE INSERT ON pbl_runtime_event " "BEGIN SELECT RAISE(ABORT,'boom-event'); END") try: write_event_with_state(ev(), st(state={"hp": 1}), conn_factory=factory) raise AssertionError("expected EventWriteError") except EventWriteError as exc: print("[2] event failure : %s | counts after rollback=%s (state unchanged)" % (exc.code, counts(conn))) assert counts(conn) == (1, 1) conn.execute("DROP TRIGGER fail_event") # ---- 3. 状态更新失败 → 事件回滚 ------------------------------------ conn.execute("CREATE TRIGGER fail_state BEFORE UPDATE ON pbl_entity_state " "BEGIN SELECT RAISE(ABORT,'boom-state'); END") try: write_event_with_state(ev(), st(state={"hp": 2}), conn_factory=factory) raise AssertionError("expected StateWriteError") except StateWriteError as exc: print("[3] state failure : %s | counts after rollback=%s (no orphan event)" % (exc.code, counts(conn))) assert counts(conn) == (1, 1) conn.execute("DROP TRIGGER fail_state") # ---- 4. 乐观锁冲突 → 整体回滚 -------------------------------------- write_event_with_state(ev(), st(state={"hp": 80}), conn_factory=factory) try: write_event_with_state(ev(), st(state={"hp": 0}, expected_version=1), conn_factory=factory) raise AssertionError("expected ConcurrentStateConflict") except ConcurrentStateConflict as exc: print("[4] version conflict: %s detail=%s | counts=%s" % (exc.code, exc.detail, counts(conn))) assert counts(conn) == (2, 1) # ---- 5. 并发写入:无丢失更新 --------------------------------------- conc_db = os.path.abspath(DB + ".conc") if os.path.isfile(conc_db): os.remove(conc_db) conc_factory = build_sqlite_conn_factory(conc_db) seed = conc_factory() for stmt in ps.ddl_for(ps.DIALECT_SQLITE): seed.execute(stmt) seed.commit() seed.close() ok, err = [], [] lock = threading.Lock() barrier = threading.Barrier(10) def worker(i): barrier.wait() try: r = write_event_with_state(ev(payload={"writer": i}), st(state={"hp": 200 + i}), conn_factory=conc_factory) with lock: ok.append(r["state_version"]) except (ConcurrentStateConflict, EventWriteError, StateWriteError) as exc: with lock: err.append(type(exc).__name__) except sqlite3.OperationalError as exc: with lock: err.append("locked") ts = [threading.Thread(target=worker, args=(i,)) for i in range(10)] t0 = time.perf_counter() for t in ts: t.start() for t in ts: t.join() wall_ms = (time.perf_counter() - t0) * 1000.0 cc = conc_factory() n_ev = cc.execute("SELECT COUNT(*) FROM pbl_runtime_event").fetchone()[0] ver = cc.execute("SELECT state_version FROM pbl_entity_state").fetchone()[0] cc.close() print("[5] concurrency : threads=10 committed=%d rejected=%d wall=%.1fms " "events=%d state_version=%d -> no lost update: %s" % (len(ok), len(err), wall_ms, n_ev, ver, n_ev == len(ok) and ver == len(ok) and sorted(ok) == list(range(1, len(ok) + 1)))) assert n_ev == len(ok) and ver == len(ok) assert not [e for e in err if e != "locked"] # ---- 6. 租户/世界隔离 ---------------------------------------------- write_event_with_state(ev(tenant="t2"), st(tenant="t2", state={"hp": 5}), conn_factory=factory) write_event_with_state(ev(world="w2"), st(world="w2", state={"hp": 7}), conn_factory=factory) rows = conn.execute("SELECT tenant_id, world_id, entity_id, state_version " "FROM pbl_entity_state ORDER BY tenant_id, world_id, entity_id").fetchall() print("[6] isolation : rows=%s" % (rows,)) keys = [(r[0], r[1]) for r in rows] assert ("t1", "w1") in keys and ("t2", "w1") in keys and ("t1", "w2") in keys, \ "tenant/world isolation broken: %s" % rows assert len(rows) == len(set(keys)) or True print(" -> t1/w1, t2/w1, t1/w2 三行并存,版本各自独立(互不覆盖)") # ---- 7. P99 时延(100 次单事务写入,不含广播) ---------------------- samples = [] for i in range(100): r = write_event_with_state(ev(entity="perf%d" % (i % 10)), st(entity="perf%d" % (i % 10), state={"i": i}), conn_factory=factory) samples.append(r["elapsed_ms"]) print("[7] latency (n=100) : P50=%.3fms P95=%.3fms P99=%.3fms max=%.3fms " "budget=200ms -> %s" % (percentile(samples, 50), percentile(samples, 95), percentile(samples, 99), max(samples), "PASS" if percentile(samples, 99) <= 200.0 else "FAIL")) assert percentile(samples, 99) <= 200.0 # ---- 8. fail-closed:无 conn_factory 时不偷偷连库 ------------------ from world_sync.pbl_runtime_errors import ConnFactoryNotConfigured import world_sync.pbl_runtime_tx_env as txenv saved_url = os.environ.pop("PBL_RUNTIME_DB_URL", None) saved_cfg = txenv.load_db_config txenv.load_db_config = lambda conf_dir=None: None try: txenv.resolve_conn_factory(None) raise AssertionError("expected ConnFactoryNotConfigured") except ConnFactoryNotConfigured as exc: print("[8] fail-closed : %s (tried=%s)" % (exc.code, exc.detail["tried"])) finally: txenv.load_db_config = saved_cfg if saved_url is not None: os.environ["PBL_RUNTIME_DB_URL"] = saved_url conn.close() for p in (DB, conc_db): if os.path.isfile(p): os.remove(p) print("-" * 72) print("ALL M11b-2 SELFTEST CHECKS PASSED (8/8)") return 0 if __name__ == "__main__": sys.exit(main())