fix(mcp): cap watchdog and startup-handshake timeouts at the largest timer delay (#1966)

Node runs a setTimeout delay above 2^31-1 ms after 1 ms instead, so a huge
CODEGRAPH_WATCHDOG_TIMEOUT_MS SIGKILLed a healthy server at once and a huge
CODEGRAPH_STARTUP_HANDSHAKE_TIMEOUT_MS abandoned the launch within
milliseconds. Both parsers now cap at 2147483647.

(cherry picked from commit a242f579ce)
This commit is contained in:
Eric Minish
2026-09-27 08:57:42 -05:00
committed by Colby McHenry
parent 71985f2620
commit 080ceed082
4 changed files with 57 additions and 4 deletions
+16
View File
@@ -18,6 +18,13 @@ describe('config parsing', () => {
expect(parseWatchdogTimeoutMs('1500')).toBe(1500);
});
it('parseWatchdogTimeoutMs caps at the largest delay a timer can hold (#1966)', () => {
// Node runs a setTimeout delay above 2^31-1 ms after 1 ms instead.
expect(parseWatchdogTimeoutMs('2147483647')).toBe(2147483647);
expect(parseWatchdogTimeoutMs('2147483648')).toBe(2147483647);
expect(parseWatchdogTimeoutMs('3000000000')).toBe(2147483647);
});
it('deriveCheckIntervalMs stays within [50, 2000] and scales with the timeout', () => {
expect(deriveCheckIntervalMs(60_000)).toBe(2000); // clamped high
expect(deriveCheckIntervalMs(500)).toBe(100); // 500/5
@@ -90,6 +97,15 @@ describe('liveness watchdog (spawned, real watchdog process)', () => {
expect(r.signal === 'SIGKILL' || (r.signal === null && r.code !== 0 && r.code !== null)).toBe(true);
}
it('leaves a healthy process alone when the timeout is set past what a timer can hold (#1966)', async () => {
const r = await runChild(
{ CODEGRAPH_WATCHDOG_TIMEOUT_MS: '3000000000' },
'setTimeout(() => process.exit(0), 1000);',
8000
);
expect(r).toEqual({ code: 0, signal: null });
}, 12000);
it('SIGKILLs a process whose main thread wedges in a sync loop', async () => {
const r = await runChild(
{ CODEGRAPH_WATCHDOG_TIMEOUT_MS: '500' },
+18
View File
@@ -38,6 +38,24 @@ describe('parseStartupHandshakeTimeoutMs', () => {
it('floors fractional values', () => {
expect(parseStartupHandshakeTimeoutMs('2500.7')).toBe(2500);
});
it('caps at the largest delay a timer can hold (#1966)', () => {
expect(parseStartupHandshakeTimeoutMs('2147483647')).toBe(2147483647);
expect(parseStartupHandshakeTimeoutMs('2147483648')).toBe(2147483647);
expect(parseStartupHandshakeTimeoutMs('3000000000')).toBe(2147483647);
});
it('does not abandon a launch at once when the timeout is set past what a timer can hold (#1966)', async () => {
let abandoned = false;
const disarm = armStartupHandshakeTimeout(
() => { abandoned = true; },
new PassThrough(),
parseStartupHandshakeTimeoutMs('3000000000'),
);
await sleep(100);
disarm();
expect(abandoned).toBe(false);
});
});
describe('armStartupHandshakeTimeout', () => {