Skip to content

Fix large-negative setTimeout delay causing allowPast jobs to never fire in Deno#369

Merged
Hexagon merged 4 commits intodevfrom
copilot/investigate-ci-test-failures
Apr 3, 2026
Merged

Fix large-negative setTimeout delay causing allowPast jobs to never fire in Deno#369
Hexagon merged 4 commits intodevfrom
copilot/investigate-ci-test-failures

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 2, 2026

When scheduling a fire-once job with allowPast: true and a far-past date, msToNext() returns a large negative value. Deno coerces this to an unsigned 32-bit integer before passing it to the OS timer, turning e.g. -197331393963 ms into ~237101653 ms (~2.7 days), so the job silently never fires.

Change

  • src/croner.tsschedule(): Clamp waitMs to 0 when negative, after the existing maxDelay cap. When the job is paused, use a 1 s backoff instead of 0 to prevent a tight setTimeout(0)_checkTriggerschedule() rescheduling loop.
if (waitMs > maxDelay) waitMs = maxDelay;
if (waitMs < 0) waitMs = this._states.paused ? 1000 : 0;  // ← new
  • test/croner.test.ts: Added a regression test that creates a fire-once job with allowPast: true, paused: true, and a far-past date, then calls resume() and asserts it fires within a bounded time.

_checkTrigger still evaluates now >= target before firing, so clamping to 0 means "check immediately" — not "fire unconditionally". Node.js and Bun were unaffected because they perform this clamping natively; Deno does not.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a cross-runtime scheduling bug where very large negative setTimeout delays (e.g., fire-once jobs in the far past with allowPast: true) can be coerced into large positive delays in Deno, preventing the job from firing.

Changes:

  • Clamp negative waitMs values to 0 in Cron.schedule() after applying the existing maxDelay cap.
  • Add an explanatory comment documenting the Deno-specific overflow/coercion behavior and its impact.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +502 to +507
// 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.
if (waitMs < 0) {
waitMs = 0;
}
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.
@Hexagon
Copy link
Copy Markdown
Owner

Hexagon commented Apr 2, 2026

@copilot apply changes based on the comments in this thread . Keep it safe. Minimal changes.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 2, 2026

@copilot apply changes based on the comments in this thread . Keep it safe. Minimal changes.

Applied in 5e06563. When waitMs < 0 and the job is paused, we now use a 1s backoff instead of 0 to prevent the tight setTimeout(0)_checkTriggerschedule() loop. When not paused, still clamps to 0 so the job fires immediately. All 551 tests pass.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +503 to +507
// 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;
}
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.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@Hexagon Hexagon merged commit 2bdf413 into dev Apr 3, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants