Skip to content

fix(runtime): 声明式 cron job 的表达式信封在 job 边界处降解 (#4567) - #4590

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-4567-cron-schedule-envelope
Aug 2, 2026
Merged

fix(runtime): 声明式 cron job 的表达式信封在 job 边界处降解 (#4567)#4590
os-zhuang merged 1 commit into
mainfrom
claude/issue-4567-cron-schedule-envelope

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #4567

问题

defineJob(JobSchema.parse)产出的 cron schedule,其 expression 已被 CronExpressionInputSchema 变换成 ADR 表达式信封 { dialect: 'cron', source: '0 1 * * *' }(authoring/persistence tier)。而 IJobService.schedule 的边界契约(@objectstack/spec/contractsJobSchedule,#4538 后该名字的唯一声明)明确写着 expression裸 cron 字符串 —— 因为 CronJobAdapter 会把它原样交给 croner。

AppPlugin(packages/runtime/src/app-plugin.ts,kernel:ready 里的 declarative-jobs 注册段)把 job.schedule 原样下传,croner 抛 CronPattern: Pattern has to be of type string.,异常又被 per-job 的 try/catch 吞成一条 warn —— 于是每一个通过 defineJob 声明的 cron job 都从未真正被调度,而作者看到的是一次成功的 build 和一次成功的 boot。interval/once 分支与 flow 的 schedule trigger 路径不受影响(前者无 transform,后者自己归一化出裸字符串)。

修复(方向 1,contract-first)

按 issue 的候选 1、Prime Directive #12 落地:在两个 tier 相接的那一个点做降解,即 AppPlugin 注册声明式 job 处(与 retryPolicy/timeout 的下传同一位置),新增 toBoundaryJobSchedule()(packages/runtime/src/job-schedule.ts):

  • 信封 expression.source → 裸字符串;timezone 只在是字符串时透传;interval/once 原样通过。
  • 没有在 adapter 里加 typeof === 'object' 容错 —— 边界仍然只认一种形状,adapter 保持严格。
  • 无法降解的形状(未知 type、AST-only 信封、非 cron dialect、缺 intervalMs/at)一律点名抛错,而不是悄悄放行。裸字符串仍被接受:那是同一 schema 的 authoring input 拼写(未经 JobSchema.parse 组装的 bundle 合法地带着它),两者都属于 authoring tier,都不会流到 adapter。

失败路径不再静默

  • 调度失败:ctx.logger.error('[AppPlugin] Background job FAILED TO SCHEDULE — it will never run', err, { appId, job, schedule }),并在本轮结束时补一条汇总 error;info 汇总行增加 failed 计数。
  • 新增计数器 job_schedule_failures_total(SEMCONV.jobScheduleFailuresTotal,labels app/job),经既有的 resolveMetrics(ctx) 约定写入 observability metrics registry。
  • "调度失败"与"缺 handler / job disabled"从此不共用一条 warn:后者描述的是本来就不会跑的 job,前者描述的是作者应得却没跑的活。

测试

packages/runtime/src/app-plugin.jobs.test.ts 用的是真实的 CronJobAdapter(croner),不是记录型 double —— 这个 bug 恰恰发生在 adapter 内部,任何"照单全收"的假实现都看不见它(为此给 runtime 加了 @objectstack/service-job 的 devDependency + vitest alias)。

  • 端到端:defineJob 的 cron job 经 AppPlugin → 真实 adapter 后,listJobs() 含该 job,且内部 croner task 的 nextRun() 落在 UTC 01:00;trigger() 能跑到 bundle handler;全程无 error 日志、计数器为 0。
  • 降解本身:packages/runtime/src/job-schedule.test.ts 钉住信封→字符串、timezone 透传、interval/once 直通,以及各类不可降解形状的报错。
  • revert-proof:单独钉住"原样下传"仍会让真实 adapter 抛 Pattern has to be of type string。实测把 toBoundaryJobSchedule(...) 换回 job.schedule 后,端到端用例即以 expected [ ] to include 'health_sweep' 失败 —— 与 issue 描述的症状一致。
  • 失败路径:不可降解的 job 触发 error 级日志 + 计数器 +1,且 warn 流中不含 schedule 相关消息;缺 handler 的 job 仍然只是 warn。

命令与结果(均在 flock 串行锁 + NODE_OPTIONS=--max-old-space-size=4096 + --filter 范围内执行):

  • turbo run test typecheck --filter=@objectstack/runtime --filter=@objectstack/observability --filter=@objectstack/service-job → runtime 76 files / 1056 tests passed,observability 5 files / 75 tests passed,service-job 5 files / 39 tests passed,service-job:typecheck 通过(runtime/observability 在类型检查覆盖率账本中仍是 DEBT,无 typecheck 脚本;runtime 的 tsup DTS 构建对本次改动做了实际类型检查)。
  • eslint 覆盖全部改动文件 → 无输出。

changeset:.changeset/declarative-cron-job-schedule-envelope.md(runtime / observability 各 patch)。

🤖 Generated with Claude Code


Generated by Claude Code

…undary (#4567)

`defineJob`'s parsed `schedule` carries the ADR expression envelope
(`{dialect:'cron',source}`) while `IJobService.schedule` — and croner behind
`CronJobAdapter` — take a bare cron string. AppPlugin passed the authored
shape verbatim, croner threw "CronPattern: Pattern has to be of type string",
and a per-job try/catch swallowed it into a warn: every declarative cron job
was declared, built, booted and never scheduled.

Convert at the single authoring→boundary seam (`toBoundaryJobSchedule`, called
where retryPolicy/timeout are already threaded); the adapters stay strict.
The failure path is now loud: error-level log with its own message, a boot
summary line, and the new `job_schedule_failures_total` counter — no longer
sharing the quiet warn used for "handler not found".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012C2cd7tL8QDoZ2QKN3djJ5
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 2, 2026 9:16am

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling labels Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/observability, @objectstack/runtime.

21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/kernel/cluster.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx (via @objectstack/observability, @objectstack/runtime)
  • content/docs/releases/v17.mdx (via @objectstack/runtime)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review August 2, 2026 09:17
@os-zhuang
os-zhuang enabled auto-merge August 2, 2026 09:17
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 2, 2026
Merged via the queue into main with commit ff17642 Aug 2, 2026
21 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4567-cron-schedule-envelope branch August 2, 2026 09:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

defineJob 解析后的 cron schedule 是表达式信封,CronJobAdapter 直接喂给 croner —— 声明式 cron job 静默调度失败

2 participants