Skip to content

time-relative/schedule cron flows permanently fail to re-bind after kernel rebuild — croner name leaked because DbJobAdapter.destroy() never destroys the cron adapter #8362

Description

@os-zhuang

现象

本地 magic-flow rig(cloud main 3f2884c4 × framework main cb43296ef,2026-08-13)实测:一个 timeRelative flow 首次绑定成功后,每一次 kernel 重建都无法重绑,且永不自愈(连续 4 次重建复现):

INFO [time-relative] bound flow 'xqao_contract_expiry_reminder_flow' → sweep 'xqao_contract.expiry_date' offsets [3]d on cron '0 8 * * *'
(kernel 被 freshness probe 驱逐、重建后)
WARN [time-relative] failed to schedule flow 'xqao_contract_expiry_reminder_flow': Cron: Tried to initialize new named job 'flow-time-relative:xqao_contract_expiry_reminder_flow', but name already taken.

之后 sweep 绑在空处,自动化静默死亡——verify_build、UI、DB 全部显示健康,唯一信号是这条 WARN。同一批重建里 record-change trigger 每次都正常重绑,对照明显。

两个对照实验钉住根因是残留的进程级 job 名

  • 把 start 节点 schedule 换成 {type:'interval'} → 重绑成功(interval 走 setInterval,不经过 croner 命名注册表);换回 cron 型立刻 name already taken
  • sys_metadata 里改 flow 名(= 换 job 名)→ 下一次重建绑定成功。

根因链(四层)

  1. 绑定层packages/triggers/trigger-schedule/src/time-relative-trigger.ts:214:job 名 flow-time-relative:<flowName>不带 env/kernel 标识;绑定前的 this.stop(flowName) 读实例级 this.bound Map——新 kernel 的新实例 Map 为空,清理 no-op。schedule-trigger.tsflow-schedule:<flowName>)结构完全相同,同样中招。
  2. 适配层cron-job-adapter.tsschedule()await this.cancel(name),但也只查自己实例this.jobs Map。
  3. croner 层new Cron(expression, { name }) 登记进 croner 进程级全局命名注册表——旧 kernel 的 Cron 对象还在,新建同名直接 throw。
  4. 销毁层(单点根因)db-job-adapter.ts:kernel 驱逐链路本身是完整的(KernelManager.evict()kernel.shutdown()plugin.destroy()JobServicePlugin.destroy()dbAdapter.destroy()),但 DbJobAdapter.destroy() 只有 await this.inner.destroy()——漏掉 this.cron(CronJobAdapter)。旧 kernel 的 croner 任务永不 stop,永久占名。

另外 trigger 的 schedule() 是 fire-and-forget(void Promise.resolve(...)),失败被降为一条 WARN。

为什么严重

  • 多租户云运行时里 kernel 驱逐是常规操作:freshness probe ~10s 一次,AI 每次 auto-publish 都会 bump freshness → 驱逐。所以「AI 建好定时自动化 → 用户改一次元数据 → 自动化死了」是常态路径。
  • job 名不带 env id:同一容器里两个环境同名 flow 不用等驱逐就冲突(AI 生成的 flow 名极易撞,如 contract_expiry_reminder_flow)。
  • 失败静默:唯一信号是没人看的 WARN,属于 valid-but-inert(ADR-0078)一类。
  • 旧 kernel 的 cron 任务不只占名字,它还活着——闭包握着已 shutdown 的旧 kernel 引擎(目前正因新绑定失败才没有双写;单修命名不修销毁会把静默死亡变成 zombie 双写)。

期望修复

  1. DbJobAdapter.destroy() 补上 this.cron 的销毁(CronJobAdapter.destroy() 已存在,只是没人调)。
  2. job 名加 environment/kernel 命名空间,杜绝跨 kernel/跨环境冲突。
  3. 重绑改为 replace 语义(bind 撞名时替换旧 job),而非 warn-and-give-up。
  4. 绑定失败升级为可观测状态(不只 WARN)。
  5. 回归测试:bind → 新插件实例模拟 kernel 重建 → 再 bind → job 恰好调度一次且能触发;覆盖 time-relative-triggerschedule-trigger 两处。

注意:即使全部修完,「kernel 不驻留时定时任务不跑」仍是架构层问题(时钟应上移到常驻层),cloud 侧另行跟踪。

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions