← Back to PRs

#14667: fix: preserve missed cron runs when updating job schedule

by WalterSumbon open 2026-02-12 13:24 View on GitHub →
stale size: M
Fixes #14658 ## Problem When a cron job is updated (via `cron.update` or config changes), the scheduler recalculated `nextRunAtMs` from "now" rather than preserving missed run times. This caused jobs to skip days of executions after being updated. ### Example scenario from #14658 - Job last ran **Feb 6**, `nextRunAtMs = Feb 7` - Job should have run **Feb 7-12** but didn't trigger - User updates job on **Feb 11** (e.g., changes wakeMode or other config) - **Old behavior:** `nextRunAtMs` jumps to **Feb 13** (skips 6 days!) - **New behavior:** `nextRunAtMs` stays **Feb 7** (catches up on next wake) ## Root Cause In `src/cron/service/ops.ts`, the `update()` function unconditionally recomputed `nextRunAtMs` from "now" when schedule or enabled state changed: ```typescript // Old code (problematic) if (scheduleChanged || enabledChanged) { if (job.enabled) { job.state.nextRunAtMs = computeJobNextRunAtMs(job, now); // ❌ From now } } ``` This discarded any missed runs, causing silent job failures with no way to catch up. ## Solution Check if the job has a missed run (`nextRunAtMs < now`) before recomputing. If it does, **preserve** the old `nextRunAtMs` so the job can catch up on the next scheduler wake: ```typescript // New code (fixed) if (scheduleChanged || enabledChanged) { if (job.enabled) { const currentNext = job.state.nextRunAtMs; const hasMissedRun = typeof currentNext === "number" && Number.isFinite(currentNext) && currentNext < now; if (hasMissedRun) { // Keep the missed run time - job will execute on next wake job.state.nextRunAtMs = currentNext; } else { // No missed run or no previous nextRunAtMs - compute fresh job.state.nextRunAtMs = computeJobNextRunAtMs(job, now); } } } ``` ## Behavior | Scenario | Old Behavior | New Behavior | |----------|-------------|--------------| | Update job with missed run (nextRunAtMs < now) | ❌ Skip to future | ✅ Preserve missed time | | Update job with future run (nextRunAtMs > now) | ✅ Recompute | ✅ Recompute | | Update job with no nextRunAtMs (disabled → enabled) | ✅ Compute fresh | ✅ Compute fresh | ## Testing Added comprehensive regression test suite (`service.issue-14658-regression.test.ts`): - ✅ Preserve missed run when updating schedule - ✅ Preserve missed run when changing schedule kind (e.g., `at` → `every`) - ✅ Recompute when no missed run exists (future nextRunAtMs) - ✅ Recompute when job had no previous nextRunAtMs (disabled → enabled) - ✅ Demonstrate the 6-day skip scenario from #14658 ## Related This is complementary to PR #14068 (merged), which fixed a similar issue in the scheduler timer logic. That PR prevented `recomputeNextRuns()` from advancing past-due times during maintenance. This PR prevents the `update()` function from doing the same thing. Both fixes work together to ensure missed runs are never silently discarded. <!-- greptile_comment --> <h2>Greptile Overview</h2> <h3>Greptile Summary</h3> This PR updates the cron service’s `update()` operation to preserve an existing past-due `job.state.nextRunAtMs` when a job’s schedule or enabled state changes. Previously, `nextRunAtMs` was always recomputed from “now”, which could skip over missed executions after an update. It also adds a regression test suite that simulates persisted store state with past-due `nextRunAtMs` values and verifies that updates do not advance those timestamps, allowing jobs to run on the next wake/run cycle. The change aligns `update()` behavior with the broader scheduler approach of not silently discarding missed runs. <h3>Confidence Score: 5/5</h3> - This PR is safe to merge with minimal risk. - The change is narrowly scoped to `cron/service/ops.update()` and matches existing scheduler semantics elsewhere (preserving past-due runs). The added tests exercise the regression scenarios described, and no unverified or concrete functional issues were found in the diff after tracing how `nextRunAtMs` is used by due detection and post-run recomputation. - No files require special attention <!-- greptile_other_comments_section --> <!-- /greptile_comment -->

Most Similar PRs