Preserve supported expression fields instead of stringifying them, and probe individual fields only when whole-expression validation fails. Convert unsupported syntax and weekday ranges ending in 7 while preserving legacy date/weekday rules and valid OR branches. Add execution regressions for issue #3099 and 64 calendar boundary cases in UTC and Asia/Shanghai. Ensure timezone child tests actually run under the Node test runner. Validation: 745 tests passed, 8 skipped; final targeted regressions passed; backend compilation passed. Differential calendar checks matched 142,360 execution timestamps. Fixes #3099
8.1 KiB
Node cron 调度器迁移
Node 调度路径使用固定版本 node-cron@4.6.0,普通任务(含额外规则)、订阅和系统 cron 共用 back/shared/cronScheduler.ts。启动/一次性任务、间隔任务、脚本并发限制和进程执行链路保持原有实现。系统模式只将语义一致的普通五字段规则交给 crond。
移除 node-schedule、其类型包以及兼容别名 cron-parser-v4。保留 cron-parser@5 做旧表达式的校验与标准化;计时器和日历推进由 node-cron 管理。
兼容行为
- 保留旧版 1–6 字段补全、五种宏、月份/星期名称、数字起点步长、L 和 #。另外明确新增
@annually(等同@yearly)、@midnight(等同@daily)、@minutely(每分钟第 0 秒)。宏按进程时区解释。?仅允许日期和星期字段;裸/N、H 和其他未列出的宏仍拒绝。 - 日期与星期同时指定时保留 OR 语义,不采用 node-cron 默认的 AND。两个原生任务分别处理日期和星期,并按计划时间过滤交集,避免重复执行;保留旧解析器的月份全日期范围及全局 nth-week 限制。
- 原生任务先暂停创建并验证,再替换旧任务。构造失败时销毁已准备的实例并保留旧快照。
- 进程内迟到的触发通过
execution:missed加入既有执行队列,记录补执行日志。并发和单任务积压上限仍由青龙队列控制;这不提供进程退出期间的持久化补跑或分布式 exactly-once 保证。 - 保留 node-cron 已支持的列表、范围和名称,不调用 cron-parser 的
stringify()重写整条表达式。先校验整条表达式,通过则跳过逐字段探测;失败时才检查并展开不支持的字段(如数字/名称起点步长)。保留原有?和#兼容处理,避免有效的6,12,18被压缩为不兼容的6/6;日期/星期拆分后的最终校验仍然保留。 - 星期步长范围以
7结尾时,先按 cron-parser 的解析值展开。例如2-7/4在现有解析器中包含星期日,而 node-cron 原样执行会遗漏星期日;仅通过语法校验不足以保证执行含义一致。 - 单月规则先剔除超出该月最大日期的值再计算日期数量,保留旧版日期/星期的通配计数语义。被判为通配的字段不再作为另一侧的 AND 约束,避免
2-31 4 MON和包含L、重复星期日的边界规则漏执行。 - 日期与星期按 OR 拆分后,若日期分支在所有指定月份中都不可达,而星期分支有效,则仅保留星期分支;移除前仍检查日期分支在任意月份下语法有效。
31 2,4 MON因而可以执行,单独的31 2,4 *仍拒绝。 - 标准化结果使用上限 512 条的只读缓存,仅缓存语法,不缓存运行实例或下次执行时间。避免大批相同表达式重复解析导致恢复 RPC 超时。
- 回调异常记录日志,后续调度继续;取消时调用原生
destroy()释放计时器和注册表。 - 持久化无效任务在恢复时跳过并记录日志,无效订阅不会产生未处理的启动拒绝。API、导入和执行使用同一套表达式校验。
复审修复
- 移除没有生产调用的
nextInvocation()接口,避免对 OR/# 等过滤规则报告不会实际执行的原生日期。回归测试直接观察实际执行和原生实例状态。 - 批量注册 RPC 时限为 5 秒加每条规则 5 毫秒,计入额外规则并封顶 120 秒;保留超时后的恢复机制,不立即重放结果不确定的写入。
- Alpine/系统模式下,数字起点步长(如
0/5)、星期包含7、日期步长、日期和星期同时受限的表达式转入 Node 路径。BusyBox 的这些解析或通配语义与旧 Node 解析器不同,不能仅因是纯数字五字段就交给 crond。 - 这些规则在
crontab.list中仍保留为注释,避免同时被系统和 Node 执行。其他可移植规则仍使用系统 crond。
夏令时差异
采用 node-cron 的当地时间语义:春季跳时不存在的时间跳过,秋季重复小时不再执行第二次。旧 cron-parser 对部分春季时间会顺延,并可能重复秋季高频任务。使用有夏令时的时区时需要考虑该行为变化;Asia/Shanghai 不受影响。回归测试固定 America/New_York 的 2026 年转换日期。
验证
npm test 和 npm run build:back。
test/fixtures/legacy-cron.json是迁移前使用 node-schedule 2.1.1 / cron-parser 4.9.0 保存的快照:269 条语法判定,18 组日历序列,每组 10 次执行。- 真正的 node-cron 实例配合虚拟时钟验证日期/星期 OR、交集去重、闰年、L/#、8 秒迟到补执行、取消释放、回调拒绝和夏令时。
test/fixtures/cron-calendar-boundaries.json保存 64 组规则与起点,以独立 cron-parser 4.9.0 / luxon 3.7.2 或公历计算产生预期结果,每组在 UTC、Asia/Shanghai 下各核对 4 次实际执行,包括 2100 年不闰和长周期第五个星期日的边界。时区子测试清理继承的NODE_TEST_CONTEXT并检查子测试输出,防止嵌套测试入口被跳过而出现假通过。- 注册构造失败回滚、无效订阅隔离、空表达式立即运行,以及现有调度恢复/503/子进程测试覆盖接入流程。
- 隔离的正式 Debian 镜像使用实际编译产物验证 10,000 条有效任务和 2 条错误任务的恢复、300 个同时到期的 Python 脚本、并发上限 16 及 HTTP 健康检查。具体结果见本次测试报告;这是生产镜像隔离验证,不是线上长期运行证明。
本次验收结果(2026-09-27)
完整回归 260 通过、3 项原有跳过,后端编译成功。Node 20 生产运行时的 21 项兼容测试通过。隔离镜像中 300/300 脚本完成,无重复或遗漏,最大并发 16,270 个成功及 30 个预设失败均落库;完成耗时 52.36 秒。恢复后健康检查 1,870/1,870 成功,任务列表 38/38 成功。重启恢复阶段另有 50 次健康检查失败。
初版重复解析曾导致恢复超过 5 秒 RPC 时限,加入有上限的语法缓存后重跑通过。本次 10,000 个任务共享两个表达式,不能视为一万种复杂表达式的容量保证;实际脚本耗时也不代表纯调度性能。
新增宏在 UTC 和 Asia/Shanghai 下验证连续两次实际触发及不提前触发;原有语法快照仅对这三个新增宏允许预期变化。
复审后验收
完整回归 267 通过、3 项原有跳过;Node 20 的 10 项修复回归及 Alpine 镜像的 24 项兼容/宏测试通过。
额外使用 9,700 个不同表达式和 300 个同时到期任务验证恢复(另有 2 个无效任务),不再局限于共享两个表达式。300/300 脚本完成,无重复或遗漏,最大并发 16,270 个退出码 0、30 个预设退出码 3 均落库。恢复后健康检查 1,650/1,650 成功、任务列表 33/33 成功;启动恢复阶段另有 260 次健康检查失败,整批脚本耗时 60.27 秒。较大快照恢复仍需等待,不承诺启动阶段始终可用或任意规模都能在时限内恢复。
深度兼容性审计(2026-10-17)
保持 cron-parser 5.4.0 和 node-cron 4.6.0,扫描 19,811 个表达式,覆盖六个字段的数字、前导零、列表、范围、起点步长、名称、L/#/? 及日期/星期组合。发现并修复星期步长末尾 7、单月日期裁剪、通配计数边界和不可达日期 OR 分支问题;47 个初始失败样例已保存为回归数据。
UTC 和 Asia/Shanghai 各比较 17,795 条规则的连续 4 次实际触发,共核对 142,360 个触发时刻,修复后无偏差、无构造失败。每个时区另外有 71 个通配 # 组合沿用已明确保留的旧适配规则,不与原解析器直接对照;22 个原版拒绝的日期输入仍拒绝,11 个参考迭代器触及搜索上限的长周期规则改用独立公历预期验证。
持久化回归覆盖 64 组规则与起点、两个时区,共 128 组日历用例;还保留 H、裸 /N、非标准 L 写法及不可达规则的拒绝行为。完整回归 745 通过、8 跳过,后端编译通过。本次为确定性语法与日历测试,不涉及生产环境压力或长期稳定性验证。