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.
This commit is contained in:
Yurii Chukhlib 2026-01-26 11:58:23 +01:00
parent 6859e1e6a6
commit d81b54918e

View File

@ -61,7 +61,13 @@ export function recomputeNextRuns(state: CronServiceState) {
); );
job.state.runningAtMs = undefined; 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);
} }
} }