File size: 3,024 Bytes
f778c12 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | // Process-boundary proof that the hard-exit watchdog survives main-thread starvation.
import { spawnSync } from "node:child_process";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { expect, it } from "vitest";
const WATCHDOG_DELAY_MS = 100;
const CHILD_TIMEOUT_MS = 3_000;
const watchdogModuleUrl = pathToFileURL(
path.resolve("src/cli/gateway-cli/shutdown-hard-exit.ts"),
).href;
function runWatchdogChild() {
const script = `
import { writeSync } from "node:fs";
import { armShutdownHardExitWatchdog } from ${JSON.stringify(watchdogModuleUrl)};
const fail = (message) => {
writeSync(2, \`error:\${message}\\n\`);
process.exit(2);
};
const firstWatchdog = armShutdownHardExitWatchdog({
delayMs: ${WATCHDOG_DELAY_MS},
onError: (error) => fail(String(error)),
});
if (firstWatchdog === null) {
fail("first-watchdog-not-armed");
}
firstWatchdog.cancel();
await new Promise((resolve) => setTimeout(resolve, ${WATCHDOG_DELAY_MS * 2}));
writeSync(1, "cancelled-watchdog-deadline-survived\\n");
process.once("beforeExit", () => {
// Reaching beforeExit proves the cancelled worker no longer retains process liveness.
writeSync(1, "cancelled-watchdog-released-liveness\\n");
const secondWatchdog = armShutdownHardExitWatchdog({
delayMs: ${WATCHDOG_DELAY_MS},
onError: (error) => fail(String(error)),
});
if (secondWatchdog === null) {
fail("second-watchdog-not-armed");
}
writeSync(1, "second-watchdog-armed\\n");
while (true) {}
});
`;
const startedAt = Date.now();
const result = spawnSync(
process.execPath,
["--import", "tsx", "--input-type=module", "--eval", script],
{
cwd: path.resolve("."),
encoding: "utf8",
env: { ...process.env, VITEST: undefined },
killSignal: "SIGKILL",
timeout: CHILD_TIMEOUT_MS,
},
);
return { elapsedMs: Date.now() - startedAt, result };
}
it("cancels one watchdog before another kills a blocked main thread", () => {
const { elapsedMs, result } = runWatchdogChild();
const diagnostics = JSON.stringify(
{
elapsedMs,
status: result.status,
signal: result.signal,
error: result.error ? String(result.error) : undefined,
stdout: result.stdout,
stderr: result.stderr,
},
null,
2,
);
const markers = [
"cancelled-watchdog-deadline-survived",
"cancelled-watchdog-released-liveness",
"second-watchdog-armed",
];
let previousMarkerIndex = -1;
for (const marker of markers) {
const markerIndex = result.stdout.indexOf(marker);
expect(markerIndex, diagnostics).toBeGreaterThan(previousMarkerIndex);
previousMarkerIndex = markerIndex;
}
expect(result.error, diagnostics).toBeUndefined();
expect(result.status, diagnostics).toBeNull();
expect(result.signal, diagnostics).toBe("SIGKILL");
expect(elapsedMs, diagnostics).toBeLessThan(CHILD_TIMEOUT_MS);
});
|