deliver: 交付收口(引擎代为提交)

This commit is contained in:
agent.develop 2026-09-22 14:50:53 +08:00
parent 7f91a13e0d
commit aef48931c1
5 changed files with 1764 additions and 1708 deletions

View File

@ -0,0 +1,48 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""M11b-2c: 测试夹具 DDL 与列契约对齐补丁(只改 tests/,不动业务代码与表定义 JSON)。
根因(实测):tests/test_m11b2_single_tx.py 的 DDL_EVENT 把 id 声明为
`id INTEGER PRIMARY KEY AUTOINCREMENT`
而 world_sync 侧列契约真源 models/pbl_runtime_event.json 明确 id = str(32)、由应用层
new_row_id()(uuid4 hex)生成并在 INSERT 列集中显式提交(见该文件 summary.comment
「id 生成方 · 单一口径,QC #4」)。写路径插入 32 位十六进制字符串到 INTEGER 主键列
=> sqlite3.IntegrityError: datatype mismatch,TestNormalWrite/TestAtomicRollback/
TestConcurrency/TestLatency 全部连坐失败。属夹具建表与契约不一致,修夹具即可。
补丁做法:把 DDL_EVENT 中 id 列改为 `id TEXT PRIMARY KEY`(SQLite 下 TEXT 主键合法,
且与 str(32) 契约一致),去掉 AUTOINCREMENT(契约不依赖数据库自增)。
"""
import io
import os
import sys
TARGET = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"tests", "test_m11b2_single_tx.py")
OLD = " id INTEGER PRIMARY KEY AUTOINCREMENT,\n tenant_id TEXT NOT NULL,\n event_uid TEXT NOT NULL,"
NEW = " id TEXT PRIMARY KEY, -- M11b-2c fixture fix: contract id = str(32) app-generated (uuid4 hex), not AUTOINCREMENT\n tenant_id TEXT NOT NULL,\n event_uid TEXT NOT NULL,"
def main():
with io.open(TARGET, "r", encoding="utf-8") as f:
src = f.read()
n = src.count(OLD)
if n == 0:
if NEW.split(",")[0] in src:
print("SKIP: fixture DDL already patched (id TEXT PRIMARY KEY)")
return 0
print("FAIL: anchor text not found in %s" % TARGET)
return 1
if n > 1:
print("FAIL: anchor text found %d times, refusing ambiguous patch" % n)
return 1
src = src.replace(OLD, NEW)
with io.open(TARGET, "w", encoding="utf-8") as f:
f.write(src)
print("OK: patched %s (1 occurrence) -> id TEXT PRIMARY KEY" % TARGET)
return 0
if __name__ == "__main__":
sys.exit(main())

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
# M11b-4 自测留证 # M11b-4 自测留证
# 生成时间: 2026-09-22 13:56:43 +0800 # 生成时间: 2026-09-22 14:45:39 +0800
# 命令: cd modules/world_sync && .venv/bin/python scripts/m11b4_selftest.py # 命令: cd modules/world_sync && .venv/bin/python scripts/m11b4_selftest.py
# Python: 3.10.12 # Python: 3.10.12
@ -7,21 +7,21 @@
AC1 广播丢失 → 3s 轮询补漏(真实 3s 节奏) AC1 广播丢失 → 3s 轮询补漏(真实 3s 节奏)
============================================================================== ==============================================================================
interval=3.0s run() 结果={"polls": 1, "recovered_count": 1, "found": true, "stopped_by": "event_matched"} interval=3.0s run() 结果={"polls": 1, "recovered_count": 1, "found": true, "stopped_by": "event_matched"}
墙钟耗时=3000.5ms(含 1 次真实 3s sleep) on_recovered 回调次数=1 墙钟耗时=3000.6ms(含 1 次真实 3s sleep) on_recovered 回调次数=1
兜底计数器={"polls": 1, "queries": 1, "recovered": 1, "duplicated": 0, "missed_by_broadcast": 1, "timeouts": 0, "stops_on_notify": 0, "scenes": ["sc_ac1_1790056603"], "interval": 3.0, "timeout": 10.0} 兜底计数器={"polls": 1, "queries": 1, "recovered": 1, "duplicated": 0, "missed_by_broadcast": 1, "timeouts": 0, "stops_on_notify": 0, "scenes": ["sc_ac1_1790059539"], "interval": 3.0, "timeout": 10.0}
[PASS] AC1.1 轮询间隔常量 = 3.0s -> 3.0 [PASS] AC1.1 轮询间隔常量 = 3.0s -> 3.0
[PASS] AC1.2 广播丢失时 found=True(补到漏网事件) [PASS] AC1.2 广播丢失时 found=True(补到漏网事件)
[PASS] AC1.3 stopped_by=event_matched(非 timeout) -> event_matched [PASS] AC1.3 stopped_by=event_matched(非 timeout) -> event_matched
[PASS] AC1.4 补漏耗时 ≤ SLO 3000ms + 500ms 调度容差(1 个轮询周期内) -> 3000.5ms <= 3500.0ms [PASS] AC1.4 补漏耗时 ≤ SLO 3000ms + 500ms 调度容差(1 个轮询周期内) -> 3000.6ms <= 3500.0ms
[PASS] AC1.5 推进后的水位线 >= 事件 created_at -> 2026-09-22 05:56:43 >= 2026-09-22 05:56:43 [PASS] AC1.5 推进后的水位线 >= 事件 created_at -> 2026-09-22 06:45:39 >= 2026-09-22 06:45:39
============================================================================== ==============================================================================
AC1b 已投递事件不重复补漏(同秒撞秒 + event_uid 去重双保险) AC1b 已投递事件不重复补漏(同秒撞秒 + event_uid 去重双保险)
============================================================================== ==============================================================================
第1轮 recovered=1 第2轮 recovered=0(水位线=2026-09-22 05:56:43) 第1轮 recovered=1 第2轮 recovered=0(水位线=2026-09-22 06:45:39)
[PASS] AC1b.1 首轮补到 1 条 [PASS] AC1b.1 首轮补到 1 条
[PASS] AC1b.2 次轮 0 条(水位线+已投递集合去重) [PASS] AC1b.2 次轮 0 条(水位线+已投递集合去重)
同秒新事件 uid=ac1b-a50a12b3debf921e created_at=2026-09-22 05:56:43 -> recovered=1 found=True 同秒新事件 uid=ac1b-91ac6c3b6096a027 created_at=2026-09-22 06:45:39 -> recovered=1 found=True
[PASS] AC1b.3 同秒新事件仍被补到(>= 水位线 + NOT IN 去重) [PASS] AC1b.3 同秒新事件仍被补到(>= 水位线 + NOT IN 去重)
============================================================================== ==============================================================================
@ -36,9 +36,9 @@ AC1c 水位线格式守卫(ISO 格式必须被拒绝,否则静默丢事件
============================================================================== ==============================================================================
AC1d 广播正常 → 轮询立即停止(stopped_by=broadcast_received) AC1d 广播正常 → 轮询立即停止(stopped_by=broadcast_received)
============================================================================== ==============================================================================
run()={"polls": 1, "found": false, "stopped_by": "broadcast_received"} 墙钟=200.9ms(远小于 3s 周期) run()={"polls": 1, "found": false, "stopped_by": "broadcast_received"} 墙钟=200.8ms(远小于 3s 周期)
[PASS] AC1d.1 收到广播后停止 [PASS] AC1d.1 收到广播后停止
[PASS] AC1d.2 未走满轮询周期 -> 200.9ms [PASS] AC1d.2 未走满轮询周期 -> 200.8ms
[PASS] AC1d.3 notified 短路后 polls 计数=1 -> 1 [PASS] AC1d.3 notified 短路后 polls 计数=1 -> 1
============================================================================== ==============================================================================
@ -57,22 +57,22 @@ AC2 补漏查询 EXPLAIN:必须命中 ix_scene_created,禁止全表扫描
AC3 端到端时延基准:裁决→write_event_with_state→COMMIT→广播→消费端感知 AC3 端到端时延基准:裁决→write_event_with_state→COMMIT→广播→消费端感知
============================================================================== ==============================================================================
样本数 n=200 SLO=1000ms dialect=sqlite(file=m11b4.sqlite) 样本数 n=200 SLO=1000ms dialect=sqlite(file=m11b4.sqlite)
基准总耗时 0.22s 基准总耗时 0.14s
分段 P99: {"write_commit": 1.971, "broadcast_emit": 0.354, "consume_wait": 0.176} 分段 P99: {"write_commit": 0.735, "broadcast_emit": 0.044, "consume_wait": 0.018}
瓶颈段 : write_commit 瓶颈段 : write_commit
感知路径: broadcast=200 polling_fallback=0 lost=0 感知路径: broadcast=200 polling_fallback=0 lost=0
广播统计: {"published": 200, "dropped_disabled": 0, "failed": 0, "retried": 0, "direct_publish": 0, "queued": 200, "subscriber_errors": 0, "max_latency_ms": 0.408, "in_flight": 0, "latency_samples": 200, "last_error": null, "enabled": true, "channels": ["in_process"], "p50_latency_ms": 0.136, "p99_latency_ms": 0.292} 广播统计: {"published": 200, "dropped_disabled": 0, "failed": 0, "retried": 0, "direct_publish": 0, "queued": 200, "subscriber_errors": 0, "max_latency_ms": 0.14, "in_flight": 0, "latency_samples": 200, "last_error": null, "enabled": true, "channels": ["in_process"], "p50_latency_ms": 0.077, "p99_latency_ms": 0.101}
P50=1.042ms P90=1.232ms P99=1.997ms max=3.124ms P50=0.651ms P90=0.700ms P99=0.752ms max=0.934ms
裁决 : PASS 裁决 : PASS
[PASS] AC3.1 全部样本被感知(无丢失) -> lost=0 [PASS] AC3.1 全部样本被感知(无丢失) -> lost=0
[PASS] AC3.2 感知全部走广播(兜底未介入正常链路) -> 200/200 [PASS] AC3.2 感知全部走广播(兜底未介入正常链路) -> 200/200
[PASS] AC3.3 P99 ≤ 1000ms -> P99=1.997ms [PASS] AC3.3 P99 ≤ 1000ms -> P99=0.752ms
============================================================================== ==============================================================================
AC3b 广播丢失时的时延归入 polling_fallback(>1s 是设计使然,单独统计) AC3b 广播丢失时的时延归入 polling_fallback(>1s 是设计使然,单独统计)
============================================================================== ==============================================================================
兜底计数器: {"polls": 1, "queries": 1, "recovered": 1, "duplicated": 0, "missed_by_broadcast": 1, "timeouts": 0, "stops_on_notify": 0, "scenes": ["sc_x_lost"], "interval": 0.2, "timeout": 5.0} 兜底计数器: {"polls": 1, "queries": 1, "recovered": 1, "duplicated": 0, "missed_by_broadcast": 1, "timeouts": 0, "stops_on_notify": 0, "scenes": ["sc_x_lost"], "interval": 0.2, "timeout": 5.0}
样本: {"scene_id": "sc_x_lost", "event_id": "f3de147964fb4f879b5342139e6d7e88", "t_write_ms": 0.718, "t_broadcast_emit_ms": 0.004, "t_consume_ms": 250.654, "e2e_ms": 251.373, "via": "polling_fallback", "perceived": true} 样本: {"scene_id": "sc_x_lost", "event_id": "21b5e89192ea46b1b753182e1bae6810", "t_write_ms": 0.721, "t_broadcast_emit_ms": 0.005, "t_consume_ms": 251.23, "e2e_ms": 251.951, "via": "polling_fallback", "perceived": true}
[PASS] AC3b.1 广播丢失时经轮询兜底仍被感知 [PASS] AC3b.1 广播丢失时经轮询兜底仍被感知
[PASS] AC3b.2 报告把它与广播路径分开统计(不混入 P99 判定) [PASS] AC3b.2 报告把它与广播路径分开统计(不混入 P99 判定)
[PASS] AC3b.3 兜底确实发起了补漏查询(polls>=1 且 recovered>=1) -> polls=1 recovered=1 [PASS] AC3b.3 兜底确实发起了补漏查询(polls>=1 且 recovered>=1) -> polls=1 recovered=1
@ -130,7 +130,7 @@ AC5 对外门面可调用性(防漏注册导致 NameError)
OK AC5.4 fallback_stats 返回计数快照 OK AC5.4 fallback_stats 返回计数快照
31/31 通过 31/31 通过
时延报告: P50=1.042ms P90=1.232ms P99=1.997ms max=3.124ms verdict=PASS 时延报告: P50=0.651ms P90=0.700ms P99=0.752ms max=0.934ms verdict=PASS
报告 JSON 落盘: /d/pipeline/workspaces/0/sdlc_general/modules/world_sync/scripts/m11b4_latency_report.json 报告 JSON 落盘: /d/pipeline/workspaces/0/sdlc_general/modules/world_sync/scripts/m11b4_latency_report.json
RESULT: PASS FAILURES=0 RESULT: PASS FAILURES=0

View File

@ -87,7 +87,8 @@ polling 与写入端事务模块 import 零交集、补漏 SQL 只读 `pbl_runti
| `POLL_INTERVAL_S` | `3.0` | 轮询周期(验收口径「广播丢失 3s 内补漏」= 1 个周期) | | `POLL_INTERVAL_S` | `3.0` | 轮询周期(验收口径「广播丢失 3s 内补漏」= 1 个周期) |
| `POLL_TIMEOUT_S` | `30.0` | 兜底循环总超时默认值(超时即停并计 `timeouts`,不造僵尸定时器);selftest 显式传 `10.0` | | `POLL_TIMEOUT_S` | `30.0` | 兜底循环总超时默认值(超时即停并计 `timeouts`,不造僵尸定时器);selftest 显式传 `10.0` |
| `POLL_LIMIT` | `500` | 单轮补漏 LIMIT,防一次拉爆;剩余下一轮继续 | | `POLL_LIMIT` | `500` | 单轮补漏 LIMIT,防一次拉爆;剩余下一轮继续 |
| `FALLBACK_SLO_MS` | `3000.0` | 补漏 SLO = 1 个轮询周期 | | `FALLBACK_SLO_MS` | `3000.0` | 补漏 SLO = 1 个轮询周期。**AC1.4 判定式为 `wall_ms <= FALLBACK_SLO_MS + SLO_TOLERANCE_MS`(≤3500ms),不是 ≤3000ms** |
| `SLO_TOLERANCE_MS` | `500.0` | AC1.4 调度容差(源码常量,已三处同步导出/登记;来源=真实 `sleep(3.0)` 唤醒抖动 + 单次周期边界,墙钟恒 ≥3000ms)。测试侧禁止本地重复定义 |
| `SEEN_IDS_IN_SQL` | `200` | 已投递 `event_uid` 参与 SQL `NOT IN` 的最大长度(防参数爆炸) | | `SEEN_IDS_IN_SQL` | `200` | 已投递 `event_uid` 参与 SQL `NOT IN` 的最大长度(防参数爆炸) |
| `E2E_LATENCY_SLO_MS` | `1000.0` | 端到端感知时延 SLO(P99 口径,仅统计广播路径样本) | | `E2E_LATENCY_SLO_MS` | `1000.0` | 端到端感知时延 SLO(P99 口径,仅统计广播路径样本) |
@ -104,8 +105,15 @@ polling 与写入端事务模块 import 零交集、补漏 SQL 只读 `pbl_runti
- MySQL:`plan_uses_scene_index(explain_poll_query(...))` → True 才算合规;`type=ALL` 判 False。测试库凭据不可用时该项记 skipped,须在交付件标注环境受限(勿写 PASS)。 - MySQL:`plan_uses_scene_index(explain_poll_query(...))` → True 才算合规;`type=ALL` 判 False。测试库凭据不可用时该项记 skipped,须在交付件标注环境受限(勿写 PASS)。
### 消费端接线方式 ### 消费端接线方式
- 消费端订阅广播:`subscribe_broadcast(handler)`;handler 里调 `get_fallback().notify_broadcast(msg, created_at=write_result["created_at"])`(`msg` = M11b-3 广播消息 dict)—— 置位 `notified` 短路标记并推进水位线,**当轮立即停止轮询**(实测墙钟 200.8ms,未走满 3s 周期)。广播消息只有 ISO `commit_ts_text`,**不能**直接当水位线用,须把写入端返回的库内格式 `created_at` 传进来,否则水位线不推进。 - 消费端订阅广播:`subscribe_broadcast(handler)`;handler 里调 `get_fallback().notify_broadcast(msg, created_at=write_result["created_at"])`(`msg` = M11b-3 广播消息 dict)—— 置位 `notified` 短路标记并推进水位线,**当轮立即停止轮询**(留证运行 `run_at=2026-09-22 14:45:43 +0800`:AC1d 墙钟 200.8ms,未走满 3s 周期)。广播消息只有 ISO `commit_ts_text`,**不能**直接当水位线用,须把写入端返回的库内格式 `created_at` 传进来,否则水位线不推进。
- 兜底循环的启停由**宿主/消费端自己掌握**:把 `run_fallback()` / `PollingFallback.run()` 放进宿主自己的定时器或线程。**模块不自建后台线程**,避免污染写入端生命周期。 - 兜底循环的启停由**宿主/消费端自己掌握**:把 `run_fallback()` / `PollingFallback.run()` 放进宿主自己的定时器或线程。**模块不自建后台线程**,避免污染写入端生命周期。
### 留证实测数值(唯一数值源:`scripts/m11b4_latency_report.json`,`run_at=2026-09-22 14:45:43 +0800`,n=200,sqlite)
- 端到端感知时延:`P50=0.651ms / P90=0.700ms / P99=0.752ms / max=0.934ms`,`slo_ms=1000.0`、`slo_passed=true`、`verdict=PASS`;感知路径 `via_broadcast=200 / via_polling_fallback=0 / lost=0`。
- 分段 P99:`write_commit=0.735 / broadcast_emit=0.044 / consume_wait=0.018`,`bottleneck_segment=write_commit`。
- 广播侧 `broadcast_stats`:`p50_latency_ms=0.077 / p99_latency_ms=0.101 / max_latency_ms=0.14`,`published=200 / queued=200 / failed=0 / retried=0 / dropped_disabled=0 / subscriber_errors=0 / in_flight=0 / last_error=null`,通道 `["in_process"]`。
- 兜底介入路径(AC3b 加速参数 interval=0.2s / timeout=5.0s):`e2e_ms=251.951`、`via=polling_fallback`、`perceived=true`、`polls=1 / recovered=1 / missed_by_broadcast=1`。
- 引用上述数字时**逐字段照抄该 JSON 并标注 `run_at`**;改代码重跑后必须同步刷新本文与 dev-notes(QC #12)。
- 广播丢失(`missed_by_broadcast` 计数 +1)时靠轮询补漏,该路径样本时延 ≥ 1 个周期,由 `via_polling_fallback` 单独统计,**不混入 P99 ≤ 1s 判定**(否则会把「兜底代价」误报成「链路超标」)。 - 广播丢失(`missed_by_broadcast` 计数 +1)时靠轮询补漏,该路径样本时延 ≥ 1 个周期,由 `via_polling_fallback` 单独统计,**不混入 P99 ≤ 1s 判定**(否则会把「兜底代价」误报成「链路超标」)。
## 陷阱 ## 陷阱

View File

@ -72,7 +72,7 @@ TransactionAborted = errs.TransactionAborted
DDL_EVENT = """ DDL_EVENT = """
CREATE TABLE IF NOT EXISTS pbl_runtime_event ( CREATE TABLE IF NOT EXISTS pbl_runtime_event (
id INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT PRIMARY KEY, -- M11b-2c fixture fix: contract id = str(32) app-generated (uuid4 hex), not AUTOINCREMENT
tenant_id TEXT NOT NULL, tenant_id TEXT NOT NULL,
event_uid TEXT NOT NULL, event_uid TEXT NOT NULL,
event_code TEXT, event_code TEXT,