Skip to content

Commit 61e7b04

Browse files
committed
fix(crestodian): cap probe timeouts
1 parent d10fd6b commit 61e7b04

2 files changed

Lines changed: 40 additions & 14 deletions

File tree

src/crestodian/probes.test.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
import { describe, expect, it } from "vitest";
2-
import { probeLocalCommand } from "./probes.js";
1+
import { afterEach, describe, expect, it, vi } from "vitest";
2+
import { probeGatewayUrl, probeLocalCommand } from "./probes.js";
3+
4+
const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
35

46
describe("crestodian probes", () => {
7+
afterEach(() => {
8+
vi.restoreAllMocks();
9+
});
10+
511
it("bounds noisy local command probe output", async () => {
612
const result = await probeLocalCommand(
713
process.execPath,
@@ -31,4 +37,21 @@ describe("crestodian probes", () => {
3137
expect(Date.now() - startedAt).toBeLessThan(2_000);
3238
},
3339
);
40+
41+
it("caps oversized gateway probe timeouts before scheduling", async () => {
42+
const timeoutSpy = vi
43+
.spyOn(globalThis, "setTimeout")
44+
.mockReturnValue(1 as unknown as ReturnType<typeof setTimeout>);
45+
vi.spyOn(globalThis, "clearTimeout").mockImplementation(() => undefined);
46+
vi.stubGlobal(
47+
"fetch",
48+
vi.fn(async () => new Response("ok", { status: 200 })),
49+
);
50+
51+
await expect(
52+
probeGatewayUrl("ws://127.0.0.1:1234", { timeoutMs: MAX_TIMER_TIMEOUT_MS + 1_000_000 }),
53+
).resolves.toMatchObject({ reachable: true });
54+
55+
expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS);
56+
});
3457
});

src/crestodian/probes.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { spawn } from "node:child_process";
2+
import { resolveTimerTimeoutMs } from "../shared/number-coercion.js";
23

34
export type LocalCommandProbe = {
45
command: string;
@@ -20,9 +21,13 @@ export async function probeLocalCommand(
2021
args: string[] = ["--version"],
2122
opts: { outputLimit?: number; timeoutKillGraceMs?: number; timeoutMs?: number } = {},
2223
): Promise<LocalCommandProbe> {
23-
const timeoutMs = opts.timeoutMs ?? 1_500;
24+
const timeoutMs = resolveTimerTimeoutMs(opts.timeoutMs, 1_500);
2425
const outputLimit = opts.outputLimit ?? LOCAL_COMMAND_PROBE_OUTPUT_MAX_CHARS;
25-
const timeoutKillGraceMs = opts.timeoutKillGraceMs ?? LOCAL_COMMAND_PROBE_KILL_GRACE_MS;
26+
const timeoutKillGraceMs = resolveTimerTimeoutMs(
27+
opts.timeoutKillGraceMs,
28+
LOCAL_COMMAND_PROBE_KILL_GRACE_MS,
29+
0,
30+
);
2631
return await new Promise((resolve) => {
2732
let stdout = "";
2833
let stderr = "";
@@ -51,15 +56,12 @@ export async function probeLocalCommand(
5156
const timer = setTimeout(() => {
5257
timedOut = true;
5358
child.kill("SIGTERM");
54-
killTimer = setTimeout(
55-
() => {
56-
child.kill("SIGKILL");
57-
child.stdout.destroy();
58-
child.stderr.destroy();
59-
finish(timeoutResult());
60-
},
61-
Math.max(0, timeoutKillGraceMs),
62-
);
59+
killTimer = setTimeout(() => {
60+
child.kill("SIGKILL");
61+
child.stdout.destroy();
62+
child.stderr.destroy();
63+
finish(timeoutResult());
64+
}, timeoutKillGraceMs);
6365
killTimer.unref?.();
6466
}, timeoutMs);
6567
child.stdout.setEncoding("utf8");
@@ -99,8 +101,9 @@ export async function probeGatewayUrl(
99101
): Promise<{ reachable: boolean; url: string; error?: string }> {
100102
const httpUrl = url.replace(/^ws:/, "http:").replace(/^wss:/, "https:");
101103
const healthUrl = new URL("/healthz", httpUrl).toString();
104+
const timeoutMs = resolveTimerTimeoutMs(opts.timeoutMs, 900);
102105
const controller = new AbortController();
103-
const timeout = setTimeout(() => controller.abort(), opts.timeoutMs ?? 900);
106+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
104107
try {
105108
const response = await fetch(healthUrl, {
106109
method: "GET",

0 commit comments

Comments
 (0)