diff --git a/packages/core/src/gateway/core-runtime/supervisor.ts b/packages/core/src/gateway/core-runtime/supervisor.ts index e7f8d40a..92d256eb 100644 --- a/packages/core/src/gateway/core-runtime/supervisor.ts +++ b/packages/core/src/gateway/core-runtime/supervisor.ts @@ -23,7 +23,10 @@ import type { CoreGatewayHealth, ManagedGatewayRuntimeMarker } from "@ccr/core/g import { delay } from "@ccr/core/gateway/internal/clock"; const gatewayRuntimeStateKey = "gateway"; -const gatewayConfigAcceptanceTimeoutMs = 5_000; +// The config-acceptance budget spans child spawn, the gateway module +// require(), and the IPC round-trip through the parent event loop, so a +// loaded host legitimately needs well more than the old hardcoded 5s. +const defaultGatewayConfigAcceptanceTimeoutMs = 30_000; const gatewayStartupTimeoutMs = 15_000; const gatewayChildOutputLimit = 4000; const privateDirMode = 0o700; @@ -98,6 +101,19 @@ export function spawnGatewayProcess( }; } +// CCR_GATEWAY_CONFIG_TIMEOUT_MS tunes the startup budget above for hosts +// whose process spawn / module load times exceed the default under load. +export function gatewayConfigAcceptanceTimeoutMs(env: NodeJS.ProcessEnv = process.env): number { + const raw = env.CCR_GATEWAY_CONFIG_TIMEOUT_MS?.trim(); + if (raw) { + const parsed = Number(raw); + if (Number.isFinite(parsed) && parsed >= 1_000) { + return Math.trunc(parsed); + } + } + return defaultGatewayConfigAcceptanceTimeoutMs; +} + function monitorGatewayConfigAcceptance(child: ChildProcess): { promise: Promise; reject: (error: Error) => void; @@ -134,9 +150,10 @@ function monitorGatewayConfigAcceptance(child: ChildProcess): { const onExit = (code: number | null, signal: NodeJS.Signals | null) => { finish(new Error(formatCoreGatewayChildExit(child, code, signal, "before accepting runtime config"))); }; + const acceptanceTimeoutMs = gatewayConfigAcceptanceTimeoutMs(); const timer = setTimeout(() => { - finish(new Error(`Core gateway did not accept runtime config within ${gatewayConfigAcceptanceTimeoutMs}ms.`)); - }, gatewayConfigAcceptanceTimeoutMs); + finish(new Error(`Core gateway did not accept runtime config within ${acceptanceTimeoutMs}ms. Set CCR_GATEWAY_CONFIG_TIMEOUT_MS to raise this limit.`)); + }, acceptanceTimeoutMs); rejectAcceptance = (error) => finish(error); child.on("message", onMessage); child.once("error", onError); diff --git a/packages/core/test/unit/gateway/gateway-config-acceptance-timeout.test.mjs b/packages/core/test/unit/gateway/gateway-config-acceptance-timeout.test.mjs new file mode 100644 index 00000000..97fd1797 --- /dev/null +++ b/packages/core/test/unit/gateway/gateway-config-acceptance-timeout.test.mjs @@ -0,0 +1,31 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { gatewayConfigAcceptanceTimeoutMs } from "@ccr/core/gateway/core-runtime/supervisor.ts"; + +test("gateway config acceptance timeout defaults to 30s when env is unset", () => { + assert.equal(gatewayConfigAcceptanceTimeoutMs({}), 30_000); +}); + +test("gateway config acceptance timeout honors CCR_GATEWAY_CONFIG_TIMEOUT_MS", () => { + assert.equal( + gatewayConfigAcceptanceTimeoutMs({ CCR_GATEWAY_CONFIG_TIMEOUT_MS: "60000" }), + 60_000 + ); +}); + +test("gateway config acceptance timeout trims whitespace and truncates fractions", () => { + assert.equal( + gatewayConfigAcceptanceTimeoutMs({ CCR_GATEWAY_CONFIG_TIMEOUT_MS: " 12345.9 " }), + 12_345 + ); +}); + +test("gateway config acceptance timeout rejects invalid or too-small overrides", () => { + for (const raw of ["", " ", "abc", "0", "-5000", "999", "NaN", "Infinity"]) { + assert.equal( + gatewayConfigAcceptanceTimeoutMs({ CCR_GATEWAY_CONFIG_TIMEOUT_MS: raw }), + 30_000, + `expected default for ${JSON.stringify(raw)}` + ); + } +}); \ No newline at end of file