UNPKG

@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.

120 lines (119 loc) 4.7 kB
import { Collection, mutable, none, option, some, } from "scats"; import { TasksQueueService } from "./tasks-queue.service.js"; import { TasksAuxiliaryWorker } from "./tasks-auxiliary-worker.js"; import log4js from "log4js"; import { TaskPeriodType, } from "./tasks-model.js"; import { SystemClock } from "./clock.js"; export const DEFAULT_POOL = "default"; const logger = log4js.getLogger("TasksPoolsService"); export class TasksPoolsService { dao; clock; pools; queuesPool = new mutable.HashMap(); auxiliaryWorker; constructor(dao, manageTasksQueueService, runAuxiliaryWorker, pools = [ { name: DEFAULT_POOL, concurrency: 1, loopInterval: 60000, }, ], clock = new SystemClock()) { this.dao = dao; this.clock = clock; const poolsCollection = Collection.from(pools); const poolNames = poolsCollection.map((p) => p.name).toSet; if (poolsCollection.size !== poolNames.size) { throw new Error("Duplicate pool names detected"); } this.pools = poolsCollection.toMap((p) => [ p.name, new TasksQueueService(dao, manageTasksQueueService, { concurrency: p.concurrency, runAuxiliaryWorker: false, loopInterval: p.loopInterval, queueNotifier: (queueName) => this.notifyQueue(queueName), }, this.clock, p.name), ]); this.auxiliaryWorker = runAuxiliaryWorker ? some(new TasksAuxiliaryWorker(dao, manageTasksQueueService, this.clock)) : none; } start() { logger.info(`Starting TasksPoolsService with ${this.pools.size} pools`); this.auxiliaryWorker.foreach((w) => w.start()); this.pools.values.foreach((p) => p.start()); } async stop(timeoutMs = 30000) { logger.info("Stopping TasksPoolsService"); let stopTimer; try { this.auxiliaryWorker.foreach((w) => w.stop()); await Promise.race([ this.pools.values.mapPromise((p) => p.stop()), new Promise((_, reject) => { stopTimer = setTimeout(() => reject(new Error("Stop timeout")), timeoutMs); }), ]); logger.info("TasksPoolsService stopped successfully"); } catch (e) { logger.error("Failed to stop TasksPoolsService gracefully", e); throw e; } finally { option(stopTimer).foreach((timer) => clearTimeout(timer)); } } registerWorker(queueName, worker, poolName = DEFAULT_POOL) { if (this.queuesPool.containsKey(queueName)) { throw new Error(`Queue '${queueName}' is already registered in pool '${this.queuesPool.get(queueName).getOrElseValue("unknown")}'`); } this.pools.get(poolName).match({ some: (pool) => { pool.registerWorker(queueName, worker); this.queuesPool.put(queueName, poolName); logger.info(`Registered worker for queue '${queueName}' in pool '${poolName}'`); }, none: () => { throw new Error(`Pool '${poolName}' not registered`); }, }); } async schedule(task) { const taskId = await this.dao.schedule(task, this.clock.now()); this.taskScheduled(task.queue, taskId); return taskId; } async scheduleAtFixedRate(task) { const taskId = await this.dao.schedulePeriodic(task, TaskPeriodType.fixed_rate, this.clock.now()); this.taskScheduled(task.queue, taskId); return taskId; } async scheduleAtFixedDelay(task) { const taskId = await this.dao.schedulePeriodic(task, TaskPeriodType.fixed_delay, this.clock.now()); this.taskScheduled(task.queue, taskId); return taskId; } async scheduleAtCron(task) { const taskId = await this.dao.schedulePeriodic(task, TaskPeriodType.cron, this.clock.now()); this.taskScheduled(task.queue, taskId); return taskId; } taskScheduled(queue, taskId) { this.notifyQueue(queue, taskId); } notifyQueue(queue, taskId = none) { this.queuesPool.get(queue).match({ some: (poolName) => { this.pools.get(poolName).foreach((pool) => { pool.taskScheduled(queue); }); }, none: () => { logger.info(`No worker registered for a queue '${queue}'. ` + `Task (id=${taskId.getOrElseValue(-1)}) will remain in pending state`); }, }); } }