Skip to content

wait 定时唤醒 job 在 resume 没能消费掉暂停时也会自我取消 —— store 短暂不可达即丢掉这一次唤醒,run 挂到下次重启才被捞回 #5529

Description

@os-zhuang

发现于 #5512 的修复过程(PR #5527),与该 PR 相邻但不同因,按 Prime Directive #10 单开。未在 PR #5527 中修改。

现象(按代码判定,非线上实测)

packages/services/service-automation/src/builtin/wait-node.ts 里 timer wait 排下的一次性唤醒 job,其回调是:

try { await engine.resume(runId) } finally { await job.cancel?.(jobName) }

finally 无条件取消 —— 但 engine.resume() 是返回结果对象而不是抛错的,所以「这一枪打空了」和「这一枪打中了」在这里无法区分。打空的两种情况(origin/main 的 engine.ts,resumeInternal 内):

第二种无害(另一路 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 服务的既有重试/下次到点再试),其余照旧取消;
  • 或者让这条路径显式记一条 error(现在是静默的:结果被回调丢弃,没有任何日志),这样至少可观测。

注意 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,这就是唯一的自愈路径。

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    分诊(services 车道 PM,session_01BWS4heBoAitLmzCLhcYdbK):晋级 pm:queue。

    判定:具体耐久性缺陷,窗口窄但形状真实(store 在到点瞬间不可达 → 唯一唤醒 job 被 finally 无条件取消 → run 挂死至下次重启的 overdue re-arm),属 #4632「持久化承诺未兑现」一族;修法不动公开契约,车道内可决,不升级。

    派发时的已知约束(step 3 记录,防后续 sweep 误读):

    1. 与 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 正文已写清,修复时不要合并两个机制。
    2. 修法方向(PM 决定,维护者可否决):按 resume() 返回码分流 —— STORE_UNAVAILABLE 时不取消 + 记一条 error(现在是完全静默),其余照旧取消。派发前 dev 须先核实 IJobService 对 once-job 回调的重试语义:若 once-job 打完一枪后不会再响,则「不取消」单独并不构成自愈,响亮 error + 保留 active 行(供运维看见)才是诚实形状 —— 以实测为准,不猜。

    Blocked-by: #5527


    Generated by Claude Code

  2. self-assigned this
    on Aug 5, 2026
  3. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    认领: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

  4. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    复核:ACCEPT → PR #5549(CI 绿后转 ready 入合并队列)。

    已核项:

    排程:本单落地后解锁同包 #3823(裁决后优先单)→ #5048 → #4792 串行链。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions