Skip to content

Commit 845a627

Browse files
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
1 parent 6264cba commit 845a627

4 files changed

Lines changed: 37 additions & 6 deletions

File tree

‎apps/realtime/src/database/operations.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@ import { env } from '@/env'
3232
const logger = createLogger('SocketDatabase')
3333

3434
const connectionString = env.DATABASE_URL
35+
// Realtime process footprint = this socketDb pool + the shared @sim/db pool.
3536
const socketDb = drizzle(
3637
instrumentPoolClient(
3738
postgres(connectionString, {
3839
prepare: false,
3940
idle_timeout: 10,
4041
connect_timeout: 20,
41-
max: 15,
42+
max: 10,
4243
onnotice: () => {},
4344
connection: { application_name: process.env.DB_APP_NAME ?? 'sim-realtime' },
4445
}),

‎apps/realtime/src/env.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const EnvSchema = z.object({
1313
NEXT_PUBLIC_APP_URL: z.string().url(),
1414
ALLOWED_ORIGINS: z.string().optional(),
1515
PORT: z.coerce.number().int().positive().default(3002),
16+
SIM_DB_ROLE: z.enum(['web', 'trigger', 'realtime']).optional(),
1617
DISABLE_AUTH: z
1718
.string()
1819
.optional()

‎apps/sim/lib/core/config/env.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const env = createEnv({
2121
DATABASE_URL: z.string().url(), // Primary database connection string
2222
DATABASE_REPLICA_URL: z.string().url().optional(), // Read-replica connection string; opt-in reads fall back to the primary when unset
2323
DB_APP_NAME: z.string().optional(), // Postgres application_name for query attribution (sim-app/sim-trigger/sim-realtime)
24+
SIM_DB_ROLE: z.enum(['web', 'trigger', 'realtime']).optional(), // Per-process pool profile selector (read directly by @sim/db)
2425
BETTER_AUTH_URL: z.string().url(), // Base URL for Better Auth service
2526
BETTER_AUTH_SECRET: z.string().min(32), // Secret key for Better Auth JWT signing
2627
DISABLE_REGISTRATION: z.boolean().optional(), // Flag to disable new user registration

‎packages/db/db.ts‎

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,38 @@ if (!connectionString) {
88
throw new Error('Missing DATABASE_URL environment variable')
99
}
1010

11+
/**
12+
* Per-role pool profiles. Starting numbers — validate against real per-role
13+
* process counts (PgBouncer transaction mode, max_connections=200).
14+
*/
15+
export const DB_POOL_PROFILES = {
16+
web: { primaryMax: 10, replicaMax: 4, appName: 'sim-app' },
17+
// 5, not 3 — one run can need 3+ simultaneous connections (parallel queries +
18+
// overlapping logging writes); 3 risks intra-run deadlock.
19+
trigger: { primaryMax: 5, replicaMax: 2, appName: 'sim-trigger' },
20+
realtime: { primaryMax: 5, replicaMax: 3, appName: 'sim-realtime' },
21+
} as const
22+
23+
type DbRole = keyof typeof DB_POOL_PROFILES
24+
25+
const roleEnv = process.env.SIM_DB_ROLE?.trim()
26+
if (roleEnv && !Object.hasOwn(DB_POOL_PROFILES, roleEnv)) {
27+
throw new Error(
28+
`Invalid SIM_DB_ROLE '${roleEnv}' — expected one of ${Object.keys(DB_POOL_PROFILES).join(', ')} (or unset for web)`
29+
)
30+
}
31+
const profile = DB_POOL_PROFILES[(roleEnv as DbRole) || 'web']
32+
1133
const poolOptions = {
1234
prepare: false,
1335
idle_timeout: 20,
1436
connect_timeout: 30,
1537
onnotice: () => {},
16-
connection: { application_name: process.env.DB_APP_NAME ?? 'sim-app' },
38+
connection: { application_name: process.env.DB_APP_NAME ?? profile.appName },
1739
}
1840

1941
const postgresClient = instrumentPoolClient(
20-
postgres(connectionString, { ...poolOptions, max: 15 }),
42+
postgres(connectionString, { ...poolOptions, max: profile.primaryMax }),
2143
'db'
2244
)
2345

@@ -37,7 +59,13 @@ if (replicaUrl && !/^postgres(ql)?:\/\//.test(replicaUrl)) {
3759
}
3860

3961
export const dbReplica: typeof db = replicaUrl
40-
? drizzle(instrumentPoolClient(postgres(replicaUrl, { ...poolOptions, max: 10 }), 'dbReplica'), {
41-
schema,
42-
})
62+
? drizzle(
63+
instrumentPoolClient(
64+
postgres(replicaUrl, { ...poolOptions, max: profile.replicaMax }),
65+
'dbReplica'
66+
),
67+
{
68+
schema,
69+
}
70+
)
4371
: db

0 commit comments

Comments
 (0)