diff --git a/server/src/__tests__/postgres-null-socket-guard.test.ts b/server/src/__tests__/postgres-null-socket-guard.test.ts index d2e7fcbec65..e3df1d87dc3 100644 --- a/server/src/__tests__/postgres-null-socket-guard.test.ts +++ b/server/src/__tests__/postgres-null-socket-guard.test.ts @@ -152,6 +152,13 @@ describe("registerPostgresNullSocketGuard", () => { registerPostgresNullSocketGuard({ POSTGRES_NULL_SOCKET_GUARD_ENABLED: "false" }); expect(process.listenerCount("uncaughtException")).toBe(before); }); + + it("adds one listener however many times it runs", () => { + const before = process.listenerCount("uncaughtException"); + registerPostgresNullSocketGuard({}); + registerPostgresNullSocketGuard({}); + expect(process.listenerCount("uncaughtException")).toBe(before + 1); + }); }); describe("handlePostgresNullSocketGuardException", () => { @@ -257,10 +264,16 @@ describe("the driver crash sequence, reproduced against a fake wire server", () expect(result.code).toBe(0); }, 20_000); - it("a non-matching uncaught exception still ends the process", async () => { + it("a non-matching uncaught exception still ends the process and reports its cause", async () => { const result = await runFixture("unrelated-crash"); expect(messageTypes(result)).not.toContain("unexpected-survival"); expect(result.code).toBe(1); + // The logger writes the fatal record to standard output; Node's own + // default handler would have used standard error, but the guard + // replaces that handler for every uncaught exception, so this is the + // only place the message and the stack can appear. + expect(result.stdout).toContain("an unrelated failure the guard must not swallow"); + expect(result.stdout).toContain("at Immediate"); }, 20_000); }); diff --git a/server/src/postgres-null-socket-guard.ts b/server/src/postgres-null-socket-guard.ts index 57616df9b33..3822d05bd9a 100644 --- a/server/src/postgres-null-socket-guard.ts +++ b/server/src/postgres-null-socket-guard.ts @@ -97,9 +97,13 @@ export function handlePostgresNullSocketGuardException(error: unknown): void { /** * Registers the guard's `uncaughtException` listener, unless an operator * disables it with `POSTGRES_NULL_SOCKET_GUARD_ENABLED=false` (or `0`). - * Call this once, early, from the server entry point. + * Call this once, early, from the server entry point. This function is + * idempotent: a repeat call adds no second listener, so a caller that runs + * many times in one process (a test suite that calls `startServer()` + * repeatedly) never exceeds Node's default listener-count warning. */ export function registerPostgresNullSocketGuard(env: NodeJS.ProcessEnv = process.env): void { if (!isGuardEnabled(env)) return; + if (process.listeners("uncaughtException").includes(handlePostgresNullSocketGuardException)) return; process.on("uncaughtException", handlePostgresNullSocketGuardException); }