69 lines
5.0 KiB
Markdown
69 lines
5.0 KiB
Markdown
---
|
||
name: world_sync
|
||
description: world_sync 模块技能文档。
|
||
---
|
||
|
||
# world_sync 模块
|
||
|
||
## 概述
|
||
世界数据同步模块,管理世界数据的导入/导出/合并同步记录与执行。依赖 world 模块(world 表)、appbase(appcodes 字典 sync_type/sync_status)。通过 load_world_sync() 挂载。
|
||
|
||
自 M11b-2/M11b-3 起,本模块同时承载 **scense_runtime 服务端权威写入薄扩展**:单事务事件+状态写入(`pbl_runtime_authority.write_event_with_state`)与**事务提交后广播**(`pbl_runtime_broadcast`)。
|
||
|
||
## 数据模型
|
||
### 表 world_sync(models/world_sync.json)
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| id | str(32) | 主键 |
|
||
| world_id | str(32) | 所属世界(→world.id) |
|
||
| sync_type | str(16) | 同步类型(0导入/1导出/2合并) |
|
||
| source | str(255) | 数据来源 |
|
||
| status | str(16) | 状态(appcodes sync_status) |
|
||
| result_json | text | 同步结果 JSON |
|
||
| sync_date | date | 同步日期(业务日期) |
|
||
| created_at | timestamp | 创建时间 |
|
||
|
||
- 主键:["id"];索引:idx_sync_world(world_id)
|
||
- 字典:world_id→world、sync_type→appcodes_kv(parentid='sync_type')、status→appcodes_kv(parentid='sync_status')
|
||
|
||
### 运行时权威表(M11b-2,models/)
|
||
`pbl_runtime_event`(append-only 事件)、`pbl_entity_state`(实体状态,state_version 服务端权威)、
|
||
`pbl_world_state_snapshot`(世界快照)。所有读写强制 `tenant_id` 打头,缺失 fail-closed。
|
||
|
||
## 关键接口
|
||
- list_world_syncs / get_world_sync / create_world_sync / update_world_sync / delete_world_sync / execute_world_sync
|
||
- wwwroot/api/world_sync.dspy(同步契约)、create/world_sync_update/world_sync_delete.dspy、get_search_*.dspy
|
||
- M11b-2:`write_event_with_state(event_data, state_update, ...)` —— 事件 + 状态在**同一事务**内提交
|
||
- M11b-3 广播 API(`world_sync/pbl_runtime_broadcast.py`,已在 `__init__.py` 导出):
|
||
- `install_broadcast_hook()` / `is_broadcast_hook_installed()`:把 `write_event_with_state` 包成 COMMIT 后广播(幂等,`load_world_sync()` 已自动调用)
|
||
- `emit_post_commit_broadcast(write_result, context=...)`:手动发一条
|
||
- `wrap_with_post_commit_broadcast(fn)`:包装任意返回事务结果的函数
|
||
- `configure_broadcast(channels=..., webhook_urls=..., redis_url=..., queue_size=2000, retry_attempts=2, retry_backoff_s=0.02, enabled=True)`:宿主启动时配置通道
|
||
- `subscribe_broadcast(handler)` / `unsubscribe_broadcast(handler)`:进程内订阅
|
||
- `broadcast_stats()` / `broadcast_last_message()` / `broadcast_latency_percentile_ms(99)` / `drain_broadcast(timeout=2.0)` / `shutdown_broadcast()`:观测与优雅退出
|
||
- 通道类:`InProcessChannel`、`WebhookChannel`、`RedisPubSubChannel`、`NoopChannel`(均继承 `BaseChannel`,`publish(message)`)
|
||
|
||
## 事务提交后广播(M11b-3)要点
|
||
- **触发点**:仅事务 COMMIT 成功之后(`commit_ts` 即延迟起点);事务回滚不广播。
|
||
- **异步**:单 dispatcher 线程 + 有界 `queue.Queue(2000)`,`submit` 用 `put_nowait` 微秒返回,不阻塞事务返回;队列满降级为当前线程直发一次。
|
||
- **SLO**:`BROADCAST_LATENCY_SLO_MS = 300.0`,口径 P99;实测 n=200 p50=2.4ms / p99=4.1ms / max=4.2ms(`scripts/m11b3_selftest_output.txt`)。
|
||
- **失败语义**:fire-and-forget + 有限重试;通道/订阅者异常一律吞掉并 `ERROR` 日志 + `stats["failed"]` 计数,**绝不回滚已提交事务**、绝不外抛给业务调用方。
|
||
- **无轮询**:广播路径不含 `time.sleep` 轮询、不读 DB;等待靠 `Queue.get(block=True)` 条件变量。
|
||
- **环境变量**:`PBL_BROADCAST_DISABLED`(全局关闭)、`PBL_BROADCAST_WEBHOOK_URLS`(逗号分隔 webhook)、`PBL_BROADCAST_REDIS_ENABLED` + `PBL_BROADCAST_REDIS_URL`(Redis pub/sub)。
|
||
|
||
## 陷阱
|
||
- 库名不硬编码:.py 用 ServerEnv().get_module_dbname('world_sync'),.dspy 用 get_sor_context(request._run_ns,'world_sync')
|
||
- dspy 无 import
|
||
- create/execute 必须设置 created_at、sync_date,否则 sor.C 静默丢记录
|
||
- update 剔除 _text 后缀
|
||
- codes.table 引用 world 不用 module.table 点号
|
||
- **广播 hook 幂等**:`install_broadcast_hook()` 重复调用不得重复包装(已用 `__wrapped_post_commit__` 标记),否则一条事件广播多次
|
||
- **广播内禁止再写事务/再发广播**:dispatcher 线程只做 publish,回写 DB 会造成递归与延迟失真
|
||
- **测试断言前用 `drain_broadcast()` 等待,禁止 sleep 轮询**(QC 会按「引入轮询」退回)
|
||
- `RedisPubSubChannel` 惰性 import `redis`:环境缺包只 `ChannelUnavailable` 跳过,不影响 InProcess/webhook 通道,不要让 Redis 成为硬依赖
|
||
- `scripts/sync_code_to_app.py` 的 `collect()` 必须以 `WS_ROOT` 拼绝对路径 —— 用相对路径 `isdir` 会恒返回空清单,导致 `--check` 打印假 PASS(M11b-3 已修,改脚本时勿回退)
|
||
|
||
## 依赖
|
||
- world、appbase、rbac
|
||
- M11b-2/M11b-3 广播零第三方硬依赖(stdlib:threading/queue/urllib/hashlib;redis 可选)
|