fix(runtime): 声明式 cron job 的表达式信封在 job 边界处降解 (#4567) - #4590
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 2, 2026 09:17
os-zhuang
enabled auto-merge
August 2, 2026 09:17
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 #4567
问题
defineJob(JobSchema.parse)产出的 cron schedule,其expression已被CronExpressionInputSchema变换成 ADR 表达式信封{ dialect: 'cron', source: '0 1 * * *' }(authoring/persistence tier)。而IJobService.schedule的边界契约(@objectstack/spec/contracts的JobSchedule,#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 的scheduletrigger 路径不受影响(前者无 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原样通过。typeof === 'object'容错 —— 边界仍然只认一种形状,adapter 保持严格。crondialect、缺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,labelsapp/job),经既有的resolveMetrics(ctx)约定写入 observability metrics registry。测试
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 直通,以及各类不可降解形状的报错。Pattern has to be of type string。实测把toBoundaryJobSchedule(...)换回job.schedule后,端到端用例即以expected [ ] to include 'health_sweep'失败 —— 与 issue 描述的症状一致。命令与结果(均在 flock 串行锁 +
NODE_OPTIONS=--max-old-space-size=4096+--filter范围内执行):turbo run test typecheck --filter=@objectstack/runtime --filter=@objectstack/observability --filter=@objectstack/service-job→ runtime76 files / 1056 tests passed,observability5 files / 75 tests passed,service-job5 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