From d81b54918e81e60ddde45206fe1f90bdd83820c5 Mon Sep 17 00:00:00 2001 From: Yurii Chukhlib Date: Mon, 26 Jan 2026 11:58:23 +0100 Subject: [PATCH] fix(cron): anchor 'every' jobs to lastRunAtMs instead of now Fixes #1972 For jobs with schedule.kind="every", nextRunAtMs should be calculated from lastRunAtMs + everyMs, not from the current time (now). Previously, recomputeNextRuns() used 'now' as the base for all job types, causing schedule drift when job state was updated (e.g., via patch). For example, with everyMs: 3600000 (1 hour): - lastRunAtMs: 1769372901882 (3:28 PM) - Expected next: 1769376501882 (4:28 PM = lastRun + 1hr) - Actual stored: 1769381650482 (5:54 PM) +85 min drift This change anchors "every" jobs to lastRunAtMs (if available) while preserving existing behavior for "at" and "cron" job types. --- src/cron/service/jobs.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/cron/service/jobs.ts b/src/cron/service/jobs.ts index 132156a0c..169f23d5d 100644 --- a/src/cron/service/jobs.ts +++ b/src/cron/service/jobs.ts @@ -61,7 +61,13 @@ export function recomputeNextRuns(state: CronServiceState) { ); job.state.runningAtMs = undefined; } - job.state.nextRunAtMs = computeJobNextRunAtMs(job, now); + // For "every" jobs, anchor to lastRunAtMs to prevent schedule drift. + // For other job types (at, cron), use current time. + const baseTime = + job.schedule.kind === "every" && typeof job.state.lastRunAtMs === "number" + ? job.state.lastRunAtMs + : now; + job.state.nextRunAtMs = computeJobNextRunAtMs(job, baseTime); } }