-
Notifications
You must be signed in to change notification settings - Fork 118
fix: schedule-runner setTimeout overflow for far-future runAt schedules #80
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Nivesh353
wants to merge
2
commits into
open-gitagent:main
Choose a base branch
from
Nivesh353:fix/schedule-timer-overflow
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+111
−4
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import { describe, it, before } from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
|
|
||
| let scheduleAt: typeof import("../dist/schedule-runner.js").scheduleAt; | ||
| let activeTimers: typeof import("../dist/schedule-runner.js").activeTimers; | ||
|
|
||
| before(async () => { | ||
| const mod = await import("../dist/schedule-runner.js"); | ||
| scheduleAt = mod.scheduleAt; | ||
| activeTimers = mod.activeTimers; | ||
| }); | ||
|
|
||
| const MAX_TIMEOUT_MS = 2_147_483_647; | ||
|
|
||
| describe("scheduleAt (timer overflow guard)", () => { | ||
| it("delay < MAX_TIMEOUT_MS: fires in a single setTimeout, callback called exactly once", (t) => { | ||
| t.mock.timers.enable({ apis: ["setTimeout", "Date"] }); | ||
|
|
||
| const start = Date.now(); | ||
| const delay = 10 * 24 * 60 * 60 * 1000; // 10 days — well under the limit | ||
| const target = start + delay; | ||
|
|
||
| let fireCount = 0; | ||
| scheduleAt("short-job", target, () => fireCount++); | ||
|
|
||
| assert.equal(fireCount, 0, "must not fire before the delay elapses"); | ||
| t.mock.timers.tick(delay); | ||
| assert.equal(fireCount, 1, "must fire exactly once once the delay elapses"); | ||
| }); | ||
|
|
||
| it("delay slightly above MAX_TIMEOUT_MS: fires via two chunks, callback called once, at the right time", (t) => { | ||
| t.mock.timers.enable({ apis: ["setTimeout", "Date"] }); | ||
|
|
||
| const start = Date.now(); | ||
| const delay = MAX_TIMEOUT_MS + 1000; // just over the limit — needs a 2nd chunk | ||
| const target = start + delay; | ||
|
|
||
| let fireCount = 0; | ||
| let firedAt = 0; | ||
| scheduleAt("two-chunk-job", target, () => { | ||
| fireCount++; | ||
| firedAt = Date.now(); | ||
| }); | ||
|
|
||
| // After the first chunk (exactly MAX_TIMEOUT_MS), it must not have fired yet — | ||
| // there's still 1000ms of real delay left. | ||
| t.mock.timers.tick(MAX_TIMEOUT_MS); | ||
| assert.equal(fireCount, 0, "must not fire after only the first chunk"); | ||
|
|
||
| // The remaining 1000ms is the second chunk. | ||
| t.mock.timers.tick(1000); | ||
| assert.equal(fireCount, 1, "must fire exactly once after the second chunk"); | ||
| assert.equal(firedAt, target, "must fire exactly at the real target time"); | ||
| }); | ||
|
|
||
| it("delay <= 0 at call time: callback fires synchronously, no timer scheduled", (t) => { | ||
| t.mock.timers.enable({ apis: ["setTimeout", "Date"] }); | ||
|
|
||
| const now = Date.now(); | ||
| let fired = false; | ||
| scheduleAt("past-job", now - 1000, () => { | ||
| fired = true; | ||
| }); | ||
|
|
||
| assert.equal(fired, true, "must fire immediately when the target time is already in the past"); | ||
| assert.equal(activeTimers.has("past-job"), false, "must not leave a dangling timer entry"); | ||
| }); | ||
|
|
||
| it("cancel mid-chunk: clearing the activeTimers entry prevents the callback from firing", (t) => { | ||
| t.mock.timers.enable({ apis: ["setTimeout", "Date"] }); | ||
|
|
||
| const start = Date.now(); | ||
| const delay = MAX_TIMEOUT_MS + 1000; // needs 2 chunks, so there's a "mid-chunk" to cancel during | ||
| const target = start + delay; | ||
|
|
||
| let fired = false; | ||
| scheduleAt("cancelled-job", target, () => { | ||
| fired = true; | ||
| }); | ||
|
|
||
| // Cancel after the first chunk has elapsed, before the second chunk's callback fires. | ||
| t.mock.timers.tick(MAX_TIMEOUT_MS); | ||
| const timer = activeTimers.get("cancelled-job"); | ||
| assert.ok(timer, "a pending timer must exist mid-chunk"); | ||
| clearTimeout(timer); | ||
| activeTimers.delete("cancelled-job"); | ||
|
|
||
| // Advance past the point where it would have fired, if not cancelled. | ||
| t.mock.timers.tick(1000); | ||
| assert.equal(fired, false, "must not fire after being cancelled mid-chunk"); | ||
| }); | ||
| }); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
scheduleAt is exported and testable in isolation — this function needs unit tests before merge. Jest/Vitest fake timers make it straightforward: advance by MAX_TIMEOUT_MS in the long-delay case and confirm the callback fires exactly once at the right time, not before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added test/schedule-timer-overflow.test.ts covering all 4 cases: delay
under the limit (fires once), delay just over it (fires via two chunks,
once, at the right time), an already-past target (fires synchronously),
and cancellation mid-chunk (clearTimeout correctly blocks the callback).
Used the existing node:test + mock.timers setup (matches sdk.test.ts /
telemetry.test.ts) rather than Jest/Vitest, so no new dependency needed.
npm run build and npm test both pass — 31/31, no regressions.