@penkov/tasks_queue
Version:
A lightweight PostgreSQL-backed task queue system with scheduling, retries, backoff strategies, and priority handling. Designed for efficiency and observability in modern Node.js applications.
66 lines (65 loc) • 3.32 kB
JavaScript
import { mutable, option, Try } from "scats";
import log4js from "log4js";
import { MetricsService } from "application-metrics";
import { TasksPipeline } from "./tasks-pipeline.js";
import { TaskFailed } from "./tasks-model.js";
import { TimeUtils } from "./time-utils.js";
const logger = log4js.getLogger("TasksQueueWorker");
export class TasksQueueWorker {
tasksQueueDao;
workers = new mutable.HashMap();
pipeline;
constructor(tasksQueueDao, concurrency = 4, loopInterval = TimeUtils.minute) {
this.tasksQueueDao = tasksQueueDao;
this.pipeline = new TasksPipeline(concurrency, () => this.tasksQueueDao.nextPending(this.workers.keySet), () => this.tasksQueueDao.peekNextStartAfter(this.workers.keySet), (t) => this.processNextTask(t), loopInterval);
}
start() {
this.pipeline.start();
}
async stop() {
return this.pipeline.stop();
}
registerWorker(queueName, worker) {
if (this.workers.containsKey(queueName)) {
logger.warn(`Replacing existing worker for queue: ${queueName}`);
}
this.workers.put(queueName, worker);
}
tasksScheduled(queueName) {
if (this.workers.containsKey(queueName)) {
this.pipeline.triggerLoop();
}
}
async processNextTask(task) {
MetricsService.counter("tasks_queue_started").inc();
await this.workers.get(task.queue).match({
some: async (worker) => {
try {
Try(() => worker.starting(task.id, task.payload)).tapFailure((e) => logger.warn(`Failed to invoke 'starting' callback for task (id=${task.id}) in queue=${task.queue}`, e));
await worker.process(task.payload, {
maxAttempts: task.maxAttempts,
currentAttempt: task.currentAttempt,
});
if (option(task.repeatType).isEmpty) {
await this.tasksQueueDao.finish(task.id);
}
else {
await this.tasksQueueDao.rescheduleIfPeriodic(task.id);
}
MetricsService.counter("tasks_queue_processed").inc();
(await Try.promise(() => worker.completed(task.id, task.payload))).tapFailure((e) => logger.warn(`Failed to invoke 'completed' callback for task (id=${task.id}) in queue=${task.queue}`, e));
}
catch (e) {
const finalStatus = await this.tasksQueueDao.fail(task.id, e["message"] || e, e instanceof TaskFailed ? e.payload : task.payload);
(await Try.promise(() => worker.failed(task.id, task.payload, finalStatus, e))).tapFailure((e) => logger.warn(`Failed to invoke 'failed' callback for task (id=${task.id}) in queue=${task.queue}`, e));
MetricsService.counter("tasks_queue_failed").inc();
logger.warn(`Failed to process task (id=${task.id}) in queue=${task.queue}`, e);
}
},
none: async () => {
MetricsService.counter("tasks_queue_skipped_no_worker").inc();
logger.info(`Failed to process task (id=${task.id}) in queue=${task.queue}: no suitable worker found`);
},
});
}
}