From 4b17d6eab48b42efd2e690fe8f27f72ef1721a03 Mon Sep 17 00:00:00 2001 From: Rene Fichtmueller Date: Fri, 17 Jul 2026 22:55:00 +0200 Subject: [PATCH] Support zero-config gateway startup --- packages/gateway/src/db/client.ts | 20 ++++++++++++ packages/gateway/src/queue/pg-boss-client.ts | 5 +++ packages/gateway/src/routes/health.ts | 15 ++++++--- packages/gateway/src/server.ts | 33 +++++++++++--------- 4 files changed, 54 insertions(+), 19 deletions(-) diff --git a/packages/gateway/src/db/client.ts b/packages/gateway/src/db/client.ts index 809f7a3..03a5a77 100644 --- a/packages/gateway/src/db/client.ts +++ b/packages/gateway/src/db/client.ts @@ -5,6 +5,16 @@ const { Pool } = pg; let pool: pg.Pool | null = null; +export function isDatabaseConfigured(): boolean { + return Boolean( + process.env['DATABASE_URL'] + || process.env['DB_HOST'] + || process.env['DB_NAME'] + || process.env['DB_USER'] + || process.env['DB_PASSWORD'], + ); +} + /** * Build pool config from DATABASE_URL (preferred) or individual DB_* env vars. * DATABASE_URL should be supplied by the runtime environment or a secret manager. @@ -32,6 +42,9 @@ function buildPoolConfig(): pg.PoolConfig { } export function getPool(): pg.Pool { + if (!isDatabaseConfigured()) { + throw new Error('Database is not configured'); + } if (!pool) { pool = new Pool(buildPoolConfig()); @@ -42,6 +55,13 @@ export function getPool(): pg.Pool { return pool; } +export async function closePool(): Promise { + if (pool) { + await pool.end(); + pool = null; + } +} + export async function query( sql: string, params?: unknown[], diff --git a/packages/gateway/src/queue/pg-boss-client.ts b/packages/gateway/src/queue/pg-boss-client.ts index 63b2270..a41dadf 100644 --- a/packages/gateway/src/queue/pg-boss-client.ts +++ b/packages/gateway/src/queue/pg-boss-client.ts @@ -1,6 +1,7 @@ import PgBoss from 'pg-boss'; import { logger } from '../observability/logger.js'; import { costStream } from '../observability/cost-stream.js'; +import { isDatabaseConfigured } from '../db/client.js'; const QUEUE_NAME = 'llm-batch'; const CONCURRENCY = 4; @@ -32,6 +33,10 @@ interface TaskResult { export async function initPgBoss(): Promise { if (boss) return; + if (!isDatabaseConfigured()) { + logger.info('DATABASE_URL/DB_* not configured - queue disabled'); + return; + } const connectionString = process.env['DATABASE_URL'] ?? diff --git a/packages/gateway/src/routes/health.ts b/packages/gateway/src/routes/health.ts index e7c546e..10cdcd2 100644 --- a/packages/gateway/src/routes/health.ts +++ b/packages/gateway/src/routes/health.ts @@ -4,7 +4,7 @@ import { dirname, join } from 'path'; import { readFileSync, existsSync } from 'fs'; import { getOllamaBaseUrl } from '../pipeline/router.js'; import { getAllBreakerStates } from '../circuit-breaker/ollama-breaker.js'; -import { query } from '../db/client.js'; +import { isDatabaseConfigured, query } from '../db/client.js'; import { getPgBoss } from '../queue/pg-boss-client.js'; import { logger } from '../observability/logger.js'; @@ -13,8 +13,8 @@ interface HealthStatus { timestamp: string; checks: { ollama: { status: 'ok' | 'down'; latency_ms?: number; error?: string }; - database: { status: 'ok' | 'down'; error?: string }; - queue: { status: 'ok' | 'down' | 'unknown'; depth?: number; error?: string }; + database: { status: 'ok' | 'down' | 'disabled'; error?: string }; + queue: { status: 'ok' | 'down' | 'unknown' | 'disabled'; depth?: number; error?: string }; review_queue: { unreviewed_count: number }; circuit_breakers: Record; }; @@ -36,7 +36,10 @@ async function checkOllama(baseUrl: string): Promise<{ status: 'ok' | 'down'; la } } -async function checkDatabase(): Promise<{ status: 'ok' | 'down'; error?: string }> { +async function checkDatabase(): Promise<{ status: 'ok' | 'down' | 'disabled'; error?: string }> { + if (!isDatabaseConfigured()) { + return { status: 'disabled' }; + } try { await withTimeout(query('SELECT 1'), 2500, 'database check timed out'); return { status: 'ok' }; @@ -59,7 +62,8 @@ async function withTimeout(promise: Promise, timeoutMs: number, message: s } } -async function checkQueue(): Promise<{ status: 'ok' | 'down' | 'unknown'; depth?: number; error?: string }> { +async function checkQueue(): Promise<{ status: 'ok' | 'down' | 'unknown' | 'disabled'; depth?: number; error?: string }> { + if (!isDatabaseConfigured()) return { status: 'disabled' }; const boss = getPgBoss(); if (!boss) return { status: 'unknown' }; @@ -79,6 +83,7 @@ async function checkQueue(): Promise<{ status: 'ok' | 'down' | 'unknown'; depth? } async function getReviewQueueCount(): Promise { + if (!isDatabaseConfigured()) return 0; try { const result = await withTimeout( query<{ count: string }>('SELECT COUNT(*) as count FROM review_queue WHERE decision IS NULL'), diff --git a/packages/gateway/src/server.ts b/packages/gateway/src/server.ts index 856b2e0..4280fd4 100644 --- a/packages/gateway/src/server.ts +++ b/packages/gateway/src/server.ts @@ -16,7 +16,7 @@ import { learningInsightsRoute } from './routes/learning-insights.js'; import { staticRoute } from './routes/static.js'; import tenantAuth from './security/tenant-auth.js'; import { internalRoute } from './routes/internal.js'; -import { getPool } from './db/client.js'; +import { closePool, getPool, isDatabaseConfigured } from './db/client.js'; import { runMigrations } from './db/migrate.js'; import { initPgBoss } from './queue/pg-boss-client.js'; import { logger } from './observability/logger.js'; @@ -190,8 +190,7 @@ async function main() { logger.info({ signal }, 'Shutdown signal received'); try { await server.close(); - const pool = getPool(); - await pool.end(); + await closePool(); logger.info('Server and DB connections closed'); process.exit(0); } catch (err) { @@ -207,15 +206,19 @@ async function main() { const host = process.env['HOST'] ?? 'localhost'; try { - try { - await runMigrations(); - } catch (migErr) { - logger.warn({ migErr }, 'Migration failed - starting server without DB'); - } - try { - await initPgBoss(); - } catch (pgErr) { - logger.warn({ pgErr }, 'PgBoss init failed - continuing without queue'); + if (isDatabaseConfigured()) { + try { + await runMigrations(); + } catch (migErr) { + logger.warn({ migErr }, 'Migration failed - starting server without DB'); + } + try { + await initPgBoss(); + } catch (pgErr) { + logger.warn({ pgErr }, 'PgBoss init failed - continuing without queue'); + } + } else { + logger.info('DATABASE_URL/DB_* not configured - persistence and queue disabled'); } // Workspace preset (apply env defaults from workspace.yaml if present) try { @@ -251,8 +254,10 @@ async function main() { // Adaptive routing learner (opt-in via ADAPTIVE_ROUTING_ENABLED=1) try { - const pool = getPool(); - scheduleAdaptiveLearner(pool as never); + if (isDatabaseConfigured()) { + const pool = getPool(); + scheduleAdaptiveLearner(pool as never); + } } catch (err) { logger.warn({ err }, 'Adaptive learner scheduling failed'); }