fix(pm2): 增加代际安全的机群关停协议 - #599
Conversation
06b2dc0 to
09dca6d
Compare
09dca6d to
b9f7750
Compare
|
已将 #599 的 3 个 PM2 协议提交重放到修复后的 #598 head 冲突处理保留了 #597 的 dashboard protected-session mutation guard,并并入认证 supervisor shutdown route; 验证: 未执行真实 PM2/daemon/Riff stop/restart。PR 继续保持 Draft,等 #598 合入后去掉父 PR diff、再跑最新 master 回归并转 Ready。 |
独立复审 #599 @
|
首审 #599 PM2 机群关停 @
|
deepcoldy
left a comment
There was a problem hiding this comment.
第二轮独立复审结论:核心关停安全链路未发现 blocker,但发现 1 个可稳定复现的 P2,建议修复后再合。
[P2] prepared 报告等待窗口短于合法 fleet 启动事务,导致报告丢失或归因到下一次崩溃
- src/core/restart-report.ts:106-119 对 prepared intent 只等待一次,默认 45s;超时后直接返回。
- src/daemon.ts:20960-20972 只在 primary daemon 启动 5s 后调用一次,没有后续重试。
- 但本 PR 自己允许启动命令最多 30s,验证至少 60s,并按进程数放大到 processCount * 2s。31 bot + dashboard 时验证预算就是 64s;因此合法事务完全可能在 reporter 的最终一次 poll 之后才 commit。
行为探针已复现:
first waiter expiry -> {sent:0,file:true}
commit -> {sent:0,file:true}
next daemon startup -> {sent:1,file:false}
即:本次成功的主动重启没有报告;若 10 分钟有效期内随后发生一次非主动崩溃/重启,旧 breadcrumb 会在那次启动被领取,报告被错误归因。若没有后续启动,则报告静默丢失。现有测试只覆盖「commit 发生在 45s 等待内」,没有覆盖超过最后一次 poll 的合法 commit。
建议让 reporter 对 durable prepared 状态持续等到 commit / abort / stale,或由 commit 提供可靠的通知/重试;只把 45s 调到 60s 仍小于可扩展的事务总预算。请补一个行为测试:commit 晚于旧 45s horizon,仍必须在同一次启动中且仅一次发出,之后的 crash startup 不得消费旧报告。
核心安全复核
没有发现 Riff 三阶段关停、PM2 预算、精确 PM2 ID 补偿、PID/starttime 代际围栏、supervisor 双层认证上的 blocker。额外跑了 32 进程 fleet 对抗探针:
- 32/32 正常退出:单批并发完成,无补偿,live=0。
- 1 个 generation 拒绝、31 个退出:等待预算后仅恢复 31 个 offline PM2 entry,拒绝者保持原 generation,不误杀、不重启 live 进程。
验证与基线
- PR head:101ac2110
- 审核期间 master 已前进到 815d9fe(#780),所以当前 head 不再包含最新 master;不过 GitHub 显示可干净合并。我在该最新 master + PR head 的真实 merge tree 上验证:
- pnpm build ✅
- 相关 19 文件 / 207 tests ✅
- 完整 pnpm test ✅(exit 0)
- 未部署、未重启 live。
Claude 首审提出的源码文本 contract 测试属于测试质量债;已有 loopback 行为测试覆盖核心路径,我不把它单独列为覆盖 blocker。
净结论:先修上述 P2,并基于合入 #780 后的新 head 重跑 restart-report 与 merge-tree 验证;修前不建议合码。
认可二审 P2(restart-report 45s poll vs 大 fleet start >45s)—— 我独立核实两半都成立二审提出的 P2 我没有直接采信,独立追了两条链路,确认成立、定级 P2 恰当(非 blocker): 报告侧(restart-report.ts):
intent 时序侧(restart-intent-store.ts + pm2-start-transaction.ts):
危害范围:仅影响 restart-report DM 的送达/归因(一次该发的重启报告漏发 + 旧 breadcrumb 在下次启动误报为「本次是有意重启」),不涉及孤儿 worker / 血缘 / 数据丢失——所以 P2 而非 blocker,与二审定级一致。 修法方向(供作者/拍板参考):把 这条不改我首审对核心关停/PID/认证链路「无 blocker」的结论——它在报告层,是独立的 P2。合并顺序/是否本 PR 内修由孙晓雪/申晗拍板;未经确认不合码、不部署。 |
P2 修复完成(e375d4fc1)按双审发现的 restart-report 时序问题补丁如下:
影响面:只改 primary daemon 的重启摘要后台任务与对应单测;不触碰 Riff 关停、worker、PM2 fleet、supervisor 认证,也不区分 CLI/后端/平台。 验证:
下一步按群内要求重新双审:先 Claude 独立首审本 delta,再由 Codex 二审。 |
复审 #599 P2 修复 @
|
deepcoldy
left a comment
There was a problem hiding this comment.
Codex 第二轮复审 @ e375d4f:P2 修复通过,未发现新 blocker / major;可以进入用户合码确认。
独立核验
- 真实 head e375d4f,包含最新 master e5a9e99,GitHub MERGEABLE。
- 生产路径不再用固定 45s 猜测 fleet 事务结束;prepared 会持续轮询,直到 durable state 变成 committed / aborted / stale。
- production 使用真实 Date.now;restart intent 10 分钟 freshness 是实际终止上限。当前 32 进程 fleet 的 start+verify 最大预算约 94s,余量充足。
- report 任务由 primary daemon fire-and-forget 启动;shutdown 不 await 它,最终 process.exit(90),500ms await 轮询不会 busy-spin,也不会延长关停预算。
- 显式 preparedCommitWaitMs 仍保留 caller/test ceiling;旧 45s 后 commit 的新测试对旧实现会失败,属于有效行为回归测试。
额外对抗探针
我没有只复跑 mock clock:
- 用独立 Node 子进程在 reporter 已观察到 prepared 后提交 intent;父 daemon 下一轮轮询成功发送一次并清除凭据。
- 对 committed intent 启动 8 个跨进程并发 claimant:恰好 1 个得到 claimed,另外 7 个得到 absent,证明锁内 claim+delete 没扩大重复发送面。
探针结果:
crossProcessCommit = sent-once
concurrentClaims = 1 claimed + 7 absent
验证
- pnpm build ✅
- 相关 5 文件 / 97 tests ✅
- Claude 复审扩展集:8 文件 / 123 tests ✅
- 全量套件两次只有高并发 import hook 超时;同名超时在最新 master 同机可复现,涉及失败文件聚焦复跑 74/74 ✅,非本补丁回归。
- 未部署、未重启 live。
非阻塞遗留
claim 后先删除 breadcrumb、再调用 Lark;若发送本身失败,本次通知仍会丢。这是旧实现既有的 at-most-once 语义,不是本补丁新增,也不影响本次确认的 45s 漏报修复。若要提升为可靠投递,建议单独做 durable outbox + 飞书稳定 uuid 去重,不能简单把删除挪到发送后,否则崩溃窗口会产生重复 DM。
净结论:双审通过。#599 的核心关停链路继续保持无 blocker;restart-report P2 已闭环。等待孙晓雪/申晗明确确认后再合码,不部署、不重启 live。
|
🚀 Released in v3.11.0 |
这次贡献解决什么
PM2 管理的是一整组 Botmux daemon。旧的 stop / restart 主要按进程名发信号、再修改 PM2 registry;当 daemon 正在做 Riff 的 prepare / persist / commit 关闭协议时,如果 PM2 提前 SIGKILL、自动拉起同名 successor,或 CLI 读到过期的 pm2 jlist,就可能出现「旧进程还没安全退完,新一代已经起来」或「信号死亡被误认成正常退出,PM2 不再拉起」的问题。
这个 PR 把整机停启改成代际安全的协议:
依赖与当前状态
3cc2b662bb9f7750ef关键安全边界
stop_exit_codes或autorestart=false会阻止启动/重启事务BOTMUX_PM2_GRACEFUL_EXIT_CODE=90影响面
本次 restack 验证
pnpm build:passed(含 domain audit / dist audit)pnpm exec tsc --noEmit:passedgit diff --check:passedgit range-diff:后两提交补丁等价;首提交仅包含当前基线已有的 file-lock import 与 dashboard mutation guard 冲突合并本轮没有启动、停止或重启真实 PM2、daemon、Riff 服务;未执行会改变本机 fleet 的真实 stop/restart smoke。#598 合入后转 Ready 前,再跑一次最新 master 回归。