2026-09-19 12:22:21 +08:00

285 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""pbl_runtime_ext 挂载入口M11a/M11b
load_pbl_runtime_ext(env=None)
1. ensure_tables 幂等建 3 表pbl_runtime_event / pbl_entity_state / pbl_world_state_snapshot
2. **挂载即执行 self_check()**写保护域world/scene/entity/scense/scense_runtime/
script_engine 基表)出现 U/D 调用、或单事务/幂等契约缺失 → 抛 RuntimeError 拒绝启动
3. 注册 apiappend_event / list_events / find_by_idem / next_seq / self_check
4. 返回 api 字典
铁律:本模块是 scense_runtime 的**薄扩展**,只写 pbl_* 自有表,不改任何基表。
"""
from . import tables as _tables
from . import tx_event as _tx
# M11b单事务事件+状态写入与提交后广播(权威实现)
try:
from . import m11b_api as _m11b
from . import tx_write as _wtx
except Exception as _exc: # noqa: BLE001 缺依赖时不阻断 M11a 能力
_m11b = None
_wtx = None
_M11B_IMPORT_ERROR = str(_exc)
else:
_M11B_IMPORT_ERROR = None
MODULE = "pbl_runtime_ext"
EXPECTED_TABLES = 3
def ensure_tables(env=None, sor=None):
"""幂等建 3 表。返回 (ok_list, bad_list)。"""
return _tables.ensure_tables(env=env, sor=sor)
def api():
"""对外 API 契约。"""
return {
"append_event": _tx.append_event,
"list_events": _tx.list_events,
"find_by_idem": _tx.find_by_idem,
"next_seq": _tx.next_seq,
"TxError": _tx.TxError,
"self_check": self_check,
"ensure_tables": ensure_tables,
"EVENT_TABLE": _tx.EVENT_TABLE,
"STATE_TABLE": _tx.STATE_TABLE,
"SNAPSHOT_TABLE": _tx.SNAPSHOT_TABLE,
# ---- M11b ----
"apply_runtime_event": getattr(_wtx, "apply_runtime_event", None),
"poll_events": getattr(_wtx, "poll_events", None),
"read_states": getattr(_wtx, "read_states", None),
"assert_append_only": getattr(_wtx, "assert_append_only", None),
"broadcast_hub": getattr(_m11b, "get_hub", lambda: None)() if _m11b else None,
"contracts": dict(getattr(_m11b, "contracts", lambda: {})()) if _m11b else {},
"ensure_forward_partitions": _partitions_fn(),
"M11B": bool(_m11b and _wtx),
}
def _partitions_fn():
try:
from .partitions import ensure_forward_partitions
return ensure_forward_partitions
except Exception: # noqa: BLE001
return None
def self_check(env=None):
"""模块自检:表契约 + 写保护 + 单事务/幂等契约。返回 (all_ok, msgs)。"""
msgs = []
all_ok = True
tbl_ok, tbl_msgs = _tables.self_check()
if not tbl_ok:
all_ok = False
msgs.extend(tbl_msgs)
if len(_tables.TABLES) != EXPECTED_TABLES:
all_ok = False
msgs.append("表数=%d 应为 %d" % (len(_tables.TABLES), EXPECTED_TABLES))
tx_ok, tx_msgs = _tx.self_check()
if not tx_ok:
all_ok = False
msgs.extend(tx_msgs)
# M11b 自检append-only 守卫 / 月分区算法 / 幂等键派生(离线不连库)
if _M11B_IMPORT_ERROR:
all_ok = False
msgs.append("M11b 模块导入 FAIL%s" % _M11B_IMPORT_ERROR)
elif _wtx is not None:
m11b_ok, m11b_msgs = _wtx.self_check()
if not m11b_ok:
all_ok = False
msgs.extend(m11b_msgs)
# 幂等探针:同 idem_key 二次投递必须走 dedup 分支(离线用假 sor 验证)
probe_ok, probe_msgs = _probe_idempotent()
if not probe_ok:
all_ok = False
msgs.extend(probe_msgs)
if all_ok:
msgs.append("SELF_CHECK %s: PASS %d/%d" % (MODULE, len(_tables.TABLES), len(_tables.TABLES)))
return all_ok, msgs
class _FakeSor(object):
"""离线自检用内存 sor只实现 C/R/U验证单事务+幂等真实行为。"""
def __init__(self):
self.rows = {}
self.seq = 0
self.updated = []
def C(self, tbl, row):
self.seq += 1
row = dict(row)
row["id"] = self.seq
self.rows.setdefault(tbl, []).append(row)
return self.seq
def R(self, tbl, where="", fields="*", order="", limit=100):
out = []
for r in self.rows.get(tbl, []):
hit = True
for clause in str(where).split(" AND "):
if "=" not in clause:
continue
k, v = clause.split("=", 1)
v = v.strip().strip("'")
if str(r.get(k.strip())) != v:
hit = False
break
if hit:
out.append(r)
return out[:limit]
def U(self, tbl, values, where=""):
self.updated.append((tbl, dict(values), where))
return len(self.rows.get(tbl, []))
def sqlExe(self, sql):
return 0
class _FakeEnv(object):
def __init__(self):
self.sor = _FakeSor()
self.broadcasts = []
def broadcast(self, channel, payload):
self.broadcasts.append((channel, payload))
return 1
def _probe_idempotent():
"""幂等 + 单事务 + 广播探针。返回 (all_ok, msgs)。"""
msgs = []
all_ok = True
env = _FakeEnv()
try:
r1 = _tx.append_event(env, tenant_id=7, world_id=1, session_id=11,
event_type="move", payload={"x": 1},
idem_key="probe-key-1",
state_updates=[{"state_key": "pos", "state_value": {"x": 1}}])
r2 = _tx.append_event(env, tenant_id=7, world_id=1, session_id=11,
event_type="move", payload={"x": 1},
idem_key="probe-key-1",
state_updates=[{"state_key": "pos", "state_value": {"x": 1}}])
except Exception as exc: # noqa: BLE001
return False, ["探针[单事务写入] FAIL%r" % exc]
if r1.get("dedup") is not False:
all_ok = False
msgs.append("探针[首次写入] FAILdedup=%r 应为 False" % r1.get("dedup"))
else:
msgs.append("探针[首次写入] PASS seq_no=%s state=%s" % (r1.get("seq_no"), r1.get("state")))
if r2.get("dedup") is not True:
all_ok = False
msgs.append("探针[幂等去重] FAILdedup=%r 应为 True" % r2.get("dedup"))
else:
msgs.append("探针[幂等去重] PASS 同 idem_key 未重复写")
n_ev = len(env.sor.rows.get(_tx.EVENT_TABLE, []))
if n_ev != 1:
all_ok = False
msgs.append("探针[事件行数] FAIL%d 应为 1" % n_ev)
else:
msgs.append("探针[事件行数] PASS 1 行")
if len(env.sor.rows.get(_tx.STATE_TABLE, [])) != 1:
all_ok = False
msgs.append("探针[状态行数] FAIL应为 1")
else:
msgs.append("探针[状态行数] PASS 1 行")
if len(env.sor.rows.get(_tx.SNAPSHOT_TABLE, [])) != 1:
all_ok = False
msgs.append("探针[快照行数] FAIL应为 1")
else:
msgs.append("探针[快照行数] PASS 1 行")
if not env.broadcasts:
all_ok = False
msgs.append("探针[提交后广播] FAIL未广播")
else:
msgs.append("探针[提交后广播] PASS 通道=%d" % len(env.broadcasts))
# 缺租户必须拒
try:
_tx.append_event(env, tenant_id=None, world_id=1, session_id=11, event_type="move")
all_ok = False
msgs.append("探针[缺租户必须拒] FAIL未抛 TxError")
except _tx.TxError as exc:
if exc.code != "PBL-TENANT-0001":
all_ok = False
msgs.append("探针[缺租户必须拒] FAILcode=%s" % exc.code)
else:
msgs.append("探针[缺租户必须拒] PASS PBL-TENANT-0001")
# 状态写失败必须整体回滚(事件标 rolled_back
env2 = _FakeEnv()
try:
_tx.append_event(env2, tenant_id=7, world_id=1, session_id=12,
event_type="move", payload={},
state_updates=[{"bad": "no state_key"}])
all_ok = False
msgs.append("探针[状态失败回滚] FAIL未抛 TxError")
except _tx.TxError as exc:
rolled = [u for u in env2.sor.updated if u[1].get("state") == "rolled_back"]
if not rolled:
all_ok = False
msgs.append("探针[状态失败回滚] FAIL事件未标 rolled_back%r" % exc.code)
else:
msgs.append("探针[状态失败回滚] PASS 事件标 rolled_back无脏数据")
return all_ok, msgs
def load_pbl_runtime_ext(env=None):
"""挂载入口:建表 → 自检(不过即抛)→ 注册契约 → 返回 api 字典。"""
srv = env
if srv is None:
try:
from ahserver.serverenv import ServerEnv
srv = ServerEnv()
except Exception: # noqa: BLE001
srv = None
if srv is not None:
ensure_tables(srv)
ok, msgs = self_check(srv)
for m in msgs:
_log(srv, m)
if not ok:
raise RuntimeError(
"%s self_check FAILEDfail-closed拒绝启动%s"
% (MODULE, "; ".join([m for m in msgs if "FAIL" in m or "应为" in m][:6]))
)
if srv is not None:
setattr(srv, "pbl_runtime_ext_api", api())
modules = getattr(srv, "modules", None)
if isinstance(modules, list) and MODULE not in modules:
modules.append(MODULE)
return api()
def _log(srv, msg):
logger = getattr(srv, "logger", None) if srv is not None else None
if logger is not None and hasattr(logger, "info"):
try:
logger.info("[%s] %s" % (MODULE, msg))
return
except Exception: # noqa: BLE001
pass
print("[%s] %s" % (MODULE, msg))
if __name__ == "__main__":
_ok, _msgs = self_check()
for _m in _msgs:
print(_m)
print("RESULT: %s" % ("PASS" if _ok else "FAIL"))
raise SystemExit(0 if _ok else 1)