← Back to PRs

#8379: fix(cron): handle past-due one-shot 'at' jobs that haven't run yet

by Gerrald12312 open 2026-02-04 00:01 View on GitHub →
stale
## Problem One-shot `at` schedule cron jobs never fire when created with `atMs` in the past. **Root cause**: In `src/cron/schedule.ts`, `computeNextRunAtMs` returns `undefined` when `schedule.atMs <= nowMs`. This causes `job.state.nextRunAtMs` to be `undefined`, and `isJobDue` checks `typeof job.state.nextRunAtMs === 'number'` which fails. ## Solution Added a special case in `isJobDue` to handle past-due one-shot jobs that haven't run yet: ```typescript // Handle past-due one-shot "at" jobs that haven't run yet: // When atMs <= nowMs, computeNextRunAtMs returns undefined, so nextRunAtMs is never set. // But these jobs should still fire immediately if they've never run. if ( job.schedule.kind === 'at' && !job.state.lastRunAtMs && nowMs >= job.schedule.atMs ) { return true; } ``` This allows: - Jobs scheduled for the past to fire immediately on next tick - Jobs that failed before running to retry - Normal future-scheduled jobs to continue working as before ## Testing - Ran the full test suite (`pnpm test`) - all cron tests pass including `src/cron/service.runs-one-shot-main-job-disables-it.test.ts` ## Why this approach Instead of modifying `computeNextRunAtMs` to return `atMs` even when past, I chose to handle this edge case in `isJobDue` because: 1. It's more explicit about the intent ("job is due because it never ran") 2. It doesn't change the semantics of `nextRunAtMs` for other code paths 3. The fix is localized and easy to understand <!-- greptile_comment --> <h2>Greptile Overview</h2> <h3>Greptile Summary</h3> This PR updates `src/cron/service/jobs.ts` to treat one-shot `at` jobs with `atMs` in the past as immediately due when they have never run, by extending `isJobDue` with a special-case check. The change aims to address a scheduling edge case where `computeNextRunAtMs` (in `src/cron/schedule.ts`) returns `undefined` for `atMs <= nowMs`, preventing `nextRunAtMs` from being set and thus preventing the job from being considered due. <h3>Confidence Score: 2/5</h3> - This PR has a meaningful risk of not actually fixing the reported bug in the main execution path. - Although `isJobDue` now considers past-due one-shot `at` jobs due, the scheduler loop (`runDueJobs` in timer.ts) appears to decide due-ness directly from `nextRunAtMs` without calling `isJobDue`, so past-due `at` jobs with `nextRunAtMs` unset would still never execute. If there are other call sites that use `isJobDue`, the fix may be partial or inconsistent across paths. - src/cron/service/timer.ts (due-job selection) and src/cron/service/jobs.ts (due semantics) <!-- greptile_other_comments_section --> <sub>(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!</sub> <!-- /greptile_comment -->

Most Similar PRs