Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/croner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,13 @@ class Cron<T = undefined> {
waitMs = maxDelay;
}

// Clamp negative delays to 0 - some runtimes (e.g. Deno) treat large negative values
// as large positive delays due to 32-bit integer overflow, causing jobs with allowPast:true
// and a far-past date to never fire. Use a backoff when paused to avoid a tight loop.
if (waitMs < 0) {
waitMs = this._states.paused ? 1000 : 0;
}
Comment on lines +502 to +507
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clamping negative waitMs to 0 can create a tight rescheduling loop when the job is paused and the target time is already in the past (e.g. paused: true + allowPast: true + far-past once date). In that case _checkTrigger never fires because paused, but schedule() keeps computing a negative delay and scheduling setTimeout(..., 0) repeatedly, which can cause high CPU usage. Consider adding a small backoff when paused/overdue (or changing pause/resume behavior to avoid rescheduling while paused).

Copilot uses AI. Check for mistakes.
Comment on lines +503 to +507
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new paused-specific branch (waitMs = this._states.paused ? 1000 : 0) changes runtime behavior but doesn’t appear to be covered by existing tests. Consider adding a regression test for a fire-once job with allowPast: true and a far-past date created with { paused: true }, then resume() it and assert it fires within a bounded time (and not after an overflow-sized delay).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added regression test in 9d31329. The test creates a fire-once job with allowPast: true, paused: true, and a far-past date (2020-01-01), verifies it doesn't fire while paused, then calls resume() and asserts it fires within 2 seconds. All 552 tests pass.


// Start the timer loop
// _checkTrigger will either call _trigger (if it's time, croner isn't paused and whatever),
// or recurse back to this function to wait for next trigger
Expand Down
37 changes: 37 additions & 0 deletions test/croner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,43 @@ test(
}),
);

test(
"Fire-once job with allowPast: true, paused: true, and far-past date should fire after resume",
//@ts-ignore
timeout(4000, (resolve) => {
let fired = false;
let resumed = false;
let settled = false;
let watchdog: ReturnType<typeof setTimeout> | undefined;
const veryOldDate = new Date("2020-01-01T00:00:00");
const job = new Cron(veryOldDate, { allowPast: true, paused: true }, () => {
fired = true;

if (!resumed || settled) return;

settled = true;
if (watchdog) clearTimeout(watchdog);
job.stop();
assertEquals(fired, true);
resolve();
});

// Should not fire while paused
setTimeout(() => {
assertEquals(fired, false);
resumed = true;
job.resume();

watchdog = setTimeout(() => {
if (settled) return;
settled = true;
job.stop();
assertEquals(fired, true);
}, 2000);
}, 100);
}),
);

test("Fire-once job with no allowPast and date > 1s in past should have null nextRun and not be running", function () {
// A once-job 3 seconds in the past without allowPast should silently not schedule
const pastTime = new Date(Date.now() - 3000);
Expand Down
Loading