fifteen/worker/src/valhalla-main.ts

111 lines
4.2 KiB
TypeScript

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";
import { handleDownloadGtfsDe } from "./jobs/download-gtfs-de.js";
const VALHALLA_CONFIG = process.env.VALHALLA_CONFIG ?? "/data/valhalla/valhalla.json";
const VALHALLA_QUEUE_NAME = process.env.VALHALLA_QUEUE_NAME ?? "valhalla";
console.log(`[valhalla-worker] Starting Transportationer Valhalla worker (queue=${VALHALLA_QUEUE_NAME})…`);
// ─── 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 <config_file> [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<void> {
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_QUEUE_NAME,
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<void> {
await stopValhallaService();
startValhallaService();
}
if (job.data.type === "download-gtfs-de") {
await handleDownloadGtfsDe(job as any);
return;
}
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 jobs on '${VALHALLA_QUEUE_NAME}' queue`);