错过的定时任务该不该补跑?用 grace_minutes 把这件事说清楚

周五下午你把笔记本合上,去开一个两小时的会。回来打开盖子,Slack 里躺着两份一模一样的日报——那份每天 15:00 跑一次的任务,在你休眠期间错过了,唤醒后补跑了一次,然后 16:00 还照常又跑了一次。你删掉重复的那份,心想“下次干脆别补了”。
但补跑也有它存在的理由。凌晨三点跑的数据同步,如果因为机器重启错过,你更希望它醒来后赶紧补上一次,而不是等到明天凌晨。**问题从来不是“补跑好不好”,而是“这个任务该等多久”。**Hermes 之前给所有任务用同一套规则,现在可以按任务单独设了。
默认是怎么处理的
先看默认行为,很多“诡异现象”都是它造成的。
调度器发现一次应该触发的时间点已经过去了,面前有两条路:立刻补跑,或者直接跳过、把锚点挪到下一次。它按一个“补跑窗口”(catch-up window)决定:
- 窗口 = 任务周期的一半;
- 再夹到 2 分钟到 2 小时之间。
所以一个每分钟跑的任务,窗口只有 2 分钟——错过两分钟以上就算过期,直接跳到下一分钟,不会疯狂补跑几十次。一个每天跑一次的任务,窗口是 12 小时,理论上你上午补开电脑它都还愿意补。一个每小时的任务,窗口是 30 分钟。
| 周期 | 默认补跑窗口 |
|---|---|
| 每 1 分钟 | 2 分钟(下限) |
| 每 15 分钟 | 7.5 分钟 |
| 每小时 | 30 分钟 |
| 每天 | 12 小时 |
| 每周 | 2 小时(上限) |
一次性任务(once)另算:给它 120 秒的宽限,过了就不跑了。
这个规则本身合理——周期长的任务容忍度大,周期短的任务快速跳过——但它有两个明显不合身的场景:
- **日报、周报这类“一天只能跑一次”的任务。**窗口 12 小时意味着早上睡醒它照样补跑,而你可能已经在手机上处理过了,于是重复通知。
- **对时点敏感的任务。**交易所开盘前 30 分钟拉数据,补跑到 11 点毫无意义,宁可不跑。
给单个任务改窗口
现在可以在任务上直接覆盖,单位是分钟:
# 永不补跑:错过的那次直接跳过
hermes cron edit market-open --grace-minutes 0
# 最多晚 3 小时还愿意补
hermes cron edit nightly-sync --grace-minutes 180
创建时也能直接写:
hermes cron create "30 9 * * 1-5" "开盘前拉取行情并生成简报" \
--name market-open \
--grace-minutes 5
取值范围是 0 到 1440 分钟(一天)。0 是有效值,含义是“永不补跑”。想把任务恢复成默认计算方式,传空字符串:
hermes cron edit nightly-sync --grace-minutes ""
它只对 cron 表达式生效。once 和 interval 这两种调度没有“错过的墙上时间点”这个概念——interval 是按间隔算下一次,once 是绝对时间——所以给它们设这个值不会有效果,命令层面也会给出提示。
三种典型用法
**一、日报周报:设 0。**你只关心“今天有没有跑过”,不需要“补一份昨天的”。设 0 之后,错过的那次被跳过并记录到日志,下次照常按点跑。重复通知的问题直接消失。
**二、机器可能休眠的夜间任务:往大里设。**笔记本用户最典型:23:00 的数据同步,你 08:00 才开机。默认窗口(半天)其实能覆盖,但如果任务周期是每周,默认只有 2 小时上限,那就明确设成 --grace-minutes 600,早上开机还能补。
**三、对时点敏感的任务:设小值。**开盘前、发布会前、每日对账前这类任务,晚 5 分钟就没价值了。给个 5 或 10 分钟,过期就跳过,避免用一份过时数据生成看起来正常的报告——过时但成功的报告,比没跑更危险。
全局关掉补跑
如果你根本不想要任何补跑行为,有一个全局开关:
hermes config set cron.catch_up_missed false
这个开关的作用是:超过补跑窗口的周期性任务被重新锚定到下一个未来时间点,不立即触发,跳过会写进日志。仍在窗口内的任务、以及手动触发的运行不受影响。它和 --grace-minutes 的关系是“全局关闭 vs 单任务放宽”——全局关掉之后,单任务的大窗口也就没意义了。
另外,如果你用托管 cron(托管部署上的外部调度器),还有一层 cron.misfire_grace_minutes(默认 10):网关等外部调度器自己的重试等这么久,再在本地补跑一次,避免整个交接环节的故障吃掉一整天。
四个容易踩的坑
- **
0是真实值,不是“没设置”。**代码里专门注了一句:0 == False,如果做“值为空就清掉”的判断,0会被悄悄当成未设置,结果反而回到默认窗口。CLI 和配置解析都按“显式给了就保留”处理,你自己写脚本改jobs.json时要小心。 - 只对 cron 表达式有效。
interval与once设了不生效,别指望用它来控制“每 5 分钟跑一次的任务多久没跑就补”。 - **profile 分发会丢字段。**如果你用 profile 导出/分发任务定义,
grace_minutes必须在允许的字段清单里,否则分发出去就没了,接收方回到默认窗口。这不是配置问题,是字段白名单的问题。 - **健康检查会跟着变。**仪表盘判断“这个任务是不是超时没跑”,用的是同一个窗口。改了 grace,超时判定也一起改——这是对的(否则仪表盘会把调度器认为准时的任务标红),但如果你习惯看某个固定阈值,会看到告警时间点变了。
怎么确认生效
hermes cron list # 每个任务详情里的 Grace 行会显示覆盖值
hermes cron doctor # 体检时会指出 grace_minutes 的问题
hermes cron list 的任务详情里会出现一行 Grace,只有设了覆盖值的任务才显示数字(形如 180 min);没设的这一行不出现,表示走默认计算。hermes cron doctor 还会检查两种异常:值不可用、以及给非 cron 表达式设了这个值(后者会被指出“没有效果”)。想验证“跳过”确实发生了,看日志里那条 Skipping missed occurrence,它会指出是 grace_minutes 还是全局 cron.catch_up_missed=false 导致的跳过,并给出下一次运行时间。
如果你正在排查“任务到底有没有跑”,cron doctor 那篇给了完整的体检流程;时区导致的错位看时区与夏令时那篇;从零开始搭定时任务的完整流程在 cron 自动化指南里。
状态
grace_minutes 于 2026-10-28 合入 main,并已随 v0.22.6(2026-11-02 发布) 进入正式发行版——升级后就能按任务单独设置补跑窗口。还在更老的版本上时,替代方案是全局的 cron.catch_up_missed:只能一刀切,没法按任务区分,但至少能先解决“重复补跑”的问题。