fix(process): use monotonic time for provider execution budgets
This commit is contained in:
parent
b953f96c0a
commit
d9e9fb2273
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
@ -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. */
|
||||
|
|
|
|||
Loading…
Reference in New Issue