/** Background worker: claims jobs, runs handlers, schedules recurring sweeps. Single process, PM2-supervised. */ import "./env"; import { claim, succeed, fail, reapStale, enqueue, prune } from "@/lib/jobs/queue"; import { runJob } from "@/lib/jobs/handlers"; import { closeDb } from "@/db"; import { hostname } from "node:os"; const WORKER = `${hostname()}:${process.pid}`; const POLL = Number(process.env.WORKER_POLL_MS ?? 5000); let stopping = false; process.on("SIGTERM", () => { stopping = true; }); process.on("SIGINT", () => { stopping = true; }); /** Recurring schedule (America/Toronto wall clock): link sweep daily 03:10, freshness 03:40, embed sweep 04:00, backup 04:20, prune weekly. */ function schedule() { const now = new Date(); const tz = new Intl.DateTimeFormat("en-CA", { timeZone: "America/Toronto", hour: "2-digit", minute: "2-digit", hour12: false, weekday: "short" }).formatToParts(now); const hh = tz.find((p) => p.type === "hour")!.value, mm = tz.find((p) => p.type === "minute")!.value, wd = tz.find((p) => p.type === "weekday")!.value; const day = now.toISOString().slice(0, 10); const t = `${hh}:${mm}`; const at = (hm: string, kind: Parameters[0], extra = "") => { if (t === hm) enqueue(kind, {}, { dedupeKey: `${kind}:${day}${extra}`, priority: 3 }).catch(() => {}); }; at("03:10", "link_sweep"); at("03:40", "freshness_sweep"); at("04:00", "embed_sweep"); at("04:20", "backup"); if (wd === "Sun" && t === "05:00") prune().catch(() => {}); } async function loop() { console.log(`[worker] ${WORKER} started, poll ${POLL} ms`); let tick = 0; while (!stopping) { try { if (tick % 12 === 0) { const n = await reapStale(); if (n) console.log(`[worker] requeued ${n} stale job(s)`); schedule(); } const job = await claim(WORKER); if (!job) { await new Promise((r) => setTimeout(r, POLL)); tick++; continue; } const t0 = Date.now(); try { const res = await runJob(job); await succeed(job.id, res ?? null); console.log(`[worker] #${job.id} ${job.kind} ok ${Date.now() - t0} ms`); } catch (e) { const msg = (e as Error).stack ?? String(e); await fail(job, msg); console.error(`[worker] #${job.id} ${job.kind} failed (attempt ${job.attempts}/${job.maxAttempts}): ${(e as Error).message}`); } } catch (e) { console.error("[worker] loop error", e); await new Promise((r) => setTimeout(r, POLL)); } } await closeDb(); console.log("[worker] stopped"); } loop();