import { resolve } from "node:path"; import { getEnv, syncTemplates } from "@autoscholar/core"; import { createLogger } from "@autoscholar/observability"; import { getStorage } from "@autoscholar/storage"; import { closeDb, getDb, runMigrationsIfRequested } from "./boot.js"; import { schedulerTick } from "./tick.js"; const log = createLogger("scheduler"); const env = getEnv(); const db = getDb(); const storage = getStorage(env.ARTIFACT_ROOT); let stopping = false; let inTick = false; async function loop() { await runMigrationsIfRequested(log); try { const synced = await syncTemplates( db, resolve(process.env.RESEARCH_TEMPLATES_DIR ?? "research/templates"), ); if (synced.length) log.info({ synced }, "research templates synced"); } catch (e) { log.warn({ err: (e as Error).message }, "template sync skipped"); } log.info( { tickMs: env.SCHEDULER_TICK_MS, leaseTtl: env.LEASE_TTL_SECONDS, publishing: env.PUBLISHING_ENABLED, dispatch: env.DISPATCH_ENABLED, }, "scheduler started", ); while (!stopping) { inTick = true; try { const r = await schedulerTick(db, storage, log); if ( r.expiredLeases.length || r.requeued || r.evaluated.length || r.published.length || r.timedOut.length || r.errors.length ) log.info(r, "tick"); } catch (e) { log.error({ err: (e as Error).message }, "tick crashed"); } inTick = false; await new Promise((r) => setTimeout(r, env.SCHEDULER_TICK_MS)); } } async function shutdown(sig: string) { log.info({ sig }, "shutting down"); stopping = true; const start = Date.now(); while (inTick && Date.now() - start < 20_000) await new Promise((r) => setTimeout(r, 100)); await closeDb(); process.exit(0); } process.on("SIGTERM", () => void shutdown("SIGTERM")); process.on("SIGINT", () => void shutdown("SIGINT")); loop().catch((e) => { log.fatal({ err: (e as Error).message }, "scheduler died"); process.exit(1); });