R8-Fix-5.1:端到端关闭绝对截止期(End-to-End Closeout Absolute Deadline)修复方案
P0:8 秒预算不是端到端预算
R8-Fix-5 的 COLLECTOR_CLOSEOUT_SOFT_BUDGET_SECONDS = 8.0 实际只裁剪了 CoreProfile drain、source stop、consumer drain 与 ingress writer drain。其后的 raw_ingress.stop_worker()(内部硬编码 5s)、writer_executor.shutdown(wait=True)、recorder.finalize()、metric_recorder.finalize()、session_writer.seal()、online_report.close()、CLI 最终交付、WireOutput.close(timeout=5.0) 与解释器退出都各自持有私有超时,因此 Stop → Process EXIT 最坏情况远超 8 秒,而 Swift 宿主的 10 秒 watchdog 以 Process.isRunning 为准。
独立验收复现
SessionWriter.append_batch() 阻塞 2 秒 drain_writer(timeout=50ms) -> ~50ms partial-writer-drain-timeout stop_worker() -> ~2015ms <- 那 50ms 什么也没限制住
根因
- 预算没有向下传递:每阶段各自
asyncio.wait_for(..., timeout=私有常量),且remaining_budget_seconds()带max(0.05, …)兜底,过期后每阶段还能白拿 50ms。 - bounded wait 无法停止已在运行的 Python 线程;durable writer 当时跑在 非守护
ThreadPoolExecutor上,解释器退出时会 join 它。 - writer 无法 drain 时仍继续执行
finalize()/seal(),与被遗弃的 writer 线程形成写文件竞态。 WireOutput.close(timeout=5.0)在关闭链之后再 join 5 秒,8s + 5s = 13s 必撞宿主 watchdog。
修复主线
- 新增
closeout.py:CloseoutDeadline(锚定stop_control_received_ns,remaining_seconds()过期精确返回0.0,无 floor)、TERMINAL_EXIT_RESERVE_SECONDS = 0.75、CloseoutLedger阶段账本、run_bounded_sync_stage()。 - 新增
single_writer_executor.py:DaemonSingleWriterExecutor单 daemon 线程 +queue.Queue;shutdown(wait=False)遗弃而非 join。 realtime_ingress.py:drain_writer()/stop_worker()移除私有固定超时,改为接受deadline/reserve_seconds。realtime.py:全部关闭阶段共享一个 deadline + ledger;closeoutStatus在全部阶段结束后最后计算;新增 Minimal Partial Closeout(writer 异常后禁止全量 seal,只落只读 partial manifest,rawSHA256 = null);heartbeat 补 P50/P95/Max 与 tick gap。perfowl_collector_core_cli.py:emit_line()返回 sequence,flush_until(seq, timeout)有界 fence;close()由 deadline 裁剪;终端路径记录terminalStoppedEmittedMonotonicNs/processExitBeginMonotonicNs/wireFlushStatus并发有界closeout_exit。
目标
Stop -> Python Process EXIT worst case < 9s 正常路径 P95 < 3s 宿主机 10s watchdog 至少留 1s 余量
禁止项
不得修改 FPS / FrameTime / P95 FrameTime / Jitter / Refresh Rate 公式、FrameLifetime 语义、ActivityTrace 与 CoreProfile Source Owner、CapturePlan、MetricRegistry、Swift 10s gracefulTimeout、R8-Fix-5 的 pump 32events/1ms 与 filter 64messages/1ms 公平门禁;不得放宽 stale-source、不得用 1000/FPS 补 FrameTime、不得补 0/60/16.67、不得扩容 queue、不得删除 undrained/drop 诊断、不得把 partial 改写成 completed、不得恢复历史回放进入 live Stop;不得 reset/stash/clean/checkout 共享脏工作区。
测试 A–G
| 编号 | 场景 | 必须断言 |
|---|---|---|
| A | append_batch 阻塞 2s + 50ms drain 预算 | 整个 writer closeout < 300~400ms;worker 状态 abandoned/timed-out;accepted ≠ persisted;Session = partial |
| B | 真实 subprocess,writer job sleep(30s) | Python 进程退出 < 1s |
| C | 正常 writer | accepted == persisted;closeoutStatus = clean;executor 正常 join |
| D | recorder.finalize() 慢 | 有界;partial;失败阶段 = raw-finalize |
| E | seal 慢 | 有界;partial;失败阶段 = report-seal;report 未被 close |
| F | wire writer 阻塞 | terminal flush 有界;closeout_exit 仍发出;进程可退出 |
| G | 宿主契约 | worst-case closeout + wire close 仍留 ≥1s watchdog 余量 |
E2 门禁 G1–G17
G1 stop -> 进程退出 <9s (worst) / 正常 P95 <3s G2 所有阶段共享同一 CloseoutDeadline G3 remaining_seconds() 过期返回精确 0.0,无兜底 floor G4 重阶段不得花费 TERMINAL_EXIT_RESERVE_SECONDS G5 OrderedRawIngress.stop_worker() 无私有超时,且被 deadline 裁剪 G6 worker 无法 join 时被遗弃而非 join(daemon) G7 writer 异常 -> 禁止全量 seal,Session 封 partial G8 minimal partial manifest 不含可用 rawSHA256 G9 WireOutput.close 不再硬编码 5s G10 terminal wire flush 有 fence,超时记 timed-out G11 stopped != closeout 完成,三者分开记录 G12 heartbeat 提供 P50/P95/Max 且样本 bounded G13 stopDiagnostics 字段完整,null 不写成 0 G14 closeoutStatus 在全部阶段结束后最后计算 G15 测试 A-G 全部存在且通过 G16 公平性测试全绿,公式 / Source Owner / CapturePlan 未被修改 G17 Python 全量、Swift 全量、站点构建、git diff --check 全部通过
不可接受的假修复
只把 stop_worker 的 5s 降到 2s,而 executor.shutdown(wait=True) 依然阻塞; 只在 asyncio.wait_for 超时后就宣称"同步线程已取消"; writer 超时后仍然继续执行完整 seal; 用扩大 queue / 延长 Swift 超时来掩盖关闭问题。
E2 实现与验证结论(2026-09-18)
新增 test_closeout_deadline.py(25 项:Test A/C/D/E + deadline 算术、ledger、daemon executor、bounded stage)与 test_closeout_process_exit.py(4 项:Test B 真实 subprocess、Test F 阻塞 wire、Test G 宿主契约),全部通过。Python 全量 643/643 OK。状态:implemented · gate-open / E2 completed / E4 pending。完整实现细节、字段清单与实现回填见站点 devlog Canonical 文档。