← back to Costa Rica
costa-rica: consolidate server.js onto lib/db's shared pool + explicit pool max (Cody gate, cycle 17) — TK-10346
f45a7968ecd9210278e410c2feb8c77e64bc41e8 · 2026-09-24 00:16:03 -0700 · Steve
Cody's cycle-16 root-cause: server.js created its OWN `new Pool(...)` separate from
lib/db.js's "single shared" pool, so the running server held TWO pools against one
DB (double the connections; the cycle-16 test-hang was the first symptom).
Fix: server.js now `const { pool } = require('./lib/db')` — dropped the duplicate
`new Pool(...)` + the now-unused `{ Pool } = require('pg')` import. All 35 inline
pool.query() call sites are unchanged (the const now points at the shared pool);
app.locals.pool === lib/db.pool (verified true at runtime), so the test cleanup
closes one pool for every route.
This is STRICTLY BETTER, not just cleanup (Cody verified empirically): server.js's
old private pool had NO `pool.on('error')` handler; lib/db's has one. A node-pg Pool
that emits 'error' (idle-client disconnect, DB restart blip) with no listener throws
as an uncaughtException — NOT caught by the cycle-10 unhandledRejection net (wrong
event class), so the OLD code would have crashed the whole process on a single idle
blip. The shared pool handles it.
Also (Cody's "do now"): added an EXPLICIT `max` to lib/db's pool
(`Number(process.env.PG_POOL_MAX) || 20`). The old two-pool setup accidentally
allowed ~20 connections (2 x node-pg's default 10) — never a chosen capacity;
consolidating would have silently halved it to 10. 20 makes that ceiling a
deliberate, documented, env-tunable value on the single fork-mode process
(Postgres max_connections=100).
Cody gate — SHIP IT, verified strictly-better on every axis (read pm2/pg source,
ran live EventEmitter + default-max probes, grepped every lib/db requirer + every
pool.end/SIGTERM/app.locals.pool use): require-order safe (dotenv line 3 before the
require; lib/db was already required by the route modules anyway); no graceful-
shutdown pool.end() exists to break; nothing else reads app.locals.pool or imported
the old pool.
Follow-up logged (pre-existing, NOT this diff): booking-pay-idempotency.test.js-class
tests require lib/db without calling dotenv.config(), so a bare `npm test` (no
exported DATABASE_URL) fails 3 tests — env-dependent; they should load dotenv
themselves so CI is env-independent.
Suite 190/190.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic
Files touched
M lib/db.jsM server.jsM test/server-routes.test.js
Diff
commit f45a7968ecd9210278e410c2feb8c77e64bc41e8
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Sep 24 00:16:03 2026 -0700
costa-rica: consolidate server.js onto lib/db's shared pool + explicit pool max (Cody gate, cycle 17) — TK-10346
Cody's cycle-16 root-cause: server.js created its OWN `new Pool(...)` separate from
lib/db.js's "single shared" pool, so the running server held TWO pools against one
DB (double the connections; the cycle-16 test-hang was the first symptom).
Fix: server.js now `const { pool } = require('./lib/db')` — dropped the duplicate
`new Pool(...)` + the now-unused `{ Pool } = require('pg')` import. All 35 inline
pool.query() call sites are unchanged (the const now points at the shared pool);
app.locals.pool === lib/db.pool (verified true at runtime), so the test cleanup
closes one pool for every route.
This is STRICTLY BETTER, not just cleanup (Cody verified empirically): server.js's
old private pool had NO `pool.on('error')` handler; lib/db's has one. A node-pg Pool
that emits 'error' (idle-client disconnect, DB restart blip) with no listener throws
as an uncaughtException — NOT caught by the cycle-10 unhandledRejection net (wrong
event class), so the OLD code would have crashed the whole process on a single idle
blip. The shared pool handles it.
Also (Cody's "do now"): added an EXPLICIT `max` to lib/db's pool
(`Number(process.env.PG_POOL_MAX) || 20`). The old two-pool setup accidentally
allowed ~20 connections (2 x node-pg's default 10) — never a chosen capacity;
consolidating would have silently halved it to 10. 20 makes that ceiling a
deliberate, documented, env-tunable value on the single fork-mode process
(Postgres max_connections=100).
Cody gate — SHIP IT, verified strictly-better on every axis (read pm2/pg source,
ran live EventEmitter + default-max probes, grepped every lib/db requirer + every
pool.end/SIGTERM/app.locals.pool use): require-order safe (dotenv line 3 before the
require; lib/db was already required by the route modules anyway); no graceful-
shutdown pool.end() exists to break; nothing else reads app.locals.pool or imported
the old pool.
Follow-up logged (pre-existing, NOT this diff): booking-pay-idempotency.test.js-class
tests require lib/db without calling dotenv.config(), so a bare `npm test` (no
exported DATABASE_URL) fails 3 tests — env-dependent; they should load dotenv
themselves so CI is env-independent.
Suite 190/190.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic
---
lib/db.js | 10 +++++++++-
server.js | 9 ++++++---
test/server-routes.test.js | 10 +++-------
3 files changed, 18 insertions(+), 11 deletions(-)
diff --git a/lib/db.js b/lib/db.js
index c807fec..6152bc0 100644
--- a/lib/db.js
+++ b/lib/db.js
@@ -1,6 +1,14 @@
'use strict';
// Single shared pg pool for server.js + all route/lib modules.
const { Pool } = require('pg');
-const pool = new Pool({ connectionString: process.env.DATABASE_URL });
+// Explicit, deliberate connection ceiling (env-tunable). Before cycle 17 server.js
+// ran a SECOND pool, so the process accidentally allowed up to ~20 connections
+// (2 x node-pg's default max of 10) — never a chosen capacity. Now there is one
+// pool with a documented max: 20 preserves that ceiling as an intentional value on
+// a single fork-mode process (Postgres max_connections is 100), tunable via PG_POOL_MAX.
+const pool = new Pool({
+ connectionString: process.env.DATABASE_URL,
+ max: Number(process.env.PG_POOL_MAX) || 20,
+});
pool.on('error', (err) => console.error('[db] idle client error', err.message));
module.exports = { pool, query: (t, p) => pool.query(t, p) };
diff --git a/server.js b/server.js
index e7da0fa..0f7956a 100644
--- a/server.js
+++ b/server.js
@@ -3,16 +3,19 @@
require('dotenv').config();
const express = require('express');
const basicAuth = require('express-basic-auth');
-const { Pool } = require('pg');
const path = require('path');
const { harden } = require('./lib/async-harden'); // forward async route throws -> error handler (Express 4)
+// Use lib/db's SINGLE shared pg pool (it also registers an idle-client 'error'
+// handler that this file's old private pool lacked). server.js previously created
+// its OWN `new Pool(...)` with the same connectionString, so the server ran TWO
+// pools against one DB — double the connections, and (cycle 16) a test-hang from
+// the un-closed second pool. Consolidated onto the shared pool. (Cody gate, cycle 17.)
+const { pool } = require('./lib/db');
const PORT = parseInt(process.env.PORT || '9791', 10);
const SITE_NAME = process.env.SITE_NAME || 'Costa Rica Directory';
const SITE_DOMAIN = process.env.SITE_DOMAIN || 'costarica.agentabrams.com';
-const pool = new Pool({ connectionString: process.env.DATABASE_URL });
-
// M2/R4 — log the full error server-side, return a GENERIC message to the client.
// Never leak DB/driver text (table names, SQL, connection strings) to a caller.
function serverError(res, e, where) {
diff --git a/test/server-routes.test.js b/test/server-routes.test.js
index 29a8900..735ddee 100644
--- a/test/server-routes.test.js
+++ b/test/server-routes.test.js
@@ -29,14 +29,10 @@ before(async () => {
});
after(async () => {
server && server.close();
- // TWO pools exist: server.js has its own (app.locals.pool, used by the inline
- // routes) AND lib/db.js has a separate shared pool (used by the mounted
- // sub-routers /api/app, /api/admin, /api/build, /webhooks). Importing server.js
- // creates both; close BOTH or a future test that hits a sub-router hangs the test
- // process on a dangling connection. (Cody gate, cycle 16 — root cause: server.js
- // duplicating lib/db's pool; follow-up ticket to consolidate.)
+ // As of cycle 17 server.js uses lib/db's SINGLE shared pool, so app.locals.pool
+ // and lib/db.pool are the SAME object — one end() closes every route's connections
+ // (inline routes + the mounted sub-routers). (Cody gate, cycle 16/17.)
try { await app.locals.pool.end(); } catch { /* already closed */ }
- try { await require('../lib/db').pool.end(); } catch { /* already closed */ }
});
function get(path) {
← 1d24359 cycle 16 docs: YOLO_NOTES ledger — server.js app export + in
·
back to Costa Rica
·
cycle 17 docs: YOLO_NOTES ledger — pool consolidation + expl cb24e9d →