Support zero-config gateway startup
This commit is contained in:
parent
fbd175918f
commit
4b17d6eab4
@ -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[],
|
||||||
|
|||||||
@ -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'] ??
|
||||||
|
|||||||
@ -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'),
|
||||||
|
|||||||
@ -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,6 +206,7 @@ async function main() {
|
|||||||
const host = process.env['HOST'] ?? 'localhost';
|
const host = process.env['HOST'] ?? 'localhost';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (isDatabaseConfigured()) {
|
||||||
try {
|
try {
|
||||||
await runMigrations();
|
await runMigrations();
|
||||||
} catch (migErr) {
|
} catch (migErr) {
|
||||||
@ -217,6 +217,9 @@ async function main() {
|
|||||||
} catch (pgErr) {
|
} catch (pgErr) {
|
||||||
logger.warn({ pgErr }, 'PgBoss init failed - continuing without queue');
|
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 {
|
||||||
const preset = await loadWorkspacePreset();
|
const preset = await loadWorkspacePreset();
|
||||||
@ -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 {
|
||||||
|
if (isDatabaseConfigured()) {
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
scheduleAdaptiveLearner(pool as never);
|
scheduleAdaptiveLearner(pool as never);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn({ err }, 'Adaptive learner scheduling failed');
|
logger.warn({ err }, 'Adaptive learner scheduling failed');
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user