点了一次 Run 测试,定时任务就再也没响过:Hermes 让手动运行变成"额外一次"


你写好一个新提醒,让它 30 分钟后提醒你去关火。为了确认这套配置真能跑,你在面板上点了那个 Run 按钮。任务立刻执行,输出正确,你满意地关掉页面。半小时后,厨房飘出一股糊味——提醒始终没响。

原因不是调度器坏了,而是你那次“测试”把这条一次性任务唯一的配额用掉了。任务自己认为已经完成,于是安安静静地退休了。

手动运行曾经算“用掉一次”

定时任务的记录里有两个容易混淆的字段:一个是“下一次该在什么时候跑”(next_run_at),一个是“配额用了几次”(repeat.completed / repeat.times)。一次性任务就是配额为 1 的那种写法。

旧逻辑把手动触发当成了正常的一次执行:跑完就记账,repeat.completed 加一。对一次性任务来说这就意味着 1/1,于是状态被写成 completed、enabled=false、next_run_at=null——记录已经是终态,真正的定时那一次永远等不到了。循环任务也会受影响:repeat.times=3 的任务手动跑一次就只剩两次。

现在的规则:手动运行是“额外一次”

新行为很直白——手动运行不占定时的那一次,也不动配额。同样的两条任务,在修复前后读回来是这样:

任务 旧行为(手动跑一次之后) 新行为
in 30m 一次性提醒 completed,enabled=false,next_run_at=null,配额 1/1 → 定时永不触发 scheduled,enabled=true,next_run_at 仍是原来的时间,配额 0/1
every 60m,repeat.times=3 配额记成 1/3,next_run_at 被推到“现在”之后 配额 0/3,next_run_at 没变

也就是说:手动那次是“额外加的班”,定时那次照常上班,配额一次都没花。

但有一种情况,手动运行就是定时运行

如果任务的那一刻已经到点(或者说调度本来就该在此时触发),你再点 Run,这一次就是那次定时运行,不会变成“跑两遍”。判断规则:

  • 到点之前手动触发 → 额外一次,保留原定时;
  • 已经到点再手动触发 → 就是那一次,没有重复执行;
  • 手动跑的过程中定时那一刻到点了 → 一次性任务跑完立刻补上这一次;循环任务则丢掉这个已经过去的时刻,直接取下一个自然时刻(和正常调度一致)。

怎么用

# 手动触发一次(下次调度照样保留)
hermes cron run <job_id>

# 看任务状态:next_run_at 和 repeat 计数都在这里
hermes cron list

# 建一条一次性任务
hermes cron create "in 30m" "先把火关了再继续写代码"

面板上的 Run 按钮、agent 通过 cronjob_manage 触发的运行、gateway 转发的运行,走的是同一条路径,规则一致——不会出现“命令行里安全、按钮上危险”的分裂。

为什么这件事在代码里不简单

难点在于“决定”和“记录”之间可能中断。任务的认领(claim)在一把 jobs 锁里完成:如果判定这是一次额外运行,就把那个还没到点的定时时刻戳在认领记录上,并在同一次写入里把 next_run_at 放回原位。这样万一进程在认领之后、完成之前崩了,磁盘上的 next_run_at 仍然指向正确的未来时刻,不会丢一次。

trigger_job(gateway 转发那条路)以前还会把 next_run_at 直接改成“现在”,并且不留副本;现在它会先把被覆盖的时刻存起来,认领时再恢复,连续点两次也不会把时间越推越歪。

顺带还补了一个可观测性坑:一次性任务如果在认领后被打断、又没留下任何输出就被清理掉,现在会在任务的输出目录里写一份诊断文件,说明“派发已认领、运行未完成、进程可能被杀”,而不是无声无息地消失。

两个容易忽略的细节

  • 全局暂停不会挡手动运行。 hermes pause 期间任何定时触发都不启动,但显式的手动运行属于操作员覆盖,仍然执行——这也是它现在必须“不占配额”的另一个理由:暂停期间你手动补跑,不应该把后面的正式运行吃掉。
  • 别拿手动运行当“跳到下一次”。 它不推进 next_run_at。想跳过本次,用 pause / resume 或直接 edit 改计划。

想系统体检整套定时任务,用 cron doctor;跨时区排班看时区与夏令时那篇;多 profile 下每次触发用哪个身份、能读哪些密钥,在按路由触发的密钥隔离里。命令参数见 hermes cron 参考页。

版本状态

这个改动在 2026 年 10 月 28 日合并进 main(PR #146113)。当前正式版 v0.22.5(2026-10-23)还是旧语义——也就是说,在旧版本上手动测试一次性任务,仍可能让它当场退休。

测试一个提醒最保险的姿势,从来都是先确认它真的还会响。现在这个前提总算成立了。