perf(db): per-role Postgres connection-pool profiles (#5232)

* perf(db): drive Postgres pool size + application_name from per-role profiles

Replace ad-hoc DB_APP_NAME sizing with a per-role profile map keyed by
SIM_DB_ROLE (web/trigger/realtime), defaulting to web. Trigger machines
open a small pool instead of 15 to avoid PgBouncer connection exhaustion.
Also size realtime's separate socketDb pool down to 10.

* fix(db): throw on invalid SIM_DB_ROLE instead of silently using web pools

* fix(db): use Object.hasOwn for SIM_DB_ROLE validation to avoid prototype keys
This commit is contained in:
Theodore Li
2026-06-27 15:15:05 -04:00
committed by GitHub
parent 6264cba52f
commit 845a6276d9
4 changed files with 37 additions and 6 deletions
+2 -1
View File
@@ -32,13 +32,14 @@ import { env } from '@/env'
const logger = createLogger('SocketDatabase')
const connectionString = env.DATABASE_URL
// Realtime process footprint = this socketDb pool + the shared @sim/db pool.
const socketDb = drizzle(
instrumentPoolClient(
postgres(connectionString, {
prepare: false,
idle_timeout: 10,
connect_timeout: 20,
max: 15,
max: 10,
onnotice: () => {},
connection: { application_name: process.env.DB_APP_NAME ?? 'sim-realtime' },
}),
+1
View File
@@ -13,6 +13,7 @@ const EnvSchema = z.object({
NEXT_PUBLIC_APP_URL: z.string().url(),
ALLOWED_ORIGINS: z.string().optional(),
PORT: z.coerce.number().int().positive().default(3002),
SIM_DB_ROLE: z.enum(['web', 'trigger', 'realtime']).optional(),
DISABLE_AUTH: z
.string()
.optional()
+1
View File
@@ -21,6 +21,7 @@ export const env = createEnv({
DATABASE_URL: z.string().url(), // Primary database connection string
DATABASE_REPLICA_URL: z.string().url().optional(), // Read-replica connection string; opt-in reads fall back to the primary when unset
DB_APP_NAME: z.string().optional(), // Postgres application_name for query attribution (sim-app/sim-trigger/sim-realtime)
SIM_DB_ROLE: z.enum(['web', 'trigger', 'realtime']).optional(), // Per-process pool profile selector (read directly by @sim/db)
BETTER_AUTH_URL: z.string().url(), // Base URL for Better Auth service
BETTER_AUTH_SECRET: z.string().min(32), // Secret key for Better Auth JWT signing
DISABLE_REGISTRATION: z.boolean().optional(), // Flag to disable new user registration
+33 -5
View File
@@ -8,16 +8,38 @@ if (!connectionString) {
throw new Error('Missing DATABASE_URL environment variable')
}
/**
* Per-role pool profiles. Starting numbers — validate against real per-role
* process counts (PgBouncer transaction mode, max_connections=200).
*/
export const DB_POOL_PROFILES = {
web: { primaryMax: 10, replicaMax: 4, appName: 'sim-app' },
// 5, not 3 — one run can need 3+ simultaneous connections (parallel queries +
// overlapping logging writes); 3 risks intra-run deadlock.
trigger: { primaryMax: 5, replicaMax: 2, appName: 'sim-trigger' },
realtime: { primaryMax: 5, replicaMax: 3, appName: 'sim-realtime' },
} as const
type DbRole = keyof typeof DB_POOL_PROFILES
const roleEnv = process.env.SIM_DB_ROLE?.trim()
if (roleEnv && !Object.hasOwn(DB_POOL_PROFILES, roleEnv)) {
throw new Error(
`Invalid SIM_DB_ROLE '${roleEnv}' — expected one of ${Object.keys(DB_POOL_PROFILES).join(', ')} (or unset for web)`
)
}
const profile = DB_POOL_PROFILES[(roleEnv as DbRole) || 'web']
const poolOptions = {
prepare: false,
idle_timeout: 20,
connect_timeout: 30,
onnotice: () => {},
connection: { application_name: process.env.DB_APP_NAME ?? 'sim-app' },
connection: { application_name: process.env.DB_APP_NAME ?? profile.appName },
}
const postgresClient = instrumentPoolClient(
postgres(connectionString, { ...poolOptions, max: 15 }),
postgres(connectionString, { ...poolOptions, max: profile.primaryMax }),
'db'
)
@@ -37,7 +59,13 @@ if (replicaUrl && !/^postgres(ql)?:\/\//.test(replicaUrl)) {
}
export const dbReplica: typeof db = replicaUrl
? drizzle(instrumentPoolClient(postgres(replicaUrl, { ...poolOptions, max: 10 }), 'dbReplica'), {
schema,
})
? drizzle(
instrumentPoolClient(
postgres(replicaUrl, { ...poolOptions, max: profile.replicaMax }),
'dbReplica'
),
{
schema,
}
)
: db