Repository navigation
wait 定时唤醒 job 在 resume 没能消费掉暂停时也会自我取消 —— store 短暂不可达即丢掉这一次唤醒,run 挂到下次重启才被捞回 #5529
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Aug 5, 2026 分诊(services 车道 PM,
session_01BWS4heBoAitLmzCLhcYdbK):晋级pm:queue。判定:具体耐久性缺陷,窗口窄但形状真实(store 在到点瞬间不可达 → 唯一唤醒 job 被
finally无条件取消 → run 挂死至下次重启的 overdue re-arm),属 #4632「持久化承诺未兑现」一族;修法不动公开契约,车道内可决,不升级。派发时的已知约束(step 3 记录,防后续 sweep 误读):
- 与 PR fix(automation): release the wait node timer job when the run leaves the node (#5512) #5527 同文件(
wait-node.ts),按同文件串行规则Blocked-by:它落地后才可派;且 fix(automation): release the wait node timer job when the run leaves the node (#5512) #5527 引入的onSuspensionReleased只覆盖「暂停被消费」的那一半,本单修的是finally保留下来的另一半职责 —— 两者语义分界 issue 正文已写清,修复时不要合并两个机制。 - 修法方向(PM 决定,维护者可否决):按
resume()返回码分流 ——STORE_UNAVAILABLE时不取消 + 记一条 error(现在是完全静默),其余照旧取消。派发前 dev 须先核实IJobService对 once-job 回调的重试语义:若 once-job 打完一枪后不会再响,则「不取消」单独并不构成自愈,响亮 error + 保留active行(供运维看见)才是诚实形状 —— 以实测为准,不猜。
Blocked-by: #5527
Generated by Claude Code
- 与 PR fix(automation): release the wait node timer job when the run leaves the node (#5512) #5527 同文件(
认领:PM 循环第 3 轮(services 车道)。Blocked-by 已解除:PR #5527(#5512)已 MERGED。
会话:session_01BWS4heBoAitLmzCLhcYdbK
分支:claude/issue-5529-wait-timer-store-unavailable
Worktree:objectstack-issue-5529
域:domain:services
文件面:packages/services/service-automation/src/builtin/wait-node.ts(timer 回调 finally)、同包测试、.changeset/*.md修法方向沿分诊评论:按
resume()返回码分流 + 响亮 error;dev 先实测IJobService对 once-job 回调的重试语义再定「不取消」是否构成自愈。同包 #5048/#4792 继续串行排队。
Generated by Claude Code
复核:ACCEPT → PR #5549(CI 绿后转 ready 入合并队列)。
已核项:
- issue 定位更正被接受并验证:排班路径的回调因本进程热缓存必命中拿不到
STORE_UNAVAILABLE(resumeInternal仅在缓存未命中时读持久 store);真正可达的现场是rearmSuspendedWaitTimers重排的那一枪 —— issue 只把它当自愈路径。两处经共用makeWaitTimerJobHandler一并修掉(与 [17.0-rc2验收] wait 定时暂停被外部 resume 短路后,一次性唤醒 job 仍保持 armed —— 次日对已完成 run 发起幽灵 resume,sys_job 里留下误导性的「待唤醒」行 #5512 收敛waitTimerJobName同理); - 前置实测按令执行:once-job 实测只响一枪(
setTimeout,+1000ms 仍 1 次)→「不取消」不构成自愈 → 落到分诊预设的第二分支(不取消 + error),且实测给出了「不取消」保住的两样东西(active: true行是真话;trigger()可不重启重打)—— error 日志把「非自动重试」与两条补救都写明; engine.ts未动(消费的是 spec 已宣告的公开AutomationResult.code字段);onSuspensionReleased与finally两机制未合并,语义分界保持;- 反向验证:预判恰 3 红 3 绿,实测逐条命中;[17.0-rc2验收] wait 定时暂停被外部 resume 短路后,一次性唤醒 job 仍保持 armed —— 次日对已完成 run 发起幽灵 resume,sys_job 里留下误导性的「待唤醒」行 #5512 的 6 条既有 teardown 用例全程绿(纯增量);
- 端到端用例走真实可达路径(双进程 + store
load抛错),排班路径用例注入返回码并在注释里写明为何不伪造缓存未命中 —— 诚实; - 全包 57 文件 695 用例绿;tsc 5 条旧账零增量([P2] framework: 66 个包用 tsup 构建、无人做类型检查 —— 实测 18 个包共 380 处 code-tier 错误(#4118 的 framework 侧对应) #4311 冻结台账,如实报告而非伪造 typecheck 绿);
check:durability-log-level等门禁 OK;生成物重锚已还原; - 顺带发现 DbJobAdapter 把「handler 没抛错」记成 sys_job_run.status='success' —— 内部自行降级的 job(如 wait 唤醒打空)在作业审计面上仍显示成功 #5548(
DbJobAdapter.wrap把内部降级记成 success 的审计面)已按 finding 归档,不阻塞本单。
排程:本单落地后解锁同包 #3823(裁决后优先单)→ #5048 → #4792 串行链。
Generated by Claude Code
- issue 定位更正被接受并验证:排班路径的回调因本进程热缓存必命中拿不到
- added a commit that references this issue
on Aug 6, 2026
发现于 #5512 的修复过程(PR #5527),与该 PR 相邻但不同因,按 Prime Directive #10 单开。未在 PR #5527 中修改。
现象(按代码判定,非线上实测)
packages/services/service-automation/src/builtin/wait-node.ts里 timer wait 排下的一次性唤醒 job,其回调是:finally无条件取消 —— 但engine.resume()是返回结果对象而不是抛错的,所以「这一枪打空了」和「这一枪打中了」在这里无法区分。打空的两种情况(origin/main的engine.ts,resumeInternal内):STORE_UNAVAILABLE(L2757 一带):持久 suspended-run store 读不到 —— 按 [automation/approvals] 进程重启后审批决策静默失效:挂起 flow run 仍只存内存(#1518 标记 COMPLETED 但 17.0.0-rc.1 未生效),approve 落库却永不推进且零报错 #4420 的设计这不能被当成「没有这个 run」,暂停仍然存在、行还在;RESUME_IN_PROGRESS(L2736):另一个 resume 正在进行中。第二种无害(另一路 resume 会消费掉暂停,PR #5527 之后它也会顺带取消 job)。第一种是个洞:暂停没被消费(run 还挂在 wait 节点上),但它唯一的唤醒 job 已经被取消了。此后没有任何东西会唤醒它,直到下一次进程启动 ——
rearmSuspendedWaitTimers会把它当 overdue 立即 resume。也就是说:store 在唤醒那一刻抖一下 + 之后不重启 = 这个 run 永远停在 wait 节点。窗口很窄(要恰好在到点那一刻 store 不可达),也能靠重启自愈,所以不急;但它属于 #4632 明确要防的那一类「持久化承诺没兑现」——只不过 #4632 覆盖的是 re-arm 路径的降级,这条在 timer 回调里,当时没看。
期望
finally里的自我取消应当只在「这一枪确实打中了(暂停被消费了)」或「这个 job 无论如何不该再响」时执行,而不是把 store 故障也当成打中。可能的方向(未决,交 PM/维护者裁断):resume()的返回码分流:STORE_UNAVAILABLE时不取消(让 job 服务的既有重试/下次到点再试),其余照旧取消;注意 PR #5527 引入的
onSuspensionReleased拆除不覆盖这条:它只在暂停真的被消费时才触发,正是「打中了」的那一半;finally保留下来的职责恰恰是「这一次性 job 不该再响」,两者的语义分歧就是这个 issue。佐证
wait-node.ts(origin/main)L87-L92:finally+One-shot: drop the job so it never re-fires。engine.ts(origin/main)L2736 / L2757:两个「resume 没消费掉暂停」的返回码。rearmSuspendedWaitTimers:overdue 分支会在下次启动时立即 resume,这就是唯一的自愈路径。