From 72d5bbc96d221709e9d9c9de96061dd5bebee7f1 Mon Sep 17 00:00:00 2001 From: Jose Camargo Date: Tue, 13 Oct 2026 15:55:46 -0300 Subject: [PATCH] fix(process): keep provider deadlines independent of wall-clock changes (#2311) * fix(process): keep provider deadlines independent of wall-clock changes * fix(process): retain monotonic targets after early timers Adapt the stronger timer/poll owner from @nosmile99 PR #2793 as integrated by 21aa877e5 and captured main 94072364c7f8410b0e86e72c912c2611572b278f. Retain all contributor reliability coverage. Tests and workloads skipped by explicit request. --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- src/process/ProviderDeadline.test.ts | 101 ++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/src/process/ProviderDeadline.test.ts b/src/process/ProviderDeadline.test.ts index ca83d98ab7..6f26c870e1 100644 --- a/src/process/ProviderDeadline.test.ts +++ b/src/process/ProviderDeadline.test.ts @@ -1,8 +1,106 @@ -import { afterEach, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, expect, it, vi } from "vitest"; import { ProviderStartupDeadline } from "./ProviderDeadline.js"; +beforeEach(() => { + vi.useFakeTimers({ + toFake: ["setTimeout", "clearTimeout", "Date", "performance"], + }); +}); + afterEach(() => { vi.restoreAllMocks(); + vi.useRealTimers(); +}); + +it.each([-3_600_000, 3_600_000])( + "keeps the elapsed budget when the wall clock jumps by %i ms", + async (jump) => { + const deadline = new ProviderStartupDeadline(1000); + try { + await vi.advanceTimersByTimeAsync(250); + vi.setSystemTime(Date.now() + jump); + expect(deadline.remainingMs()).toBe(750); + expect(deadline.interruption).toBeUndefined(); + expect(deadline.signal.aborted).toBe(false); + const waiting = deadline.wait(2000); + await vi.advanceTimersByTimeAsync(749); + expect(deadline.interruption).toBeUndefined(); + await vi.advanceTimersByTimeAsync(1); + await expect(waiting).resolves.toBe("aborted"); + expect(deadline.interruption).toBe("timeout"); + expect(deadline.remainingMs()).toBe(0); + expect(vi.getTimerCount()).toBe(0); + } finally { + deadline.dispose(); + } + }, +); + +it("preserves caller cancellation after a forward wall-clock correction", async () => { + const controller = new AbortController(); + const reason = new Error("caller cancelled"); + const deadline = new ProviderStartupDeadline(1000, controller.signal); + try { + const waiting = deadline.wait(1000); + await vi.advanceTimersByTimeAsync(100); + vi.setSystemTime(Date.now() + 3_600_000); + controller.abort(reason); + await expect(waiting).resolves.toBe("aborted"); + expect(deadline.interruption).toBe("cancelled"); + expect(deadline.signal.reason).toBe(reason); + expect(vi.getTimerCount()).toBe(0); + } finally { + deadline.dispose(); + } +}); + +it("does not revive an expired budget after the wall clock moves backward", async () => { + const deadline = new ProviderStartupDeadline(1000); + try { + await vi.advanceTimersByTimeAsync(1000); + vi.setSystemTime(Date.now() - 3_600_000); + expect(deadline.remainingMs()).toBe(0); + expect(deadline.interruption).toBe("timeout"); + await expect(deadline.wait(1000)).resolves.toBe("aborted"); + expect(vi.getTimerCount()).toBe(0); + } finally { + deadline.dispose(); + } +}); + +it("classifies elapsed time before a delayed timer callback and a later abort", () => { + const controller = new AbortController(); + const deadline = new ProviderStartupDeadline(1000, controller.signal); + try { + // Model an event-loop stall: elapsed time advanced, but timers have not run. + vi.spyOn(performance, "now").mockReturnValue(1001); + vi.setSystemTime(Date.now() - 3_600_000); + expect(deadline.interruption).toBe("timeout"); + expect(deadline.remainingMs()).toBe(0); + controller.abort(); + expect(deadline.interruption).toBe("timeout"); + expect(deadline.signal.reason).toMatchObject({ name: "TimeoutError" }); + expect(vi.getTimerCount()).toBe(0); + } finally { + deadline.dispose(); + } +}); + +it("keeps remaining milliseconds integral for downstream request budgets", () => { + const now = vi.spyOn(performance, "now").mockReturnValue(10.25); + const deadline = new ProviderStartupDeadline(1000); + try { + now.mockReturnValue(260.5); + expect(deadline.remainingMs()).toBe(750); + now.mockReturnValue(1010.2); + expect(deadline.remainingMs()).toBe(1); + expect(deadline.interruption).toBeUndefined(); + now.mockReturnValue(1010.25); + expect(deadline.remainingMs()).toBe(0); + expect(deadline.interruption).toBe("timeout"); + } finally { + deadline.dispose(); + } }); it("keeps remaining time and cancellation classification stable across wall-clock jumps", () => { @@ -24,6 +122,7 @@ it("keeps remaining time and cancellation classification stable across wall-cloc }); it("aborts a bounded wait when its real monotonic deadline expires", async () => { + vi.useRealTimers(); const deadline = new ProviderStartupDeadline(40); try { await expect(deadline.wait(500)).resolves.toBe("aborted");