#22948: fix(cron): every-schedule boundary returns nowMs instead of next slot (#22895)
size: M
trusted-contributor
Cluster:
Cron Job Management Fixes
## Problem
When `elapsed` is an exact multiple of `everyMs` in `computeNextRunAtMs`, the ceiling-division formula `Math.floor((elapsed + everyMs - 1) / everyMs)` returns the **current** slot instead of the next one, producing `nextRunAtMs === nowMs`.
This causes the job to be perpetually "due" after a gateway restart:
1. `recomputeNextRuns` sees `now >= nextRun` → recomputes → gets the same value
2. `runMissedJobs` double-executes the job
3. `applyJobResult` recomputes `nextRunAtMs` from `endedAt`, landing on a later anchor-aligned slot
4. Dashboard shows an incorrect NEXT time (e.g. **56m for a 30m job**)
### Steps to reproduce (from #22895)
1. Create a cron job with `schedule.kind = "every"`, `everyMs = 1800000` (30m)
2. Let it run successfully
3. Restart the gateway
4. Observe NEXT shows ~56m instead of ~24m
## Fix
Replace the ceiling-division with `Math.floor(elapsed / everyMs) + 1` which always returns a **strictly-future** slot. Both formulas produce identical results for non-boundary cases; only the exact-multiple edge case differs.
```diff
- const steps = Math.max(1, Math.floor((elapsed + everyMs - 1) / everyMs));
+ const steps = Math.floor(elapsed / everyMs) + 1;
```
## Testing
- 3 new tests covering the restart scenario, stale `runningAtMs`, and all anchor offsets
- All 178 existing cron tests pass (32 test files, 0 failures)
```
✓ preserves correct nextRunAtMs for every-30m job after restart (last ran 6m ago)
✓ handles restart when every-30m job was running (stale runningAtMs)
✓ next run never exceeds everyMs from now for various anchor offsets
```
Fixes #22895
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Fixes a critical boundary condition bug in `every`-schedule cron jobs where `elapsed` being an exact multiple of `everyMs` caused `computeNextRunAtMs` to return `nowMs` instead of a future time. This led to perpetual "due" status after gateway restarts, resulting in double-execution and incorrect dashboard timing displays (e.g., 56m for a 30m job).
**Key changes:**
- Replaced ceiling-division formula with `Math.floor(elapsed / everyMs) + 1` in `src/cron/schedule.ts:45`, which always returns a strictly-future slot
- Removed unnecessary `Math.max(1, ...)` wrapper since new formula guarantees result ≥ 1
- Added comprehensive test coverage for restart scenarios, stale `runningAtMs`, and anchor offset edge cases
**Mathematical correctness:**
- Old formula at boundary: `elapsed = everyMs` → `steps = 1` → `nextRunAtMs = nowMs` (bug)
- New formula at boundary: `elapsed = everyMs` → `steps = 2` → `nextRunAtMs = nowMs + everyMs` (correct)
- All non-boundary cases produce identical results
**Test coverage:**
- 3 new tests specifically for issue #22895 (218 lines total)
- Tests verify restart behavior, stale state handling, and all anchor offsets (0-29 minutes)
- All 178 existing cron tests remain passing
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge with minimal risk - it fixes a well-defined boundary condition bug with thorough test coverage
- The fix is mathematically sound and minimal (single line change), addresses a specific bug with clear reproduction steps, includes comprehensive test coverage (3 new tests covering all edge cases), and all existing tests pass. The change preserves behavior for all non-boundary cases while correctly fixing the exact-multiple edge case. No security or performance concerns.
- No files require special attention
<sub>Last reviewed commit: 629445e</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#23290: fix(cron): use lastRunAtMs for next schedule of interval jobs after...
by SidQin-cyber · 2026-02-22
89.7%
#12747: fix: catch up missed cron-expression job runs on restart
by obin94-commits · 2026-02-09
84.2%
#9060: Fix: Preserve scheduled cron jobs after gateway restart
by vishaltandale00 · 2026-02-04
83.8%
#7022: fix(cron): prevent schedule drift on gateway restart for 'every' jobs
by marciob · 2026-02-02
83.6%
#22911: fix(cron): correct next execution time calculation after gateway re...
by anandsuraj · 2026-02-21
82.8%
#8034: fix(cron): run past-due one-shot jobs immediately on startup
by FelixFoster · 2026-02-03
81.5%
#16132: fix(cron): prevent duplicate job fires via MIN_REFIRE_GAP_MS guard
by widingmarcus-cyber · 2026-02-14
81.4%
#17838: fix: prevent cron job spin loop by not recomputing nextRunAtMs for ...
by MisterGuy420 · 2026-02-16
80.8%
#11857: fix: recompute stale cron nextRunAtMs on gateway restart
by Yida-Dev · 2026-02-08
80.8%
#13796: fix: skip recomputing nextRunAtMs for running cron jobs (#13739)
by echoVic · 2026-02-11
80.6%