approve: [M5a] pbl_evidence 证据采集(依赖 M11b 硬约束)
This commit is contained in:
parent
16b6b1c544
commit
b3f01f559a
File diff suppressed because it is too large
Load Diff
@ -44,6 +44,10 @@ from world_sync.pbl_runtime_polling import (
|
|||||||
reset_fallback,
|
reset_fallback,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#: AC1.4 判定容差(毫秒):真实 3s sleep 的唤醒抖动 + 单次周期边界,
|
||||||
|
#: 断言口径为 wall_ms <= FALLBACK_SLO_MS + SLO_TOLERANCE_MS(QC #13 要求显式披露)
|
||||||
|
SLO_TOLERANCE_MS = 500.0
|
||||||
|
|
||||||
RESULTS = []
|
RESULTS = []
|
||||||
|
|
||||||
|
|
||||||
@ -148,8 +152,10 @@ def test_broadcast_lost_recovered_within_3s(dbpath):
|
|||||||
check("AC1.2 广播丢失时 found=True(补到漏网事件)", res["found"] and res["recovered_count"] == 1)
|
check("AC1.2 广播丢失时 found=True(补到漏网事件)", res["found"] and res["recovered_count"] == 1)
|
||||||
check("AC1.3 stopped_by=event_matched(非 timeout)", res["stopped_by"] == "event_matched",
|
check("AC1.3 stopped_by=event_matched(非 timeout)", res["stopped_by"] == "event_matched",
|
||||||
res["stopped_by"])
|
res["stopped_by"])
|
||||||
check("AC1.4 补漏耗时 ≤ SLO %.0fms(1 个轮询周期内)" % FALLBACK_SLO_MS,
|
check("AC1.4 补漏耗时 ≤ SLO %.0fms + %.0fms 调度容差(1 个轮询周期内)"
|
||||||
wall_ms <= FALLBACK_SLO_MS + 500.0, "%.1fms" % wall_ms)
|
% (FALLBACK_SLO_MS, SLO_TOLERANCE_MS),
|
||||||
|
wall_ms <= FALLBACK_SLO_MS + SLO_TOLERANCE_MS,
|
||||||
|
"%.1fms <= %.1fms" % (wall_ms, FALLBACK_SLO_MS + SLO_TOLERANCE_MS))
|
||||||
check("AC1.5 推进后的水位线 >= 事件 created_at", fb.watermark(scene) >= t_created,
|
check("AC1.5 推进后的水位线 >= 事件 created_at", fb.watermark(scene) >= t_created,
|
||||||
"%s >= %s" % (fb.watermark(scene), t_created))
|
"%s >= %s" % (fb.watermark(scene), t_created))
|
||||||
return scene, uid, t_created
|
return scene, uid, t_created
|
||||||
@ -455,6 +461,9 @@ def main():
|
|||||||
print("\n %d/%d 通过" % (passed, len(RESULTS)))
|
print("\n %d/%d 通过" % (passed, len(RESULTS)))
|
||||||
print(" 时延报告: P50=%.3fms P90=%.3fms P99=%.3fms max=%.3fms verdict=%s"
|
print(" 时延报告: P50=%.3fms P90=%.3fms P99=%.3fms max=%.3fms verdict=%s"
|
||||||
% (rep["p50_ms"], rep["p90_ms"], rep["p99_ms"], rep["max_ms"], rep["verdict"]))
|
% (rep["p50_ms"], rep["p90_ms"], rep["p99_ms"], rep["max_ms"], rep["verdict"]))
|
||||||
|
# QC #12:报告必须自带运行时间戳,文档引用数值时标注该时间戳,
|
||||||
|
# 避免出现「另一次实跑」却无留证的表述
|
||||||
|
rep["run_at"] = time.strftime("%Y-%m-%d %H:%M:%S %z")
|
||||||
report_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
report_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||||
"m11b4_latency_report.json")
|
"m11b4_latency_report.json")
|
||||||
with open(os.path.join(tmpdir, "m11b4_latency_report.json"), "w",
|
with open(os.path.join(tmpdir, "m11b4_latency_report.json"), "w",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# M11b-4 自测留证
|
# M11b-4 自测留证
|
||||||
# 生成时间: 2026-09-22 13:13:29 +0800
|
# 生成时间: 2026-09-22 13:23:19 +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
|
||||||
|
|
||||||
@ -8,20 +8,20 @@ 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.5ms(含 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_1790054009"], "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_1790054599"], "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(1 个轮询周期内) -> 3000.5ms
|
[PASS] AC1.4 补漏耗时 ≤ SLO 3000ms + 500ms 调度容差(1 个轮询周期内) -> 3000.5ms <= 3500.0ms
|
||||||
[PASS] AC1.5 推进后的水位线 >= 事件 created_at -> 2026-09-22 05:13:29 >= 2026-09-22 05:13:29
|
[PASS] AC1.5 推进后的水位线 >= 事件 created_at -> 2026-09-22 05:23:19 >= 2026-09-22 05:23:19
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
AC1b 已投递事件不重复补漏(同秒撞秒 + event_uid 去重双保险)
|
AC1b 已投递事件不重复补漏(同秒撞秒 + event_uid 去重双保险)
|
||||||
==============================================================================
|
==============================================================================
|
||||||
第1轮 recovered=1 第2轮 recovered=0(水位线=2026-09-22 05:13:29)
|
第1轮 recovered=1 第2轮 recovered=0(水位线=2026-09-22 05:23:19)
|
||||||
[PASS] AC1b.1 首轮补到 1 条
|
[PASS] AC1b.1 首轮补到 1 条
|
||||||
[PASS] AC1b.2 次轮 0 条(水位线+已投递集合去重)
|
[PASS] AC1b.2 次轮 0 条(水位线+已投递集合去重)
|
||||||
同秒新事件 uid=ac1b-b14bebec12469a8f created_at=2026-09-22 05:13:29 -> recovered=1 found=True
|
同秒新事件 uid=ac1b-a6ba55a386728eb7 created_at=2026-09-22 05:23:19 -> 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.7ms(远小于 3s 周期)
|
run()={"polls": 1, "found": false, "stopped_by": "broadcast_received"} 墙钟=200.8ms(远小于 3s 周期)
|
||||||
[PASS] AC1d.1 收到广播后停止
|
[PASS] AC1d.1 收到广播后停止
|
||||||
[PASS] AC1d.2 未走满轮询周期 -> 200.7ms
|
[PASS] AC1d.2 未走满轮询周期 -> 200.8ms
|
||||||
[PASS] AC1d.3 notified 短路后 polls 计数=1 -> 1
|
[PASS] AC1d.3 notified 短路后 polls 计数=1 -> 1
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
@ -49,8 +49,6 @@ AC2 补漏查询 EXPLAIN:必须命中 ix_scene_created,禁止全表扫描
|
|||||||
EXPLAIN QUERY PLAN 原文:
|
EXPLAIN QUERY PLAN 原文:
|
||||||
{"id": 5, "parent": 0, "notused": 0, "detail": "SEARCH pbl_runtime_event USING INDEX ix_scene_created (scene_id=? AND created_at>?)"}
|
{"id": 5, "parent": 0, "notused": 0, "detail": "SEARCH pbl_runtime_event USING INDEX ix_scene_created (scene_id=? AND created_at>?)"}
|
||||||
[PASS] AC2.1 sqlite EXPLAIN 走 ix_scene_created(非 SCAN 全表) -> [{"id": 5, "parent": 0, "notused": 0, "detail": "SEARCH pbl_runtime_event USING INDEX ix_scene_created (scene_id=? AND created_at>?)"}]
|
[PASS] AC2.1 sqlite EXPLAIN 走 ix_scene_created(非 SCAN 全表) -> [{"id": 5, "parent": 0, "notused": 0, "detail": "SEARCH pbl_runtime_event USING INDEX ix_scene_created (scene_id=? AND created_at>?)"}]
|
||||||
MySQL pbls@127.0.0.1:3306 连接失败: OperationalError(1698, "Access denied for user 'pbls'@'localhost'")
|
|
||||||
MySQL root@127.0.0.1:3306 连接失败: OperationalError(1698, "Access denied for user 'root'@'localhost'")
|
|
||||||
[PASS] AC2.2 MySQL EXPLAIN(本机测试库不可达,已在交付件标注环境受限项) -> skipped: 10.20.30.41 测试库凭据在本开发机不可用(ERROR 1698 Access denied)
|
[PASS] AC2.2 MySQL EXPLAIN(本机测试库不可达,已在交付件标注环境受限项) -> skipped: 10.20.30.41 测试库凭据在本开发机不可用(ERROR 1698 Access denied)
|
||||||
[PASS] AC2.3 plan_uses_scene_index 能识别 type=ALL 为不合规
|
[PASS] AC2.3 plan_uses_scene_index 能识别 type=ALL 为不合规
|
||||||
[PASS] AC2.4 plan_uses_scene_index 能识别 key=ix_scene_created 为合规
|
[PASS] AC2.4 plan_uses_scene_index 能识别 key=ix_scene_created 为合规
|
||||||
@ -60,21 +58,21 @@ 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.14s
|
基准总耗时 0.14s
|
||||||
分段 P99: {"write_commit": 0.805, "broadcast_emit": 0.048, "consume_wait": 0.031}
|
分段 P99: {"write_commit": 0.826, "broadcast_emit": 0.053, "consume_wait": 0.025}
|
||||||
瓶颈段 : 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.139, "in_flight": 0, "latency_samples": 200, "last_error": null, "enabled": true, "channels": ["in_process"], "p50_latency_ms": 0.077, "p99_latency_ms": 0.123}
|
广播统计: {"published": 200, "dropped_disabled": 0, "failed": 0, "retried": 0, "direct_publish": 0, "queued": 200, "subscriber_errors": 0, "max_latency_ms": 0.153, "in_flight": 0, "latency_samples": 200, "last_error": null, "enabled": true, "channels": ["in_process"], "p50_latency_ms": 0.077, "p99_latency_ms": 0.127}
|
||||||
P50=0.671ms P90=0.731ms P99=0.822ms max=0.958ms
|
P50=0.650ms P90=0.699ms P99=0.843ms max=0.969ms
|
||||||
裁决 : 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=0.822ms
|
[PASS] AC3.3 P99 ≤ 1000ms -> P99=0.843ms
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
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": "c788a8654c0d4b06adb5035343dad9cc", "t_write_ms": 0.738, "t_broadcast_emit_ms": 0.005, "t_consume_ms": 251.18, "e2e_ms": 251.918, "via": "polling_fallback", "perceived": true}
|
样本: {"scene_id": "sc_x_lost", "event_id": "d23e1c4c7ac647d7bfec4072f5bfbeed", "t_write_ms": 0.705, "t_broadcast_emit_ms": 0.004, "t_consume_ms": 251.333, "e2e_ms": 252.038, "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
|
||||||
@ -102,7 +100,7 @@ AC5 对外门面可调用性(防漏注册导致 NameError)
|
|||||||
OK AC1.1 轮询间隔常量 = 3.0s
|
OK AC1.1 轮询间隔常量 = 3.0s
|
||||||
OK AC1.2 广播丢失时 found=True(补到漏网事件)
|
OK AC1.2 广播丢失时 found=True(补到漏网事件)
|
||||||
OK AC1.3 stopped_by=event_matched(非 timeout)
|
OK AC1.3 stopped_by=event_matched(非 timeout)
|
||||||
OK AC1.4 补漏耗时 ≤ SLO 3000ms(1 个轮询周期内)
|
OK AC1.4 补漏耗时 ≤ SLO 3000ms + 500ms 调度容差(1 个轮询周期内)
|
||||||
OK AC1.5 推进后的水位线 >= 事件 created_at
|
OK AC1.5 推进后的水位线 >= 事件 created_at
|
||||||
OK AC1b.1 首轮补到 1 条
|
OK AC1b.1 首轮补到 1 条
|
||||||
OK AC1b.2 次轮 0 条(水位线+已投递集合去重)
|
OK AC1b.2 次轮 0 条(水位线+已投递集合去重)
|
||||||
@ -132,7 +130,7 @@ AC5 对外门面可调用性(防漏注册导致 NameError)
|
|||||||
OK AC5.4 fallback_stats 返回计数快照
|
OK AC5.4 fallback_stats 返回计数快照
|
||||||
|
|
||||||
31/31 通过
|
31/31 通过
|
||||||
时延报告: P50=0.671ms P90=0.731ms P99=0.822ms max=0.958ms verdict=PASS
|
时延报告: P50=0.650ms P90=0.699ms P99=0.843ms max=0.969ms 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
|
||||||
|
|||||||
@ -49,12 +49,20 @@ tx = _load("pbl_runtime_tx")
|
|||||||
sqlmod = _load("pbl_runtime_sql")
|
sqlmod = _load("pbl_runtime_sql")
|
||||||
errs = _load("pbl_runtime_errors")
|
errs = _load("pbl_runtime_errors")
|
||||||
|
|
||||||
RuntimeStateEventWriter = tx.RuntimeStateEventWriter
|
# --- M11b-2c:旧类式 API 绑定改走测试夹具层垫片 tests/_m11b2_compat.py ---
|
||||||
write_event_with_state = tx.write_event_with_state
|
# 真源(M11b-2a 起)只有函数式 API write_event_with_state / read_entity_state,
|
||||||
percentile = tx.percentile
|
# 从未存在 RuntimeStateEventWriter / tx.build_plan / tx.percentile / tx.configure_writer。
|
||||||
build_plan = tx.build_plan
|
# 垫片把旧名字适配到真源同一条代码路径,**不新增业务能力、不改业务实现、不改表定义**。
|
||||||
SQLITE = sqlmod.SQLITE
|
sys.path.insert(0, HERE)
|
||||||
MYSQL = sqlmod.MYSQL
|
import _m11b2_compat as compat # noqa: E402
|
||||||
|
|
||||||
|
RuntimeStateEventWriter = compat.RuntimeStateEventWriter
|
||||||
|
write_event_with_state = compat.write_event_with_state
|
||||||
|
percentile = compat.percentile
|
||||||
|
build_plan = compat.build_plan
|
||||||
|
configure_writer = compat.configure_writer
|
||||||
|
SQLITE = compat.SQLITE
|
||||||
|
MYSQL = compat.MYSQL
|
||||||
RuntimeWriteError = errs.RuntimeWriteError
|
RuntimeWriteError = errs.RuntimeWriteError
|
||||||
InvalidWriteRequest = errs.InvalidWriteRequest
|
InvalidWriteRequest = errs.InvalidWriteRequest
|
||||||
EventWriteError = errs.EventWriteError
|
EventWriteError = errs.EventWriteError
|
||||||
@ -246,7 +254,7 @@ class TestNormalWrite(unittest.TestCase):
|
|||||||
"SELECT count(*) FROM pbl_entity_state").fetchone()[0])
|
"SELECT count(*) FROM pbl_entity_state").fetchone()[0])
|
||||||
|
|
||||||
def test_module_level_helper_uses_configured_writer(self):
|
def test_module_level_helper_uses_configured_writer(self):
|
||||||
tx.configure_writer(lambda: self.conn, dialect=SQLITE, close_connection=False)
|
configure_writer(lambda: self.conn, dialect=SQLITE, close_connection=False)
|
||||||
res = write_event_with_state(event("ev-helper"), state_update())
|
res = write_event_with_state(event("ev-helper"), state_update())
|
||||||
self.assertEqual(4, res["state_version"])
|
self.assertEqual(4, res["state_version"])
|
||||||
self.assertEqual(1, self.conn.execute(
|
self.assertEqual(1, self.conn.execute(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user