@accounter/server
Version:
Accounter GraphQL server
69 lines (59 loc) • 2.03 kB
text/typescript
import { monitorEventLoopDelay } from 'node:perf_hooks';
import type { Pool } from 'pg';
import { getTenantDbClientStats } from '../modules/app-providers/tenant-db-client.js';
const NS_PER_MS = 1e6;
export interface PoolMonitorOptions {
pool: Pool;
/** Emit a heartbeat this often. */
intervalMs: number;
/** Pool size, for saturation reporting. */
max: number;
}
/**
* Periodic health heartbeat for the Postgres pool.
*
* The failure this exists for is silent by construction: an exhausted pool
* produces no error, no CPU, and no log line — requests simply queue in
* `pool.connect()` forever. `waiting > 0` alongside `idle === 0` is that state,
* visible in a single line, and the heartbeat continuing to print at all is
* proof the event loop is still turning.
*/
export function startPoolMonitor({ pool, intervalMs, max }: PoolMonitorOptions): {
stop: () => void;
} {
const loopDelay = monitorEventLoopDelay({ resolution: 20 });
loopDelay.enable();
const timer = setInterval(() => {
const clients = getTenantDbClientStats();
const saturated = pool.waitingCount > 0 || pool.totalCount - pool.idleCount >= max;
const snapshot = {
msg: 'db-pool-heartbeat',
pool: {
total: pool.totalCount,
idle: pool.idleCount,
waiting: pool.waitingCount,
max,
},
sessions: {
holdingConnection: clients.holdingConnection,
maxIdleMs: clients.maxIdleMs,
},
eventLoopDelayP99Ms: Math.round(loopDelay.percentile(99) / NS_PER_MS),
};
// A saturated pool is one step from wedging every request — say so loudly.
if (saturated) {
console.error(JSON.stringify({ ...snapshot, level: 'error', saturated: true }));
} else {
console.log(JSON.stringify(snapshot));
}
loopDelay.reset();
}, intervalMs);
// The heartbeat must never be the reason the process stays alive.
timer.unref();
return {
stop: () => {
clearInterval(timer);
loopDelay.disable();
},
};
}