diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 3322dd9f..a400b54a 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -29,11 +29,31 @@ const connectionString = process.env.DATABASE_URL!; // during clients-page fanout without log-storm. const POOL_MAX = process.env.NODE_ENV === 'development' ? 30 : 20; +// Pool reliability hardening (post-audit F8): +// During the audit the dev server twice entered a stuck state where every +// query 500'd with `write CONNECT_TIMEOUT` while the DB was healthy +// (1 of 100 connections used, queryable from psql immediately). +// The Docker bridge can silently drop TCP sockets and postgres-js's pool +// holds onto the stale handles until max_lifetime expires. +// - connect_timeout: 5s so failures surface fast instead of stalling +// requests for 10s before erroring. +// - max_lifetime: 10min so connections recycle before stale sockets +// accumulate. Was 30min — too long for the Docker socket-drop pattern. +// - onnotice: surfaces postgres NOTICE/WARNING messages that we'd +// otherwise miss (extension warnings, deprecation hints). const queryClient = postgres(connectionString, { max: POOL_MAX, idle_timeout: 20, - connect_timeout: 10, - max_lifetime: 60 * 30, + connect_timeout: 5, + max_lifetime: 60 * 10, + onnotice: (notice) => { + // postgres-js types `notice` as `unknown`; the runtime shape is + // { severity, code, message, ... }. Only surface WARNING+. + const n = notice as { severity?: string; message?: string }; + if (n.severity && n.severity !== 'NOTICE') { + console.warn(`[postgres ${n.severity}] ${n.message ?? ''}`); + } + }, connection: { // ms values per postgres.js types; these become Postgres GUC settings // applied at session start.