@sidequest/engine
Version:
@sidequest/engine is the core engine of SideQuest, a distributed background job processing system for Node.js and TypeScript.
152 lines (149 loc) • 6.58 kB
JavaScript
import { logger } from '@sidequest/core';
import nodeCron from 'node-cron';
import { Engine } from '../engine.js';
import { Dispatcher } from '../execution/dispatcher.js';
import { ExecutorManager } from '../execution/executor-manager.js';
import { QueueManager } from '../execution/queue-manager.js';
import { cleanupFinishedJobs } from '../routines/cleanup-finished-job.js';
import { releaseStaleJobs } from '../routines/release-stale-jobs.js';
import { gracefulShutdown } from '../utils/shutdown.js';
class MainWorker {
shuttingDown = false;
dispatcher;
engine = new Engine();
backend;
/**
* Starts a Sidequest worker process with the given configuration.
* @param sidequestConfig The Sidequest configuration for the worker.
*/
async runWorker(sidequestConfig) {
if (!this.shuttingDown) {
try {
const nonNullConfig = await this.engine.configure(sidequestConfig);
this.backend = this.engine.getBackend();
this.dispatcher = new Dispatcher(this.backend, new QueueManager(this.backend, nonNullConfig.queues, nonNullConfig.queueDefaults), new ExecutorManager(this.backend, nonNullConfig));
this.dispatcher.start();
await this.startCron(nonNullConfig.releaseStaleJobsIntervalMin, nonNullConfig.releaseStaleJobsMaxStaleMs, nonNullConfig.releaseStaleJobsMaxClaimedMs, nonNullConfig.cleanupFinishedJobsIntervalMin, nonNullConfig.cleanupFinishedJobsOlderThan);
}
catch (error) {
logger("Worker").error(error);
process.exit(1);
}
}
else {
logger("Worker").warn("Worker is already shutting down, ignoring run signal.");
}
}
/**
* Gracefully shuts down the worker and releases resources.
*/
async shutdown() {
if (!this.shuttingDown) {
this.shuttingDown = true;
await this.dispatcher?.stop();
await this.engine.close();
}
}
/**
* Starts cron job for releasing stale jobs.
* Also executes the task immediately.
*/
async startAndExecuteStaleJobsReleaseCron(intervalMin, maxStaleMs, maxClaimedMs) {
if (!this.backend) {
throw new Error("Backend is not initialized. Cannot start stale jobs release cron.");
}
logger("Worker").debug(`Starting stale jobs release cron with interval: ${intervalMin} minutes`);
const releaseTask = nodeCron.schedule(`*/${intervalMin} * * * *`, async () => {
try {
logger("Worker").debug("Running stale jobs release task");
await releaseStaleJobs(this.backend, maxStaleMs, maxClaimedMs);
}
catch (error) {
logger("Worker").error("Error on running ReleaseStaleJob!", error);
}
});
return releaseTask.execute();
}
/**
* Starts cron job for cleaning up finished jobs.
* Also executes the task immediately.
*/
async startAndExecuteFinishedJobsCleanupCron(intervalMin, cutoffMs) {
if (!this.backend) {
throw new Error("Backend is not initialized. Cannot start finished jobs cleanup cron.");
}
logger("Worker").debug(`Starting finished jobs cleanup cron with interval: ${intervalMin} minutes`);
const cleanupTask = nodeCron.schedule(`*/${intervalMin} * * * *`, async () => {
try {
logger("Worker").debug("Running finished jobs cleanup task");
await cleanupFinishedJobs(this.backend, cutoffMs);
}
catch (error) {
logger("Worker").error("Error on running CleanupJob!", error);
}
});
return cleanupTask.execute();
}
/**
* Starts cron jobs for releasing stale jobs and cleaning up finished jobs.
*
* @param staleIntervalMin Interval in minutes for releasing stale jobs, or false to disable.
* @param maxStaleMs Maximum age in milliseconds for stale jobs.
* @param maxClaimedMs Maximum age in milliseconds for claimed jobs.
* @param cleanupIntervalMin Interval in minutes for cleaning up finished jobs, or false to disable
* @param cleanupCutoffMs Maximum age in milliseconds for finished jobs to be cleaned up.
*/
async startCron(staleIntervalMin, maxStaleMs, maxClaimedMs, cleanupIntervalMin, cleanupCutoffMs) {
logger("Worker").debug("Starting cron jobs");
const promises = [];
if (staleIntervalMin !== false) {
promises.push(this.startAndExecuteStaleJobsReleaseCron(staleIntervalMin, maxStaleMs, maxClaimedMs));
}
if (cleanupIntervalMin !== false) {
promises.push(this.startAndExecuteFinishedJobsCleanupCron(cleanupIntervalMin, cleanupCutoffMs));
}
await Promise.all(promises).catch((error) => {
logger("Worker").error(error);
});
}
}
const isChildProcess = !!process.send;
if (isChildProcess) {
const worker = new MainWorker();
process.on("message",
// eslint-disable-next-line @typescript-eslint/no-misused-promises
async ({ type, sidequestConfig }) => {
if (type === "start") {
if (!sidequestConfig) {
throw new Error("No Sidequest configuration provided to worker!");
}
if (!worker.shuttingDown) {
gracefulShutdown(worker.shutdown.bind(worker), "Worker", sidequestConfig.gracefulShutdown);
logger("Worker").info("Starting worker with provided configuration...");
return await worker.runWorker(sidequestConfig);
}
else {
logger("Worker").warn("Worker is already shutting down, ignoring start signal.");
}
}
else if (type === "shutdown") {
if (!worker.shuttingDown) {
logger("Worker").info("Received shutdown message, shutting down worker...");
await worker.shutdown();
logger("Worker").info("Worker shutdown complete.");
process.exit(0);
}
else {
logger("Worker").debug("Worker is already shutting down, ignoring shutdown signal.");
}
}
});
process.on("disconnect", () => {
logger("Worker").error("Parent process disconnected, exiting...");
process.exit();
});
if (process.send)
process.send("ready");
}
export { MainWorker };
//# sourceMappingURL=main.js.map