import { Worker, type Job } from "bullmq"; import { spawn, type ChildProcess } from "child_process"; import { existsSync } from "fs"; import { createBullMQConnection } from "./redis.js"; import { handleBuildValhalla } from "./jobs/build-valhalla.js"; const VALHALLA_CONFIG = process.env.VALHALLA_CONFIG ?? "/data/valhalla/valhalla.json"; console.log("[valhalla-worker] Starting Transportationer Valhalla worker…"); // ─── Valhalla service process manager ───────────────────────────────────────── // The valhalla_service HTTP server runs as a child process alongside this // BullMQ worker. When a build-valhalla job arrives, we stop the server, rebuild // the routing tiles (using the Valhalla tools installed in this container), // then restart the server. let valhallaProc: ChildProcess | null = null; function startValhallaService(): void { if (!existsSync(VALHALLA_CONFIG)) { console.log("[valhalla-worker] No config yet — will start after first tile build"); return; } console.log("[valhalla-worker] Starting valhalla_service…"); // valhalla_service [concurrency] — positional arg, not -c flag valhallaProc = spawn("valhalla_service", [VALHALLA_CONFIG], { stdio: "inherit", }); valhallaProc.on("exit", (code, signal) => { console.log(`[valhalla-worker] valhalla_service exited (code=${code}, signal=${signal})`); valhallaProc = null; }); } function stopValhallaService(): Promise { return new Promise((resolve) => { if (!valhallaProc) { resolve(); return; } const proc = valhallaProc; proc.once("exit", () => resolve()); proc.kill("SIGTERM"); // Force kill after 10 s if it doesn't exit cleanly setTimeout(() => { if (valhallaProc === proc) proc.kill("SIGKILL"); }, 10_000); }); } // ─── BullMQ worker ──────────────────────────────────────────────────────────── const worker = new Worker( "valhalla", async (job: Job) => { console.log(`[valhalla-worker] Processing job ${job.id} type=${job.data.type} city=${job.data.citySlug ?? "(rebuild)"}`); // Valhalla keeps serving old tiles while the new tiles are being built. // restartService is called from inside handleBuildValhalla only after the // tile build completes — the service is only down for the few seconds it // takes to restart, and compute-routing jobs retry transparently across that // window via fetchMatrix's built-in retry logic. async function restartService(): Promise { await stopValhallaService(); startValhallaService(); } await handleBuildValhalla(job as any, restartService); }, { connection: createBullMQConnection(), concurrency: 1, lockDuration: 1_800_000, // 30 min — large-region tile builds can be very slow lockRenewTime: 60_000, }, ); worker.on("completed", (job) => { console.log(`[valhalla-worker] ✓ Job ${job.id} (${job.data.type}) completed`); }); worker.on("failed", (job, err) => { console.error(`[valhalla-worker] ✗ Job ${job?.id} (${job?.data?.type}) failed:`, err.message); }); worker.on("active", (job) => { const city = job.data.citySlug ?? job.data.removeSlugs?.join(",") ?? "rebuild"; console.log(`[valhalla-worker] → Job ${job.id} (${job.data.type}) started city=${city}`); }); worker.on("error", (err) => { console.error("[valhalla-worker] Worker error:", err.message); }); const shutdown = async () => { console.log("[valhalla-worker] Shutting down gracefully…"); await worker.close(); await stopValhallaService(); process.exit(0); }; process.on("SIGTERM", shutdown); process.on("SIGINT", shutdown); // Start serving if tiles already exist from a previous run startValhallaService(); console.log("[valhalla-worker] Ready — waiting for build-valhalla jobs on 'valhalla' queue");