fix(automation): keep a wait timer's one-shot armed when the shot never consumed the pause (#5529) - #5549
Merged
Conversation
…er consumed the pause (#5529) `engine.resume()` reports failure by RETURNING a code, not by throwing, so the timer callback's unconditional `finally` could not tell a shot that consumed the pause from one that missed. On `STORE_UNAVAILABLE` — the durable suspended-run store unreadable, which per #4420 must never read as "no such run" — the pause survived but its only wake-up job cancelled itself, so nothing woke that run until the next process start's overdue re-arm. The one-shot now settles on the return code: `STORE_UNAVAILABLE` keeps the job armed and reports the degradation at `error` (the path was previously silent); success, `RESUME_IN_PROGRESS`, machine-state failures and thrown errors cancel exactly as before. Measured, not assumed: a `once` schedule is a single `setTimeout` in `IntervalJobAdapter`, so surviving is not a retry. It keeps the `sys_job` row `active` with its deadline and keeps the registration `trigger()`-able, which is a no-restart remedy the log line names; a cancel destroys both. Both arming sites now share one handler so they cannot drift, the same reason `waitTimerJobName` is one declaration. Separate from #5512's `onSuspensionReleased`, which answers the RUN-side question. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BWS4heBoAitLmzCLhcYdbK
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5529
前提核对(合并 #5527 之后)
分支切自
origin/main(含 PR #5527 =f0d98e10)。核对结果:waitTimerJobName提取、onSuspensionReleased钩子都已存在;finally无条件取消仍在(合并后位于wait-node.tsL99-115,fix(automation): release the wait node timer job when the run leaves the node (#5512) #5527 刻意保留并写明两者回答不同问题);engine.ts的两个返回码仍在:RESUME_IN_PROGRESS(L2854)、STORE_UNAVAILABLE(L2875)。AutomationResult.code是packages/spec/src/contracts/automation-service.tsL221 已宣告的公开字段,所以读它是契约内消费,不动engine.ts。一处需要更正 issue 的归因(重要,请复核):issue 把洞定位在
wait-node.tsL87-92,也就是排班路径的回调。但resumeInternal只在热缓存未命中时才读持久 store(loadSuspendedRunStrict:先this.suspendedRuns.get(runId),该缓存只在forgetSuspendedRun时才删),而once定时器就跑在排下它的那个进程里 —— 所以在排班路径上,run 一定在本进程缓存里命中,那条回调拿不到STORE_UNAVAILABLE。真正可达的是同文件里rearmSuspendedWaitTimers重排的那一枪(新进程、缓存为空、真的去读 store),而它带着一模一样的无条件finally—— issue 只把rearmSuspendedWaitTimers当作自愈路径提及,没注意它同时也是同一缺陷的第二个现场。机制、修法、文件都与 issue 所述一致,所以前提成立;只是可达的那个现场是同文件的兄弟站点。两处合用一个 handler 修掉(排班路径那一支按构造是潜伏的,但留着两种「怎么收尾这一枪」的写法正是 #5512 刚收敛掉的漂移)。
前置实测:once-job 回调结束后会不会再响(不猜)
派发要求先实测再定形状。临时 harness 跑在
service-job包里(已删除),结论:once在IntervalJobAdapter里就是一个setTimeout(DbJobAdapter把全部定时机制委派给它),打完一枪不会再响。所以落到派发说明里的第二个分支:「不取消」不构成自愈。但实测同时显示「不取消」保住了
cancel会销毁的两样东西,这是选择它的实据:sys_job行保持active: true且带着 deadline —— 在这里这是真话(run 确实还在等);取消后翻成active: false,读起来是「这个唤醒已经办完了」,而 run 还挂着。trigger('flow-wait:{runId}:{nodeId}')能在 store 恢复后不重启地把这一枪重打一遍。取消之后trigger直接报 job not found,只剩下次启动的 overdue re-arm。所以最终形状 = 不取消 + error 日志,且日志把「这不是自动重试」和上面两条补救都写明,不暗示会自己好。
改了什么
packages/services/service-automation/src/builtin/wait-node.ts—— 抽出一个共用的makeWaitTimerJobHandler(...)(返回() =大于Promise< void >),排班路径与 re-arm 路径共用,理由与waitTimerJobName只有一份声明相同。按返回码分流:STORE_UNAVAILABLE—— 暂停没被消费([automation/approvals] 进程重启后审批决策静默失效:挂起 flow run 仍只存内存(#1518 标记 COMPLETED 但 17.0.0-rc.1 未生效),approve 落库却永不推进且零报错 #4420 划的正是这条线:store 读不到绝不等于「没这个 run」),不取消,并记一条 error(此前完全静默:结果被回调丢弃,连warn都没有)。日志带 jobName、runId、原因,以及两条补救。RESUME_IN_PROGRESS(另一路 resume 正在消费,[17.0-rc2验收] wait 定时暂停被外部 resume 短路后,一次性唤醒 job 仍保持 armed —— 次日对已完成 run 发起幽灵 resume,sys_job 里留下误导性的「待唤醒」行 #5512 的onSuspensionReleased会顺带取消)、machine-state 失败(RUN_NOT_FOUND等,没有暂停可服务),以及resume抛错(抛错不是 store 故障,不给豁免)。按要求没有合并
onSuspensionReleased与finally两个机制:前者答「RUN 离开了节点」,后者答「这个 JOB 打完了它唯一的一枪,且这一枪把暂停结清了」。RearmLogger现在extends WaitTimerLogger,error仍是必需(#4632)。未动engine.ts。反向验证(方向先判后跑)
预判:把无条件取消恢复回去,应当只有 3 条新用例转红(两条「未被取消」断言 + 那条 error 日志断言),3 条「其余照旧」用例保持绿,既有用例全部不动。理由:这些 pin 断言的是旧代码总会执行的一次 cancel 之缺席,以及旧代码从不产生的一行日志 —— 没有反转,也没有「因为什么都没产生所以绿」的空壳。
实跑(把
if (result?.code === 'STORE_UNAVAILABLE')改成if (false))与预判一致:既有 6 条 #5512 teardown 用例全程保持绿,说明本次是纯增量,没有顶掉别人的覆盖。
测试
新增 describe「wait timer one-shot vs. a shot that never consumed the pause (#5529)」6 条。可达的那条端到端用例走 re-arm 路径:进程 1 挂起 → 进程 2 冷启动、
load抛connection refused而其余委派(于是底层行仍可检查)→ re-arm 重排 → 手动触发这一枪,断言 run 仍挂着(inner.list()仍含该 runId、下游节点没跑)、cancelled为空、error 日志带 jobName/runId/两条补救。排班路径那条用例是注入返回码(改
engine.resume)而非诱发,用例注释里直说了为什么:本进程热缓存命中,引擎在这里产不出这个码;伪造一次缓存未命中去「证明」它,只会 pin 一个引擎并不存在的场景。它验证的是 handler 的分支,以及排班站点确实走了这个共用 handler 而不是自留finally。tsc --noEmit(本包在 #4311 DEBT 台账内,errors被冻结):改动前后均为 5 raw errors,且wait-node*相关 0 条 —— 增量为零。另跑:check:type-check-coverageOK、check:durability-log-levelOK(24 seams)、check-nul-bytesOK,并按控制字符自查规则对两个改动文件做了grep -naP扫描(干净)。改动文件 eslint 无输出。顺带说明:构建 spec 时
packages/spec/authorable-surface.base.json被gen:schema重锚到当前 HEAD(与本次改动无关的生成物副作用),已git checkout还原,不在本 PR 内。范围外发现
finding,未进pm:queue):DbJobAdapter.wrap()只按 handler 是否抛错判定结果,所以这类「内部降级、不抛错」的 handler 在sys_job_run上记成success(实测行见上)。属 job 域审计面,不是本次 automation 域的面;wait 定时唤醒 job 在 resume 没能消费掉暂停时也会自我取消 —— store 短暂不可达即丢掉这一次唤醒,run 挂到下次重启才被捞回 #5529 之后该路径已有 error 日志且 job 保持active,所以 run 卡住本身是可见的,缺的只是审计行里那一格。两种读法都成立,严重度交分诊。同包 #5048 / #4792 的面未触碰。
Generated by Claude Code