#14667: fix: preserve missed cron runs when updating job schedule
stale
size: M
Cluster:
Cron Job Management Fixes
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
#12443: fix(cron): don't advance past-due jobs that haven't been executed
by rummangeminicode · 2026-02-09
89.6%
#12448: fix: prevent cron list/status from silently skipping due jobs
by Yida-Dev · 2026-02-09
89.3%
#12982: fix(cron): prevent status/list from advancing overdue job nextRunAtMs
by hclsys · 2026-02-10
88.2%
#12747: fix: catch up missed cron-expression job runs on restart
by obin94-commits · 2026-02-09
87.8%
#11108: fix(cron): prevent missed jobs from being skipped on timer recompute
by Bentlybro · 2026-02-07
86.7%
#9060: Fix: Preserve scheduled cron jobs after gateway restart
by vishaltandale00 · 2026-02-04
86.1%
#13796: fix: skip recomputing nextRunAtMs for running cron jobs (#13739)
by echoVic · 2026-02-11
85.7%
#12303: fix(cron): correct nextRunAtMs calculation and prevent timer stall
by colddonkey · 2026-02-09
84.6%
#11813: fix(cron): ensure 'at' schedule type correctly registers nextWakeAt...
by leo-reifying · 2026-02-08
84.4%
#17949: fix: clear stale runningAtMs in cron.run() before already-running c...
by yasumorishima · 2026-02-16
84.3%