Files
optima/api/src/workert.ts
T
HoloPanio 2eb387811d fix(worker): break circular import by extracting PgBoss singleton
incremental-sync.ts and api/cw/sync.ts imported getBoss() from workert.ts.
When workert.ts (the entry point) dynamically imported incremental-sync.ts,
it triggered a circular module re-evaluation that hung indefinitely.

Extract the PgBoss singleton and getBoss() factory to a new boss-instance.ts
module that neither has top-level async side-effects nor imports from
workert.ts. All consumers (workert.ts, index.ts, incremental-sync.ts,
cw/sync.ts) now import from boss-instance.ts instead.
2026-04-14 00:34:33 +00:00

240 lines
8.3 KiB
TypeScript

import { io, Socket } from "socket.io-client";
import { WorkerQueue } from "./modules/workers/queues";
import { setupEventDebugger } from "./modules/logging/eventDebugger";
import { boss, getBoss } from "./boss-instance";
let bossStartPromise: Promise<void> | null = null;
let reservationQueueReady = false;
let managerSocket: Socket | null = null;
let managerSocketReadyPromise: Promise<Socket> | null = null;
async function ensureBossStarted(): Promise<void> {
if (bossStartPromise) return bossStartPromise;
bossStartPromise = boss.start().then(() => undefined);
await bossStartPromise;
}
async function ensureReservationQueue(): Promise<void> {
if (reservationQueueReady) return;
try {
await boss.createQueue(WorkerQueue.WORKER_NAMESPACE_RESERVATION);
} catch {
// Queue may already exist; ignore to keep this idempotent.
}
reservationQueueReady = true;
}
export function ensureManagerSocketReady(): Promise<Socket> {
if (managerSocket && managerSocket.connected)
return Promise.resolve(managerSocket);
if (managerSocketReadyPromise) return managerSocketReadyPromise;
const managerUrl = process.env.MANAGER_SOCKET_URL ?? "http://localhost:8671";
managerSocket = io(managerUrl, {
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: Infinity,
});
managerSocket.on("connect", () => {
console.log("[worker] Connected to manager socket on :8671");
});
managerSocket.on("disconnect", (reason) => {
console.warn(`[worker] Manager socket disconnected: ${reason}`);
});
managerSocket.on("connect_error", (err) => {
console.error("[worker] Manager socket connect_error", err.message);
});
managerSocket.on("reconnect_attempt", () => {
console.log("[worker] Attempting to reconnect to manager socket...");
});
managerSocketReadyPromise = new Promise<Socket>((resolve) => {
const socket = managerSocket!;
const onConnect = () => {
socket.off("connect_error", onConnectError);
resolve(socket);
};
const onConnectError = (err: Error) => {
// Log the error but don't reject - let Socket.IO handle reconnection
console.warn("[worker] Connection attempt failed:", err.message, "- retrying...");
};
socket.once("connect", onConnect);
socket.on("connect_error", onConnectError);
});
return managerSocketReadyPromise;
}
/**
* Ask PgBoss to generate a durable job ID we can reuse as a worker namespace ID.
*
* A short-lived reservation job is created and immediately canceled to avoid
* growing the queue while still obtaining a PgBoss-generated ID.
*/
export async function reserveWorkerId(queueType: WorkerQueue): Promise<string> {
await ensureBossStarted();
await ensureReservationQueue();
const workerId = await boss.send(WorkerQueue.WORKER_NAMESPACE_RESERVATION, {
queueType,
requestedAt: new Date().toISOString(),
});
if (!workerId) {
throw new Error("Failed to reserve PgBoss job ID for worker namespace");
}
try {
await boss.cancel(WorkerQueue.WORKER_NAMESPACE_RESERVATION, workerId);
} catch {
// Best-effort cleanup only.
}
return workerId;
}
async function ensureDalpuriSyncQueue(): Promise<void> {
try {
console.log("[worker] Creating DALPURI_FULL_SYNC queue...");
await boss.createQueue(WorkerQueue.DALPURI_FULL_SYNC);
console.log("[worker] DALPURI_FULL_SYNC queue ready");
} catch (err) {
console.log("[worker] DALPURI_FULL_SYNC queue already exists (or error):", (err as Error).message);
}
try {
console.log("[worker] Creating DALPURI_INCREMENTAL_SYNC queue...");
await boss.createQueue(WorkerQueue.DALPURI_INCREMENTAL_SYNC);
console.log("[worker] DALPURI_INCREMENTAL_SYNC queue ready");
} catch (err) {
console.log("[worker] DALPURI_INCREMENTAL_SYNC queue already exists (or error):", (err as Error).message);
}
try {
console.log("[worker] Creating REFRESH_SALES_METRICS queue...");
await boss.createQueue(WorkerQueue.REFRESH_SALES_METRICS);
console.log("[worker] REFRESH_SALES_METRICS queue ready");
} catch (err) {
console.log("[worker] REFRESH_SALES_METRICS queue already exists (or error):", (err as Error).message);
}
}
/**
* Initialize the worker system. Must be called before enqueueing jobs.
* Starts PgBoss connection to PostgreSQL.
*/
export async function initializeWorkerSystem(): Promise<void> {
await ensureBossStarted();
await ensureReservationQueue();
await ensureDalpuriSyncQueue();
console.log("[worker] Worker system initialized - ready for job enqueueing");
}
if (import.meta.main) {
// if (Bun.env.NODE_ENV === "development") {
// setupEventDebugger({ processLabel: "WORKER" });
// }
console.log("[worker] Worker process starting...");
console.log(
`[worker] Connecting to PgBoss on DATABASE_URL and SocketIO on ${process.env.MANAGER_SOCKET_URL ?? "http://localhost:8671"}`
);
console.log(`[worker] DATABASE_URL set: ${!!process.env.DATABASE_URL}`);
// Ensure PgBoss is connected and queues exist
console.log("[worker] Starting PgBoss...");
try {
await Promise.race([
ensureBossStarted(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("boss.start() timed out after 30s")), 30_000)
),
]);
} catch (err) {
console.error("[worker] FATAL: PgBoss failed to start:", err);
process.exit(1);
}
console.log("[worker] PgBoss started successfully");
console.log("[worker] Ensuring sync queues...");
await ensureDalpuriSyncQueue();
console.log("[worker] Sync queues ready");
// Register job handler for DALPURI_FULL_SYNC
console.log("[worker] Importing sync-manager...");
const { enqueueDalpuriFullSync } = await import("./modules/workers/sync-manager");
console.log("[worker] Importing dalpuri-sync...");
const { executeIncrementalSync } = await import("./modules/workers/dalpuri-sync");
console.log("[worker] Importing incremental-sync...");
const { enqueueIncrementalSync } = await import("./modules/workers/incremental-sync");
await boss.work(WorkerQueue.DALPURI_FULL_SYNC, async () => {
const socket = await ensureManagerSocketReady();
await enqueueDalpuriFullSync(socket);
});
console.log("[worker] Registered DALPURI_FULL_SYNC job handler");
await boss.work(WorkerQueue.DALPURI_INCREMENTAL_SYNC, async () => {
const startedAt = Date.now();
console.log("[worker] DALPURI_INCREMENTAL_SYNC started");
try {
await executeIncrementalSync();
console.log(
`[worker] DALPURI_INCREMENTAL_SYNC completed in ${Date.now() - startedAt}ms`
);
} catch (err) {
console.error(
`[worker] DALPURI_INCREMENTAL_SYNC failed in ${Date.now() - startedAt}ms`,
err
);
throw err;
}
});
console.log("[worker] Registered DALPURI_INCREMENTAL_SYNC job handler");
const enqueueIncrementalWithLogging = () => {
enqueueIncrementalSync().catch((err) => {
console.error(
`[worker] interval enqueueIncrementalSync failed: ${err?.message ?? err}`
);
});
};
// Keep a worker-local 5s scheduler so incremental sync continues even when
// API interval scheduling is unavailable.
enqueueIncrementalWithLogging();
setInterval(enqueueIncrementalWithLogging, 5_000);
console.log("[worker] Started 5-second incremental enqueue interval");
// Register job handler for REFRESH_SALES_METRICS
const { executeSalesMetricsRefresh } = await import("./modules/workers/sales-metrics");
await boss.work(WorkerQueue.REFRESH_SALES_METRICS, async (jobs) => {
const job = Array.isArray(jobs) ? jobs[0] : jobs;
const data = job?.data as { forceColdLoad?: boolean } | undefined;
const forceColdLoad = data?.forceColdLoad ?? false;
await executeSalesMetricsRefresh({ forceColdLoad });
});
console.log("[worker] Registered REFRESH_SALES_METRICS job handler");
// Initiate manager socket connection (will reconnect automatically if it fails)
ensureManagerSocketReady().catch((err) => {
console.error("[worker] Failed to establish manager socket:", err.message);
console.log("[worker] Will continue retrying in background...");
});
console.log("[worker] Worker process ready - waiting for job requests");
// Keep process alive
process.on("SIGTERM", async () => {
console.log("[worker] SIGTERM received, shutting down gracefully");
await boss.stop();
process.exit(0);
});
}