Two changes consolidated as the root-cause fix for the recurring dev server hangs: 1) DEV pool max 60 → 30. 60 caused 60 simultaneous query log lines written via process.stderr per page-load on heavy admin pages. stderr write backpressure stalled the Node event loop, manifesting as full HTTP request hangs (TCP accept worked, server never wrote the response). 30 is enough headroom for the clients-page aggregate fanout (≈12 queries) + sidebar widgets without the log-storm. 2) DRIZZLE_LOG opt-in. Drizzle's `logger: true` setting writes every query (full SQL + params) to stderr. With 30 concurrent queries the stderr buffer fills faster than the terminal can drain. Default is now off in dev; set DRIZZLE_LOG=1 explicitly when you need it. Stress-tested with rapid navigation across /dashboard /clients /documents /yachts /companies /interests /berths /website-analytics — all 200, no hangs, no timeouts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
63 lines
2.5 KiB
TypeScript
63 lines
2.5 KiB
TypeScript
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
import postgres from 'postgres';
|
|
|
|
import * as schema from './schema';
|
|
|
|
const connectionString = process.env.DATABASE_URL!;
|
|
|
|
// Connection pool for queries.
|
|
//
|
|
// `statement_timeout` and `idle_in_transaction_session_timeout` are set
|
|
// per-connection via `connection.options` (postgres.js exposes the
|
|
// startup-parameter list there). Without these, a slow query or a
|
|
// transaction left open by a crashed handler holds a connection slot
|
|
// indefinitely and eventually exhausts the pool (max=20). The 30s
|
|
// statement cap is well above expected query latency; tune up if a
|
|
// legitimate report needs longer.
|
|
//
|
|
// `max_lifetime` recycles connections every 30 minutes so any
|
|
// per-connection state drift (prepared statements, GUCs) doesn't
|
|
// accumulate forever.
|
|
// Larger pool in development because Next dev fans out (HMR refetches,
|
|
// multi-widget dashboards, React Query refetch-on-focus) and a single
|
|
// admin clicking around can saturate 20 slots. Production stays at the
|
|
// conservative 20 so we don't hammer postgres in a multi-replica deploy.
|
|
//
|
|
// 60 was too aggressive locally — postgres + the drizzle logger creates
|
|
// massive log volume that backed up node's stderr, blocking the event
|
|
// loop on otherwise-cheap requests. 30 is a middle ground that holds
|
|
// during clients-page fanout without log-storm.
|
|
const POOL_MAX = process.env.NODE_ENV === 'development' ? 30 : 20;
|
|
|
|
const queryClient = postgres(connectionString, {
|
|
max: POOL_MAX,
|
|
idle_timeout: 20,
|
|
connect_timeout: 10,
|
|
max_lifetime: 60 * 30,
|
|
connection: {
|
|
// ms values per postgres.js types; these become Postgres GUC settings
|
|
// applied at session start.
|
|
statement_timeout: 30_000,
|
|
idle_in_transaction_session_timeout: 10_000,
|
|
},
|
|
});
|
|
|
|
// Drizzle query logging is opt-in via DRIZZLE_LOG=1 in development.
|
|
// Was on-by-default in dev but the volume (per-query SQL + parameters
|
|
// for 60+ queries per page load) saturated process.stderr and blocked
|
|
// the Node event loop, causing apparent dev-server "hangs" after
|
|
// repeated navigation. The HTTP request logs from Next still show
|
|
// query routing, which is enough for normal debugging.
|
|
export const db = drizzle(queryClient, {
|
|
schema,
|
|
logger: process.env.DRIZZLE_LOG === '1',
|
|
});
|
|
|
|
/** Close the underlying connection pool. Used by the vitest teardown so
|
|
* the parent process can exit cleanly. */
|
|
export async function closeDb(): Promise<void> {
|
|
await queryClient.end({ timeout: 5 });
|
|
}
|
|
|
|
export type Database = typeof db;
|