fix(process): use monotonic time for provider execution budgets

This commit is contained in:
morluto 2026-10-13 15:01:59 +08:00
parent b953f96c0a
commit d9e9fb2273
6 changed files with 47 additions and 12 deletions

View File

@ -176,11 +176,11 @@ export const runFirmwareCommand = async (context: {
};
let failure: unknown;
try {
const deadline = Date.now() + FIRMWARE_LIMITS.timeoutMs;
const deadline = performance.now() + FIRMWARE_LIMITS.timeoutMs;
while (!(await supervisor.waitForOutputClose(100))) {
checkDiagnostics();
if (signal.aborted) throw new AnalysisCancelledError(operation);
if (Date.now() >= deadline)
if (performance.now() >= deadline)
throw new AnalysisTimeoutError(operation, FIRMWARE_LIMITS.timeoutMs);
if (context.outputBudget !== undefined)
await inventoryFirmwareOutput(

View File

@ -279,6 +279,6 @@ export const checkRecoveryDeadline = (
signal?: AbortSignal,
): void => {
checkRecoveryCancellation(signal);
if (Date.now() >= deadline)
if (performance.now() >= deadline)
throw new AnalysisTimeoutError(OPERATION, RECOVERY_LIMITS.timeoutMs);
};

View File

@ -33,7 +33,7 @@ it("rejects an exhausted shared execution budget before launching the next comma
arguments: ["--version"],
cwd: "/tmp/unused",
environment: {},
deadline: Date.now() - 1,
deadline: performance.now() - 1,
launcher: async () => {
throw new Error("An expired budget must not launch");
},
@ -52,7 +52,7 @@ it("preserves caller cancellation before acquiring an expired execution budget",
arguments: ["--version"],
cwd: "/tmp/unused",
environment: {},
deadline: Date.now() - 1,
deadline: performance.now() - 1,
signal: AbortSignal.abort(),
launcher: async () => {
throw new Error("A cancelled request must not launch");
@ -86,7 +86,7 @@ const commandFixture = (
arguments: ["--version"],
cwd: "/tmp/unused",
environment: {},
deadline: Date.now() + 5_000,
deadline: performance.now() + 5_000,
launcher,
},
};
@ -140,7 +140,7 @@ it("enforces the shared deadline while exited output streams remain open", async
});
try {
await expect(
runWakaruCommand({ ...context, deadline: Date.now() + 50 }),
runWakaruCommand({ ...context, deadline: performance.now() + 50 }),
).rejects.toMatchObject({
_tag: "AnalysisTimeoutError",
operation: "recover_javascript_sources",

View File

@ -100,7 +100,7 @@ export class WakaruProvider implements JavaScriptRecoveryPort {
let tree: SafeOutputTree | undefined;
let failed: { readonly cause: unknown } | undefined;
let execution: AnalysisExecution | undefined;
const deadline = Date.now() + RECOVERY_LIMITS.timeoutMs;
const deadline = performance.now() + RECOVERY_LIMITS.timeoutMs;
try {
checkRecoveryCancellation(options?.signal);
const engine = await resolveWakaruCommand(this.environment);

View File

@ -0,0 +1,35 @@
import { afterEach, expect, it, vi } from "vitest";
import { ProviderStartupDeadline } from "./ProviderDeadline.js";
afterEach(() => {
vi.restoreAllMocks();
});
it("keeps remaining time and cancellation classification stable across wall-clock jumps", () => {
const dateNow = vi.spyOn(Date, "now").mockReturnValue(10_000);
const caller = new AbortController();
const deadline = new ProviderStartupDeadline(10_000, caller.signal);
try {
dateNow.mockReturnValue(9_000_000_000_000);
const remaining = deadline.remainingMs();
expect(remaining).toBeGreaterThan(0);
expect(remaining).toBeLessThanOrEqual(10_000);
expect(Number.isInteger(remaining)).toBe(true);
expect(deadline.interruption).toBeUndefined();
caller.abort(new Error("caller stopped startup"));
expect(deadline.interruption).toBe("cancelled");
} finally {
deadline.dispose();
}
});
it("aborts a bounded wait when its real monotonic deadline expires", async () => {
const deadline = new ProviderStartupDeadline(40);
try {
await expect(deadline.wait(500)).resolves.toBe("aborted");
expect(deadline.interruption).toBe("timeout");
expect(deadline.remainingMs()).toBe(0);
} finally {
deadline.dispose();
}
});

View File

@ -42,11 +42,11 @@ export class ProviderStartupDeadline {
) {
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
throw new RangeError("Provider startup timeout must be positive");
this.#deadlineAt = Date.now() + timeoutMs;
this.#deadlineAt = performance.now() + timeoutMs;
this.#externalSignal = signal;
this.#onExternalAbort = () => {
if (this.#controller.signal.aborted) return;
if (Date.now() >= this.#deadlineAt) {
if (performance.now() >= this.#deadlineAt) {
this.#interruption = "timeout";
this.#abort(
new DOMException("Provider startup deadline elapsed", "TimeoutError"),
@ -79,13 +79,13 @@ export class ProviderStartupDeadline {
get interruption(): "cancelled" | "timeout" | undefined {
return (
this.#interruption ??
(Date.now() >= this.#deadlineAt ? "timeout" : undefined)
(performance.now() >= this.#deadlineAt ? "timeout" : undefined)
);
}
/** Milliseconds still available to the next startup phase. */
remainingMs(): number {
return Math.max(0, this.#deadlineAt - Date.now());
return Math.max(0, Math.ceil(this.#deadlineAt - performance.now()));
}
/** Wait for at most one polling interval under the shared deadline signal. */