world_sync/docs/work-log-2026-09-22-m11b2c.md
2026-09-22 17:37:22 +08:00

109 lines
11 KiB
Markdown
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.

# Work Log — 2026-09-22 · M11b-2c:world_sync 单事务库函数(write_event_with_state / read_entity_state)
- 模块:`world_sync`(宿主镜像/消费方侧,运行时事件与实体状态单事务写入器)
- 里程碑:**M11b-2**(单事务写两表);上游真源模块 `pbl_runtime_ext`(表结构与 `tx_write/tx_read` 能力权威源)
- 任务:`[M11b-2c-T2] 补 M11b-2 开发文档 dev-notes.md + code_files.md`(task_id=`kICTWazBqgIWtKNO7sz8A`,classify_task=new_dev)
- 本文按 module-development-spec「Work Logs 强制项」产出:本次改了什么、数据模型、契约接口、陷阱与裁决、验证证据。
- 范围纪律:本任务**只补文档**,未修改任何业务代码 / `models/*.json` / `scripts/load_path.py`(后者归 T3)。
## 1. 数据模型
M11b-2 写路径涉及两张表,**结构权威源在 `pbl_runtime_ext/models/`**,`world_sync/models/` 下是**消费方内联副本**(不是引用式声明——`database-table-definition-spec` 只认 summary/fields/indexes/codes 四段式,`scripts/validate_models_json.py` 对 `key=schema` 引用式写法直接判 FAIL,QC #5 结论)。
### 1.1 `pbl_runtime_event`(运行时事件,append-only)
- 真源:`modules/pbl_runtime_ext/models/pbl_runtime_event.json`;副本:`modules/world_sync/models/pbl_runtime_event.json`(8782B)
- 一致性:由 `scripts/m11b2b_column_diff.py` + `scripts/m11b2_selftest.py` 机械核对,`mirror-sha256-equal` 断言真源与 `apps/scense/pkgs/world_sync` 镜像**字节级一致**(本次重跑 PASS)。
- 语义要点:
- **append-only**:写路径只做 INSERT,不 UPDATE / 不 DELETE(`TestScopeAndSqlContract::test_event_sql_is_insert_only` 守这条)。
- `event_uid` 是幂等/重试定位键(默认 uuid4 hex,可由入参指定以便测试因果链);旧口径的 `event_id` 已废弃。
- `state_version` 与状态行生效后的版本号**对齐**(同事务内第 4 步算出再写第 5 步),供 M11b-3 广播去重与 M11b-4 轮询增量拉取。
- `seq_no` 严格单调递增(探针断言 PASS);`source` 默认 `world_sync.m11b2`;`broadcast` 标记位由 M11b-3 消费。
- `payload` 以 JSON **文本**入库(`json.dumps` 后写 `text` 列;抽象类型归一 `json→text`、`bigint→long`,不保留物理修饰)。
### 1.2 `pbl_entity_state`(实体状态,服务端权威版本)
- 真源:`modules/pbl_runtime_ext/models/pbl_entity_state.json`;副本:`modules/world_sync/models/pbl_entity_state.json`(6099B)
- 列集(真源实测 10 列,早期「8 列」口径作废):`tenant_id / id / session_id / entity_id / state_json / state_version / checksum / updated_by / created_at / updated_at`
- 定位键 = 唯一键 `uk_es (tenant_id, session_id, entity_id)`,与 `write_event_with_state` / `read_entity_state` 的 WHERE 完全一致(**没有** `world_id` 列,`world_id` 仅作历史兼容入参被忽略)。
- `state_version` 单调 +1,**客户端禁写**;`state_json` 存完整新状态(非 diff)。
- `id` 类型偏离:真源 `BIGINT AUTO_INCREMENT`,world_sync 副本内联为 `str/length=32`(spec 合规写法),主键由应用层生成 32 位 uuid。**写路径 INSERT 不显式提交 id**——生产 MySQL 交给真源 DDL 的 AUTO_INCREMENT,本地/测试 sqlite DDL 用应用层生成值。此偏离为有意选择(副本侧不走「登记放行 id 非 str(32)」路线,因为本任务范围禁改校验器脚本),评审勿判结构缺陷。
- 主键只在 `summary[0].primary=["id"]` 单点声明,`indexes[]` 内不出现 `PRIMARY` 条目(spec 规则 `index_name_primary`)。
## 2. 契约接口
### 2.1 两个库函数(M11b-2 的全部对外面)
```python
# world_sync/pbl_runtime_tx.py:351
def write_event_with_state(event_data=None, state_update=None, conn_factory=None,
conn=None, event_uid=None, source=None, clock=None) -> dict
# world_sync/pbl_runtime_tx.py:310
def read_entity_state(conn_factory=None, tenant_id=None, session_id=None,
entity_id=None, world_id=None, conn=None) -> dict | None
```
- `event_data` 必填:`tenant_id` / `entity_id` / `event_type`;可选 `payload`、`session_id`、`scene_id`、`world_id`、`actor_id`、`causation_id`、`idem_key`、`seq`/`seq_no`、`tx_group`、`source`、`broadcast`。
- `state_update` 必填:`tenant_id` / `session_id` / `entity_id`;新状态键 `state`(或 `state_json`,dict 或 JSON 文本皆可);可选 `expected_version`(乐观锁)、`updated_by`。**传 `None` = 只写事件不改状态**(返回 `action="event_only"`)。
- 成功返回:`{event_uid, state_version, action: updated|inserted|event_only, elapsed_ms, isolation_level_applied, dialect, ...}`。
- `elapsed_ms` 是 P99 统计的采样来源;
- `isolation_level_applied=False` 在 sqlite 下是**正常**(无该概念),MySQL/PG 下应为 True。
- `read_entity_state` 返回 `{"state": ..., "state_version": int, "updated_at": ...}`,查不到返回 `None`;它是**独立短事务**,不与写入事务共用(避免长事务),供上层冲突后重读版本再重试。
- 复用 `conn` 时仍由本函数负责该连接的 commit/rollback,**调用方不要自行 commit**。
### 2.2 三处同步注册(module-development-spec 铁律 4,已 grep 核验)
| 环节 | 位置 |
|---|---|
| ① 实现 | `world_sync/pbl_runtime_tx.py:310,351`(`__all__` 已导出) |
| ② 包导出 | `world_sync/__init__.py:119-120`(import)+ `:209-210`(`__all__`) |
| ③ 宿主挂载 | `world_sync/init.py:513-514` → `env.write_event_with_state` / `env.read_entity_state` |
适配层 `world_sync/pbl_runtime_tx_sqlor.py:126,154` 提供同名薄包装(宿主连接池接线,**不重复实现事务逻辑**)。
### 2.3 无 HTTP 端点(重要)
M11b-2 **不新增** `.dspy` / `.ui`,因此 `scripts/load_path.py` 中**没有**这两个函数的权限条目,也不该有——它们是 Python 库函数,经 `load_world_sync()` 挂到 `ServerEnv` 供宿主调用,不走 HTTP 路由,不存在 401/403 面。`wwwroot/api/README-N-A-M11b2.md` 是「Not Applicable」的显式留档。正式声明见 `projects/pbls/docs/02-develop/m11b2c-rbac-path-declaration.md`。
### 2.4 事务与冲突契约(详版在 dev-notes.md `[M11b-2c]` 章 §2)
`BEGIN`(sqlite `BEGIN IMMEDIATE`)→ 会话级 `READ COMMITTED`(MySQL/PG;失败降级不阻断,靠乐观锁兜底并透出标志)→ 读 `state_version`(MySQL/PG 加 `FOR UPDATE`)→ `expected_version` 预检 → 状态 CAS 更新/INSERT → 事件 INSERT → `COMMIT`;任一步失败 `_rollback_quiet` + 明确异常(`ConcurrentStateConflict` / `StateWriteError` / `EventWriteError` / `TxAbortError` / `TxConfigError` / `ConnFactoryNotConfigured`),**两表 0 变更**,绝不吞错。
## 3. 陷阱(本模块改动前必读)
1. **别在 world_sync 侧建表**。DDL 归 `pbl_runtime_ext`;`world_sync/models/*.json` 只是消费方契约视图。改真源必须同步本副本并重跑 `m11b2b_column_diff.py` + `m11b2_selftest.py`。
2. **列名不许写死**。列集由 `pbl_runtime_authority` 从真源机械导出,`_assert_contract()` 在建 SQL 之前 fail-closed(`AuthorityResolutionError`)。新增列只改真源 JSON + 同步副本,代码里出现字面量列名清单 = 双写分叉隐患。
3. **定位键是 `tenant_id + session_id + entity_id`**,不是 `world_id`(表里没这列)。传 `world_id` 只被忽略——别指望它做隔离。
4. **`expected_version` 两级校验缺一不可**:预检给友好错误(带 expected/actual),CAS(`rowcount != 1`)防预检与写入之间的窄窗口。只留预检会丢失更新。
5. **状态行 INSERT 不带 `id`**(见 §1.2 偏离说明),且 UNIQUE 列空串必须归一为 `None`(`''` 会互相撞唯一键)。
6. **写序是先状态、后事件**。反过来会让事件携带一个可能被回滚的 `state_version`。
7. **`isolation_level_applied=False` 不等于失败**(sqlite 无该概念)。测试断言要按 dialect 分支。
8. **`conn_factory` 三级优先级**:显式注入(可传 DB URL)→ 复用 `conn` → 宿主 `ServerEnv` 连接池(`POOL_KEYS=("sqlor_pools","db_pools","pools","sqlorPools")`,库名键 `("pbl","world_sync","default")`)。拿不到池就抛 `ConnFactoryNotConfigured`,**禁止**回落到裸连或读 `env/{test,prod}.json` 凭据(project-directory-spec §8.1)。
9. **`m11b2_selftest.py` 的 `cross-check-sql` 检查器**:M11b-2c 重构后 `EVENT_COLUMNS = columns_of(EVENT_TABLE)`(运行时派生),旧正则抓字面量元组会误报「未能解析」。已由 commit `6532d8f` 改为三级解析(A 字面量 / B1 运行时 import / B2 静态回落)。再遇到同类 FAIL 先怀疑检查器而非业务代码。
10. **遗留测试文件是技术债不是验收依据**:`tests/test_m11b2_single_tx.py`、`tests/test_m11b2_wiring.py` 的 29 条失败源于脚手架 DDL 与真源不一致、仍用已废弃的 `event_id` 关键字、缺显式 dialect、`init.py` 相对路径等(详见 `m11b2c-tests-result.md` §4.1 逐条归因)。等价覆盖已在 `pbl_runtime_ext/tests/test_m11b2c_runtime_ext.py`(9 passed,含 2 条 P99 门禁)。重写需单列 rework 任务。
11. **广播/轮询不在此模块的 M11b-2 路径里**:post-commit 广播由 `init.py` 的 hook 把 `write_event_with_state` 包一层(M11b-3),3s 轮询兜底在 `pbl_runtime_polling.py`(M11b-4)。摘掉这两层不影响 M11b-2 写入正确性——排查时先确认看到的是哪一层。
12. **P99 数据是离线 sqlite/内存引擎实测**,不含网络 RTT 与真实 InnoDB 锁等待,只能作为逻辑时延下界;上线后在测试环境按 `pbl_runtime_latency.py` 口径复测。
## 4. 验证证据(本次实跑)
```
$ cd modules/world_sync && .venv/bin/python scripts/m11b2_selftest.py # exit code 0
PASS [mirror-sha256-equal] pbl_runtime_event 真源与镜像字节级一致 -> True
PASS [mirror-sha256-equal] pbl_entity_state 真源与镜像字节级一致 -> True
断言合计 554:PASS 554 / FAIL 0
RESULT: PASS
```
- 全文落盘:`projects/pbls/docs/02-develop/m11b2c-t2-selftest-rerun.log`(64695B)
- 真源侧:`pbl_runtime_ext` pytest `9 passed in 0.41s`(0 失败);`m11b_selftest.py` `总计 110 项,PASS 110,FAIL 0 → RESULT: PASS`
- P99(`m11b2c_p99_probe.py`,n=60,SLA=200ms):`write_event_with_state[INSERT] P99=0.2414ms` / `read_entity_state P99=0.2241ms` / `write_event_with_state[UPDATE] P99=0.4984ms`,均 PASS;`seq_no` 严格递增、读回 `entity_id` 一致性 PASS
- 模块 HEAD:`world_sync` @ `5e3c48d`、`pbl_runtime_ext` @ `9a79e63`(`git status --porcelain` 本任务无代码改动)
## 5. 关联文档
- 应用级开发说明(本里程碑正文):`projects/pbls/docs/02-develop/dev-notes.md` → `[M11b-2c] world_sync 库函数` 章
- 真源侧:`dev-notes-m11b2c-runtime-ext.md`;迁移:`dev-notes-m11b2a-world-sync-migration.md`;A3 修复:`m11b2c-a3-fix-notes.md`
- RBAC 口径:`m11b2c-rbac-path-declaration.md`;实跑留证:`m11b2c-tests-result.md` 及各 `m11b2c-tests-*-raw.log`
- 模块技能:`modules/world_sync/skill/SKILL.md`;上一份工作日志:`modules/world_sync/docs/work-log-2026-09-20.md`