mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
@@ -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' },
|
||||
}),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user