fix(db): let the maintenance job run behind a pooler that refuses startup options

This commit is contained in:
Marc-André Bombeck committed 2026-10-08 22:57:30 +02:00
1 parent 0d368170f3
commit 32cb75c1aa
3 files changed
+56 -1

No files matched your search

+3
View File
@@ -195,6 +195,9 @@ in `ignore_startup_parameters`. Either list it there, or set
`DATABASE_SESSION_OPTIONS_DISABLED=1` so the app sends no session
settings; set the timeout and `work_mem` on the database role instead
(`ALTER ROLE healthlog SET work_mem = '16MB'`) if you still want them.
The measurement maintenance job (Admin, System status) then applies its own
settings with `SET` after connecting. Its concurrent index rebuild needs a
session, so run it through a session-mode pool or a direct connection.
### Overriding the per-container pool ceiling
@@ -0,0 +1,30 @@
import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("@/lib/db", () => ({
prisma: {},
sessionOptionsDisabled: () =>
process.env.DATABASE_SESSION_OPTIONS_DISABLED === "1",
}));
import {
MAINTENANCE_SESSION_OPTIONS,
maintenanceSessionStatements,
} from "@/lib/jobs/measurement-maintenance";
describe("maintenance session settings behind a pooler", () => {
afterEach(() => {
delete process.env.DATABASE_SESSION_OPTIONS_DISABLED;
});
it("restates every startup option as one SET statement", () => {
const statements = maintenanceSessionStatements();
const optionCount = MAINTENANCE_SESSION_OPTIONS.match(/-c /g)?.length;
expect(statements).toHaveLength(optionCount ?? 0);
expect(statements).toEqual([
"SET statement_timeout = '0'",
"SET idle_in_transaction_session_timeout = '0'",
"SET maintenance_work_mem = '128MB'",
"SET lock_timeout = '300000'",
]);
});
});
+23 -1
View File
@@ -35,6 +35,7 @@
*/
import { Client } from "pg";
import type { Job } from "pg-boss";
import { sessionOptionsDisabled } from "@/lib/db";
import { withJobLock } from "@/lib/jobs/job-lock";
import { jobDone, jobFailed, type JobOutcome } from "@/lib/jobs/job-outcome";
@@ -68,6 +69,21 @@ export interface MeasurementMaintenancePayload {
export const MAINTENANCE_SESSION_OPTIONS =
"-c statement_timeout=0 -c idle_in_transaction_session_timeout=0 -c maintenance_work_mem=128MB -c lock_timeout=300000";
/**
* The same settings as `SET` statements, for a host behind a connection
* pooler that refuses the `options` startup parameter
* (`DATABASE_SESSION_OPTIONS_DISABLED`). Built from the constant above, whose
* every value is a literal, so nothing user-supplied reaches the SQL.
*/
export function maintenanceSessionStatements(): string[] {
return MAINTENANCE_SESSION_OPTIONS.split(/\s*-c\s+/)
.filter(Boolean)
.map((pair) => {
const [name, value] = pair.trim().split("=");
return `SET ${name} = '${value}'`;
});
}
/** The only shape an index name may have before it is spliced into SQL. */
const INDEX_NAME = /^[a-z0-9_]{1,63}$/;
@@ -310,14 +326,20 @@ export async function handleMeasurementMaintenance(
const guarded = await withJobLock(
MEASUREMENT_MAINTENANCE_QUEUE,
async () => {
const viaStatements = sessionOptionsDisabled();
const client = new Client({
connectionString: process.env.DATABASE_URL,
options: MAINTENANCE_SESSION_OPTIONS,
options: viaStatements ? undefined : MAINTENANCE_SESSION_OPTIONS,
keepAlive: true,
});
client.on("error", () => {});
await client.connect();
try {
if (viaStatements) {
for (const statement of maintenanceSessionStatements()) {
await client.query(statement);
}
}
return await runMeasurementMaintenance(client, payload);
} finally {
await client