fix(planner): fail-loud DATABASE_URL + dedicated /health pool

Mirrors #422 (fail-loud DB URL) and #430 (health-check pool reuse) for
the planner app, which still had:

- two \`process.env.DATABASE_URL ?? \"postgres://trails:trails@localhost:5432/trails\"\`
  call sites that would silently boot a misconfigured prod against
  localhost
- a /health handler that opened a fresh postgres client + connection on
  every probe (no pool, per-call TCP/TLS handshake)

Both now route through @trails-cool/db's \`getDatabaseUrl()\` (refuses to
start in production if unset / matches the dev default; E2E=true is the
opt-out) and a module-level singleton client (max: 2, idle_timeout: 30).

Full repo: pnpm typecheck, pnpm lint, pnpm test all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-05-25 22:49:16 +02:00
parent ca2388e41c
commit 4f902accd7
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9

View file

@ -10,7 +10,8 @@ import { join, extname, resolve } from "node:path";
import { setupYjsWebSocket } from "./app/lib/yjs-server.ts";
import { createBoss, startWorker } from "@trails-cool/jobs";
import { expireSessionsJob } from "./app/jobs/expire-sessions.ts";
import postgres from "postgres";
import { getDatabaseUrl } from "@trails-cool/db";
import postgres, { type Sql } from "postgres";
// See apps/journal/server.ts for the SENTRY_DSN / SENTRY_DISABLED contract.
const FLAGSHIP_PLANNER_SENTRY_DSN =
@ -75,17 +76,27 @@ async function handleMetrics(_req: IncomingMessage, res: ServerResponse): Promis
const version = process.env.SENTRY_RELEASE ?? "dev";
// Module-level singleton dedicated to /health (mirrors PR #430 for the
// journal). Previously a fresh client + connection was opened on every
// probe; under monitoring cadence that's a fresh handshake every few
// seconds. Kept separate from any future main-app pool so a starvation
// event on the app pool doesn't fail the liveness probe.
let healthClient: Sql | null = null;
function getHealthClient(): Sql {
if (!healthClient) {
healthClient = postgres(getDatabaseUrl(), { max: 2, idle_timeout: 30 });
}
return healthClient;
}
async function handleHealth(_req: IncomingMessage, res: ServerResponse): Promise<void> {
const client = postgres(process.env.DATABASE_URL ?? "postgres://trails:trails@localhost:5432/trails", { max: 1 });
try {
await client`SELECT 1`;
await getHealthClient()`SELECT 1`;
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok", version, db: "connected" }));
} catch {
res.writeHead(503, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "degraded", version, db: "unreachable" }));
} finally {
await client.end();
}
}
@ -127,7 +138,7 @@ server.listen(port, async () => {
logger.info({ port }, "Planner server listening");
logger.info({ port, path: "/sync/:sessionId" }, "Yjs WebSocket available");
const boss = createBoss(process.env.DATABASE_URL ?? "postgres://trails:trails@localhost:5432/trails");
const boss = createBoss(getDatabaseUrl());
await startWorker(boss, [expireSessionsJob]);
logger.info("Background job worker started");
});