import { config } from "dotenv"; import { existsSync } from "node:fs"; import { join } from "node:path"; for (const f of [".env.local", ".env"]) if (existsSync(join(process.cwd(), f))) config({ path: join(process.cwd(), f), quiet: true } as never); const { sql } = await import("@/db"); const { claimJob, finishJob, enqueue } = await import("@/lib/jobs/queue"); const { handlers } = await import("@/lib/jobs/handlers"); const POLL = Number(process.env.WORKER_POLL_MS ?? 5000); const log = (level: string, msg: string, extra: Record = {}) => console.log(JSON.stringify({ t: new Date().toISOString(), level, component: "worker", msg, ...extra })); let stopping = false; process.on("SIGTERM", () => { stopping = true; log("info", "SIGTERM received, finishing current job"); }); process.on("SIGINT", () => { stopping = true; }); async function schedule() { // Local time of the node (America/Toronto on the cluster). Runs once per day each. const now = new Date(); const hhmm = now.getHours() * 100 + now.getMinutes(); const today = now.toISOString().slice(0, 10); const mark = async (key: string) => { await sql`INSERT INTO settings (key, value) VALUES (${key}, ${sql.json({ day: today } as never)}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`; }; const done = async (key: string) => { const [r] = await sql`SELECT value FROM settings WHERE key = ${key}`; return r?.value?.day === today; }; if (hhmm >= 10 && !(await done("private.worker.daily_stats"))) { await enqueue("daily_stats", {}, { dedupe: true }); await mark("private.worker.daily_stats"); } if (hhmm >= 440 && !(await done("private.worker.backup"))) { await enqueue("backup", {}, { dedupe: true }); await mark("private.worker.backup"); } if (hhmm >= 300 && !(await done("private.worker.relations"))) { await enqueue("relations", {}, { dedupe: true }); await mark("private.worker.relations"); } } log("info", "worker started", { poll_ms: POLL }); while (!stopping) { try { await schedule(); const job = await claimJob(); if (!job) { await new Promise((r) => setTimeout(r, POLL)); continue; } const t0 = Date.now(); log("info", "job start", { id: job.id, kind: job.kind, attempt: job.attempts }); try { const result = await handlers[job.kind]?.(job.payload); await finishJob(job.id, true); log("info", "job done", { id: job.id, kind: job.kind, ms: Date.now() - t0, result }); } catch (e) { const msg = (e as Error).message ?? String(e); await finishJob(job.id, false, msg); log("error", "job failed", { id: job.id, kind: job.kind, error: msg.slice(0, 500) }); } } catch (e) { log("error", "loop error", { error: (e as Error).message }); await new Promise((r) => setTimeout(r, POLL)); } } await sql.end(); log("info", "worker stopped");