-
-
Notifications
You must be signed in to change notification settings - Fork 52.7k
Description
Summary
Cron jobs with schedule.kind=cron can skip scheduled runs after 2026.2.3. On each timer tick, CronService forces a reload and recomputes nextRunAtMs before checking due jobs. computeNextRunAtMs() uses Croner.nextRun(now), which returns the next run after now. If the timer fires at the scheduled boundary, nextRunAtMs is advanced to the next interval and runDueJobs() sees nothing due.
Observed
- Hourly/30m jobs skip intervals;
nextRunAtMsjumps ahead even though the run never executed. - This started after 2026.2.3 (Mac, LaunchAgent).
Root Cause (code path)
onTimer() in src/cron/service/timer.ts:
await ensureLoaded(state, { forceReload: true });
await runDueJobs(state);ensureLoaded() calls recomputeNextRuns(), which calls computeNextRunAtMs() with nowMs. For cron schedules it does:
new Cron(expr, { timezone }).nextRun(new Date(nowMs))Croner nextRun() returns the next occurrence after now, so a run exactly at now is skipped.
Minimal Fix (works locally)
Avoid recomputing before the due check. Example:
- await ensureLoaded(state, { forceReload: true });
+ await ensureLoaded(state);Safer Alternative Fixes
- Only force-reload/recompute when the store file mtime changes.
- Or, in
computeNextRunAtMs(), ifCron(expr).match(now)then returnnowbefore callingnextRun(). - Or, move
recomputeNextRuns()to afterrunDueJobs()on each tick.
Notes on side effects
Removing the forced reload means out-of-band edits to the cron store file won’t be picked up until restart or an API call. If that’s why forceReload exists, a conditional reload (mtime check) would preserve that behavior without skipping runs.