diff --git a/web/lib/db.test.ts b/web/lib/db.test.ts index 508a9e2..ecfa586 100644 --- a/web/lib/db.test.ts +++ b/web/lib/db.test.ts @@ -232,6 +232,14 @@ describe('createPool threads idleTimeoutMillis into the pg Pool (via getPool)', ] as const; const saved: Record = {}; + function configurePoolEnvironment(): void { + process.env.BENCH_DB_HOST = 'localhost'; + process.env.BENCH_DB_NAME = 'bench'; + process.env.BENCH_DB_USER = 'bench_reader'; + process.env.BENCH_DB_PASSWORD = 'fixture-pw'; + process.env.BENCH_DB_SSL = 'disable'; + } + beforeEach(async () => { for (const k of ENV_KEYS) saved[k] = process.env[k]; await resetPool(); @@ -246,11 +254,7 @@ describe('createPool threads idleTimeoutMillis into the pg Pool (via getPool)', }); it('uses the resolved timeouts as pool options', () => { - process.env.BENCH_DB_HOST = 'localhost'; - process.env.BENCH_DB_NAME = 'bench'; - process.env.BENCH_DB_USER = 'bench_reader'; - process.env.BENCH_DB_PASSWORD = 'fixture-pw'; // skips the IAM/Signer path - process.env.BENCH_DB_SSL = 'disable'; // avoids the BENCH_DB_CA requirement + configurePoolEnvironment(); process.env.BENCH_DB_IDLE_TIMEOUT_MS = '123456'; process.env.BENCH_DB_STATEMENT_TIMEOUT_MS = '23456'; @@ -262,6 +266,17 @@ describe('createPool threads idleTimeoutMillis into the pg Pool (via getPool)', expect(pool.options.idleTimeoutMillis).toBe(123456); expect(pool.options.statement_timeout).toBe(23456); }); + + it('handles idle-client errors on the shared pool', () => { + configurePoolEnvironment(); + const pool = getPool(); + const error = new Error('fixture idle-client disconnect'); + const log = vi.spyOn(console, 'error').mockImplementation(() => {}); + + expect(pool.listenerCount('error')).toBe(1); + expect(() => pool.emit('error', error)).not.toThrow(); + expect(log).toHaveBeenCalledWith('bench: idle PostgreSQL client error', error); + }); }); describe('requireEnv', () => { diff --git a/web/lib/db.ts b/web/lib/db.ts index 892bf7d..bed2535 100644 --- a/web/lib/db.ts +++ b/web/lib/db.ts @@ -173,7 +173,7 @@ export function passwordProvider(config: DbConfig): () => Promise { } function createPool(config: DbConfig = readConfig()): Pool { - return new Pool({ + const pool = new Pool({ host: config.host, port: config.port, database: config.database, @@ -184,6 +184,13 @@ function createPool(config: DbConfig = readConfig()): Pool { idleTimeoutMillis: config.idleTimeoutMillis, statement_timeout: config.statementTimeoutMillis, }); + // pg emits idle-client connection failures on the pool. Without this + // listener, EventEmitter turns a recoverable disconnect into an uncaught + // exception. The pool removes the failed client and replaces it on demand. + pool.on('error', (error) => { + console.error('bench: idle PostgreSQL client error', error); + }); + return pool; } // A single pool is shared across warm serverless invocations and survives