add health server and metrics to db queue worker

This commit is contained in:
Alex Holliday committed 2026-06-25 10:36:31 -07:00
1 parent fd536541d7
commit b3d9c5b94c
3 files changed
+91 -5

No files matched your search

+15 -5
View File
@@ -36,6 +36,10 @@ const CONCURRENCY: Record<JobType, number> = {
export class DBQueueWorker extends JobScheduler implements IQueueWorker {
static SERVICE_NAME = SERVICE_NAME;
private initComplete: boolean = false;
private lastTickAt: number | null = null;
private inFlight: Record<JobType, number> = {
check: 0,
"geo-check": 0,
@@ -46,6 +50,7 @@ export class DBQueueWorker extends JobScheduler implements IQueueWorker {
constructor(
private logger: ILogger,
private isDbConnected: () => boolean,
jobsRepository: IJobsRepository, // ← no modifier: forwarded to super
private monitorsRepository: IMonitorsRepository,
private checksRepository: IChecksRepository,
@@ -70,6 +75,7 @@ export class DBQueueWorker extends JobScheduler implements IQueueWorker {
static async create(
logger: ILogger,
isDbConnected: () => boolean,
jobsRepository: IJobsRepository,
monitorsRepository: IMonitorsRepository,
checksRepository: IChecksRepository,
@@ -87,6 +93,7 @@ export class DBQueueWorker extends JobScheduler implements IQueueWorker {
): Promise<DBQueueWorker> {
const instance = new DBQueueWorker(
logger,
isDbConnected,
jobsRepository,
monitorsRepository,
checksRepository,
@@ -213,6 +220,7 @@ export class DBQueueWorker extends JobScheduler implements IQueueWorker {
private startLoop = (type: JobType) => {
this.pollMs[type] = POLL_MS;
const tick = async () => {
this.lastTickAt = Date.now();
if (this.stopped) return;
this.ticking[type] = true;
try {
@@ -288,6 +296,7 @@ export class DBQueueWorker extends JobScheduler implements IQueueWorker {
this.startLoop(type);
}
}
this.initComplete = true;
return true;
};
@@ -298,6 +307,7 @@ export class DBQueueWorker extends JobScheduler implements IQueueWorker {
};
override drain = async () => {
this.draining = true;
this.stopped = true;
const cutoff = Date.now() + DRAIN_TIMEOUT_MS;
while (this.getInFlightCount() > 0 && Date.now() < cutoff) {
@@ -326,11 +336,11 @@ export class DBQueueWorker extends JobScheduler implements IQueueWorker {
return {
workerId: this.workerId,
mode: this.queueMode,
dbConnected: true, // TODO
initComplete: true, // TODO
draining: false, // TODO
lastTickAt: null, // TODO
inFlight: Number.MAX_SAFE_INTEGER, // TODO
dbConnected: this.isDbConnected(),
initComplete: this.initComplete,
draining: this.draining,
lastTickAt: this.lastTickAt,
inFlight: this.getInFlightCount(),
};
};
}
+74
View File
@@ -0,0 +1,74 @@
import http from "http";
import { IQueueWorker, WorkerHealth } from "@/worker/worker.interface.js";
import { ILogger } from "@/utils/logger.js";
const SERVICE_NAME = "HealthServer";
const LIVENESS_STALE_MS = 30_000; // 30s ~6x POLL_MAX_MS so we don't restart workers
export interface IHealthServer {
listen(): Promise<void>;
close(): Promise<void>;
}
const isLive = (health: WorkerHealth): boolean => {
if (!health.initComplete) return false; // Not init yet
if (health.draining) return true; // Draining workers are still healthy
if (health.lastTickAt === null) return false; // Never ticked
if (Date.now() - health.lastTickAt >= LIVENESS_STALE_MS) return false; // Stale if last tick exceed LIVENESS_STALE_MS
return true;
};
const isReady = (health: WorkerHealth): boolean => {
if (health.draining) return false; // Draining workers are not ready
return health.initComplete && health.dbConnected;
};
export class HealthServer implements IHealthServer {
private server: http.Server;
constructor(
private logger: ILogger,
private port: string,
private worker: IQueueWorker
) {
this.server = http.createServer(this.handle);
}
private handle = (req: http.IncomingMessage, res: http.ServerResponse) => {
const path = (req.url ?? "").split("?")[0]; // Strip query params
if (path !== "/livez" && path !== "/readyz") {
res.writeHead(404).end();
return;
}
// Get health
const health = this.worker.getHealth();
const ok = path === "/livez" ? isLive(health) : isReady(health);
const code = ok ? 200 : 503;
res.writeHead(code, { "Content-Type": "application/json" });
res.end(JSON.stringify(health));
};
listen = () => {
return new Promise<void>((resolve) => {
const port = Number(this.port) || 52346;
this.server.listen(port, () => {
this.logger.info({
message: `Health server listening on ${port}`,
service: SERVICE_NAME,
method: "listen",
});
resolve();
});
});
};
close = () => {
return new Promise<void>((resolve) => {
this.server.close(() => resolve());
});
};
}
@@ -23,6 +23,7 @@ export class JobScheduler implements IJobScheduler {
}
protected stopped = false;
protected draining = false;
protected ticking: Partial<Record<JobType, boolean>> = {}; // Whether a loop is mid tick
protected tickFns = new Map<JobType, () => void>(); // Store tick fns so we can run them immediately on wake
protected timers = new Map<JobType | "heartbeat", NodeJS.Timeout>();
@@ -156,6 +157,7 @@ export class JobScheduler implements IJobScheduler {
drain = async () => {
this.stopped = true;
this.draining = true;
};
shutdown = async () => {