285 lines
9.7 KiB
Python
285 lines
9.7 KiB
Python
# -*- 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. 注册 api(append_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("探针[首次写入] FAIL:dedup=%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("探针[幂等去重] FAIL:dedup=%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("探针[缺租户必须拒] FAIL:code=%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 FAILED(fail-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)
|