Support zero-config gateway startup

This commit is contained in:
Rene Fichtmueller 2026-07-17 22:55:00 +02:00
parent fbd175918f
commit 4b17d6eab4
4 changed files with 54 additions and 19 deletions

View File

@ -5,6 +5,16 @@ const { Pool } = pg;
let pool: pg.Pool | null = null; 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. * 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. * 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 { export function getPool(): pg.Pool {
if (!isDatabaseConfigured()) {
throw new Error('Database is not configured');
}
if (!pool) { if (!pool) {
pool = new Pool(buildPoolConfig()); pool = new Pool(buildPoolConfig());
@ -42,6 +55,13 @@ export function getPool(): pg.Pool {
return pool; return pool;
} }
export async function closePool(): Promise<void> {
if (pool) {
await pool.end();
pool = null;
}
}
export async function query<T extends pg.QueryResultRow = pg.QueryResultRow>( export async function query<T extends pg.QueryResultRow = pg.QueryResultRow>(
sql: string, sql: string,
params?: unknown[], params?: unknown[],

View File

@ -1,6 +1,7 @@
import PgBoss from 'pg-boss'; import PgBoss from 'pg-boss';
import { logger } from '../observability/logger.js'; import { logger } from '../observability/logger.js';
import { costStream } from '../observability/cost-stream.js'; import { costStream } from '../observability/cost-stream.js';
import { isDatabaseConfigured } from '../db/client.js';
const QUEUE_NAME = 'llm-batch'; const QUEUE_NAME = 'llm-batch';
const CONCURRENCY = 4; const CONCURRENCY = 4;
@ -32,6 +33,10 @@ interface TaskResult {
export async function initPgBoss(): Promise<void> { export async function initPgBoss(): Promise<void> {
if (boss) return; if (boss) return;
if (!isDatabaseConfigured()) {
logger.info('DATABASE_URL/DB_* not configured - queue disabled');
return;
}
const connectionString = const connectionString =
process.env['DATABASE_URL'] ?? process.env['DATABASE_URL'] ??

View File

@ -4,7 +4,7 @@ import { dirname, join } from 'path';
import { readFileSync, existsSync } from 'fs'; import { readFileSync, existsSync } from 'fs';
import { getOllamaBaseUrl } from '../pipeline/router.js'; import { getOllamaBaseUrl } from '../pipeline/router.js';
import { getAllBreakerStates } from '../circuit-breaker/ollama-breaker.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 { getPgBoss } from '../queue/pg-boss-client.js';
import { logger } from '../observability/logger.js'; import { logger } from '../observability/logger.js';
@ -13,8 +13,8 @@ interface HealthStatus {
timestamp: string; timestamp: string;
checks: { checks: {
ollama: { status: 'ok' | 'down'; latency_ms?: number; error?: string }; ollama: { status: 'ok' | 'down'; latency_ms?: number; error?: string };
database: { status: 'ok' | 'down'; error?: string }; database: { status: 'ok' | 'down' | 'disabled'; error?: string };
queue: { status: 'ok' | 'down' | 'unknown'; depth?: number; error?: string }; queue: { status: 'ok' | 'down' | 'unknown' | 'disabled'; depth?: number; error?: string };
review_queue: { unreviewed_count: number }; review_queue: { unreviewed_count: number };
circuit_breakers: Record<string, 'closed' | 'open' | 'half-open'>; circuit_breakers: Record<string, 'closed' | 'open' | 'half-open'>;
}; };
@ -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 { try {
await withTimeout(query('SELECT 1'), 2500, 'database check timed out'); await withTimeout(query('SELECT 1'), 2500, 'database check timed out');
return { status: 'ok' }; return { status: 'ok' };
@ -59,7 +62,8 @@ async function withTimeout<T>(promise: Promise<T>, 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(); const boss = getPgBoss();
if (!boss) return { status: 'unknown' }; if (!boss) return { status: 'unknown' };
@ -79,6 +83,7 @@ async function checkQueue(): Promise<{ status: 'ok' | 'down' | 'unknown'; depth?
} }
async function getReviewQueueCount(): Promise<number> { async function getReviewQueueCount(): Promise<number> {
if (!isDatabaseConfigured()) return 0;
try { try {
const result = await withTimeout( const result = await withTimeout(
query<{ count: string }>('SELECT COUNT(*) as count FROM review_queue WHERE decision IS NULL'), query<{ count: string }>('SELECT COUNT(*) as count FROM review_queue WHERE decision IS NULL'),

View File

@ -16,7 +16,7 @@ import { learningInsightsRoute } from './routes/learning-insights.js';
import { staticRoute } from './routes/static.js'; import { staticRoute } from './routes/static.js';
import tenantAuth from './security/tenant-auth.js'; import tenantAuth from './security/tenant-auth.js';
import { internalRoute } from './routes/internal.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 { runMigrations } from './db/migrate.js';
import { initPgBoss } from './queue/pg-boss-client.js'; import { initPgBoss } from './queue/pg-boss-client.js';
import { logger } from './observability/logger.js'; import { logger } from './observability/logger.js';
@ -190,8 +190,7 @@ async function main() {
logger.info({ signal }, 'Shutdown signal received'); logger.info({ signal }, 'Shutdown signal received');
try { try {
await server.close(); await server.close();
const pool = getPool(); await closePool();
await pool.end();
logger.info('Server and DB connections closed'); logger.info('Server and DB connections closed');
process.exit(0); process.exit(0);
} catch (err) { } catch (err) {
@ -207,15 +206,19 @@ async function main() {
const host = process.env['HOST'] ?? 'localhost'; const host = process.env['HOST'] ?? 'localhost';
try { try {
try { if (isDatabaseConfigured()) {
await runMigrations(); try {
} catch (migErr) { await runMigrations();
logger.warn({ migErr }, 'Migration failed - starting server without DB'); } catch (migErr) {
} logger.warn({ migErr }, 'Migration failed - starting server without DB');
try { }
await initPgBoss(); try {
} catch (pgErr) { await initPgBoss();
logger.warn({ pgErr }, 'PgBoss init failed - continuing without queue'); } 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) // Workspace preset (apply env defaults from workspace.yaml if present)
try { try {
@ -251,8 +254,10 @@ async function main() {
// Adaptive routing learner (opt-in via ADAPTIVE_ROUTING_ENABLED=1) // Adaptive routing learner (opt-in via ADAPTIVE_ROUTING_ENABLED=1)
try { try {
const pool = getPool(); if (isDatabaseConfigured()) {
scheduleAdaptiveLearner(pool as never); const pool = getPool();
scheduleAdaptiveLearner(pool as never);
}
} catch (err) { } catch (err) {
logger.warn({ err }, 'Adaptive learner scheduling failed'); logger.warn({ err }, 'Adaptive learner scheduling failed');
} }