diff --git a/server/db/pg.js b/server/db/pg.js index 64f5ea8..a63739d 100644 --- a/server/db/pg.js +++ b/server/db/pg.js @@ -10,7 +10,27 @@ const path = require('path'); // to match the SQLite backend. Otherwise `expires_at < Date.now()` would compare a string to a number. types.setTypeParser(20, (v) => (v === null ? null : parseInt(v, 10))); -const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10 }); +// Pool sizing + SAFETY TIMEOUTS. The old `max:10` with no connectionTimeoutMillis meant that once all 10 +// connections were in use (a leaked/stuck client, a long query, or just a login burst) EVERY new query waited +// FOREVER for a free client — the "login spinner that never ends" outage. Now: +// • connectionTimeoutMillis — a saturated pool makes callers ERROR fast instead of hanging indefinitely. +// • statement_timeout — a runaway query is killed, so it can't pin a connection permanently. +// • idle_in_transaction_session_timeout — reclaims a client left "idle in transaction" (a hung tx()), +// which is the classic way a pool slowly leaks to zero. +// • max raised to 20 for more headroom. +// All tunable via env so they can be adjusted without a code change. +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + max: Number(process.env.PG_POOL_MAX || 20), + connectionTimeoutMillis: Number(process.env.PG_CONN_TIMEOUT_MS || 10000), + idleTimeoutMillis: Number(process.env.PG_IDLE_TIMEOUT_MS || 30000), + statement_timeout: Number(process.env.PG_STATEMENT_TIMEOUT_MS || 60000), + idle_in_transaction_session_timeout: Number(process.env.PG_IDLE_TX_TIMEOUT_MS || 60000), +}); + +// Don't let an unexpected idle-client error crash the process (pg emits 'error' on broken idle connections); +// the pool evicts the bad client and the next query gets a fresh one. +pool.on('error', (e) => { try { console.warn('[pg] idle client error:', e && e.message); } catch (_) {} }); // Repos use '?' placeholders (SQLite style); Postgres wants $1,$2,… — replace positionally. Safe because // no literal '?' appears inside any SQL string literal in this codebase.