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.

1,299 lines (1,286 loc) 51 kB
'use strict'; var scats = require('scats'); var log4js = require('log4js'); var applicationMetrics = require('application-metrics'); var tslib = require('tslib'); var common = require('@nestjs/common'); var core = require('@nestjs/core'); var swagger = require('@nestjs/swagger'); exports.TaskStatus = void 0; (function (TaskStatus) { TaskStatus["pending"] = "pending"; TaskStatus["in_progress"] = "in_progress"; TaskStatus["finished"] = "finished"; TaskStatus["error"] = "error"; })(exports.TaskStatus || (exports.TaskStatus = {})); exports.BackoffType = void 0; (function (BackoffType) { BackoffType["constant"] = "constant"; BackoffType["linear"] = "linear"; BackoffType["exponential"] = "exponential"; })(exports.BackoffType || (exports.BackoffType = {})); exports.TaskPeriodType = void 0; (function (TaskPeriodType) { TaskPeriodType["fixed_rate"] = "fixed_rate"; TaskPeriodType["fixed_delay"] = "fixed_delay"; })(exports.TaskPeriodType || (exports.TaskPeriodType = {})); exports.MissedRunStrategy = void 0; (function (MissedRunStrategy) { MissedRunStrategy["catch_up"] = "catch_up"; MissedRunStrategy["skip_missed"] = "skip_missed"; })(exports.MissedRunStrategy || (exports.MissedRunStrategy = {})); class TaskFailed extends Error { payload; constructor(message, payload) { super(message); this.payload = payload; } } class TimeUtils { static second = 1000; static minute = TimeUtils.second * 60; static hour = TimeUtils.minute * 60; static day = TimeUtils.hour * 24; } const defaultLoopInterval = TimeUtils.minute; var PipelineNextOperationType; (function (PipelineNextOperationType) { PipelineNextOperationType[PipelineNextOperationType["PollNext"] = 0] = "PollNext"; PipelineNextOperationType[PipelineNextOperationType["Sleep"] = 1] = "Sleep"; })(PipelineNextOperationType || (PipelineNextOperationType = {})); const PipelineNextOperationFactory = { pollNext: { type: PipelineNextOperationType.PollNext, }, sleep: (delayMs = defaultLoopInterval) => ({ type: PipelineNextOperationType.Sleep, delayMs, }), }; const logger$4 = log4js.getLogger("TasksPipeline"); const noop = () => { }; class TasksPipeline { maxConcurrentTasks; pollNextTask; peekNextStartAfter; processTask; loopInterval; tasksInProcess = 0; loopRunning = false; nextLoopTime = 0; periodicTaskFetcher = null; stopRequested = false; tasksCountListener = noop; loopTimer; constructor(maxConcurrentTasks, pollNextTask, peekNextStartAfter, processTask, loopInterval = defaultLoopInterval) { this.maxConcurrentTasks = maxConcurrentTasks; this.pollNextTask = pollNextTask; this.peekNextStartAfter = peekNextStartAfter; this.processTask = processTask; this.loopInterval = loopInterval; this.loopTimer = async () => { this.periodicTaskFetcher = null; let sleepInterval = this.loopInterval; try { sleepInterval = await this.loop(); } finally { if (!this.periodicTaskFetcher) { this.nextLoopTime = Date.now() + sleepInterval; this.periodicTaskFetcher = setTimeout(() => this.loopTimer(), Math.min(this.loopInterval, sleepInterval)); } } }; } start() { this.stopRequested = false; scats.option(this.periodicTaskFetcher).foreach((t) => clearTimeout(t)); this.nextLoopTime = Date.now() + 1; this.periodicTaskFetcher = setTimeout(() => this.loopTimer(), 1); } async stop() { this.stopRequested = true; scats.option(this.periodicTaskFetcher).foreach((t) => clearTimeout(t)); if (this.tasksInProcess > 0) { await new Promise((resolve) => { this.tasksCountListener = (n) => { if (n <= 0) { this.tasksCountListener = noop; resolve(); } }; }); } } triggerLoop() { if (!this.loopRunning && !this.stopRequested) { setTimeout(() => this.loop(), 1); } } async loop() { if (this.loopRunning || this.stopRequested) { return this.loopInterval; } this.loopRunning = true; let sleepInterval = this.loopInterval; try { let nextOp = PipelineNextOperationFactory.pollNext; while (nextOp.type !== PipelineNextOperationType.Sleep) { if (this.tasksInProcess < this.maxConcurrentTasks && !this.stopRequested) { nextOp = await this.fetchNextTask(); } else { nextOp = PipelineNextOperationFactory.sleep(this.loopInterval); } } sleepInterval = Math.min(nextOp.delayMs, this.loopInterval); } finally { this.loopRunning = false; } const nextTriggerTime = Date.now() + sleepInterval; if (this.periodicTaskFetcher && this.nextLoopTime > nextTriggerTime) { logger$4.trace(`Resetting loop timer to closer time: from ${new Date(this.nextLoopTime)} to new ${new Date(nextTriggerTime)}`); clearTimeout(this.periodicTaskFetcher); this.nextLoopTime = nextTriggerTime; this.periodicTaskFetcher = setTimeout(() => this.loopTimer(), Math.min(this.loopInterval, sleepInterval)); } return sleepInterval; } async fetchNextTask() { const task = await this.pollNextTask(); return task.match({ some: async (t) => { this.tasksInProcess++; setImmediate(() => this.processTaskInLoop(t)); return this.tasksInProcess < this.maxConcurrentTasks ? PipelineNextOperationFactory.pollNext : PipelineNextOperationFactory.sleep(this.loopInterval); }, none: async () => { const nextTimeOpt = await this.peekNextStartAfter(); const delayMs = nextTimeOpt .map((startAfter) => { const delay = startAfter.getTime() - Date.now(); return Math.max(0, delay); }) .getOrElseValue(this.loopInterval); return PipelineNextOperationFactory.sleep(delayMs); }, }); } async processTaskInLoop(t) { try { logger$4.debug(`Starting task (id=${t.id}) in queue ${t.queue} (active ${this.tasksInProcess} of ${this.maxConcurrentTasks})`); await this.processTask(t); } catch (error) { logger$4.warn(`Failed to process task ${t.id} in queue ${t.queue} `, error); } finally { this.taskIsDone(); logger$4.debug(`Finished working with task ${t.id} in queue ${t.queue} (active ${this.tasksInProcess} of ${this.maxConcurrentTasks})`); setImmediate(() => this.loop()); } } taskIsDone() { this.tasksInProcess--; this.tasksCountListener(this.tasksInProcess); } } const logger$3 = log4js.getLogger("TasksQueueWorker"); class TasksQueueWorker { tasksQueueDao; workers = new scats.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$3.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) { applicationMetrics.MetricsService.counter("tasks_queue_started").inc(); await this.workers.get(task.queue).match({ some: async (worker) => { try { scats.Try(() => worker.starting(task.id, task.payload)).tapFailure((e) => logger$3.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 (scats.option(task.repeatType).isEmpty) { await this.tasksQueueDao.finish(task.id); } else { await this.tasksQueueDao.rescheduleIfPeriodic(task.id); } applicationMetrics.MetricsService.counter("tasks_queue_processed").inc(); (await scats.Try.promise(() => worker.completed(task.id, task.payload))).tapFailure((e) => logger$3.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 scats.Try.promise(() => worker.failed(task.id, task.payload, finalStatus, e))).tapFailure((e) => logger$3.warn(`Failed to invoke 'failed' callback for task (id=${task.id}) in queue=${task.queue}`, e)); applicationMetrics.MetricsService.counter("tasks_queue_failed").inc(); logger$3.warn(`Failed to process task (id=${task.id}) in queue=${task.queue}`, e); } }, none: async () => { applicationMetrics.MetricsService.counter("tasks_queue_skipped_no_worker").inc(); logger$3.info(`Failed to process task (id=${task.id}) in queue=${task.queue}: no suitable worker found`); }, }); } } const logger$2 = log4js.getLogger("TasksAuxiliaryWorker"); class TasksAuxiliaryWorker { tasksQueueDao; manageTasksQueueService; workerTimer = null; metricsTimer = null; queuesCounts = scats.HashMap.empty; constructor(tasksQueueDao, manageTasksQueueService) { this.tasksQueueDao = tasksQueueDao; this.manageTasksQueueService = manageTasksQueueService; } start() { const runWorker = () => { this.runAuxiliaryJobs(); }; this.workerTimer = setInterval(() => { try { runWorker(); } catch (e) { logger$2.warn("Failed to process stalled tasks", e); } }, TimeUtils.second * 30); try { runWorker(); } catch (e) { logger$2.warn("Failed to process stalled tasks", e); } const runMetrics = () => { this.fetchMetrics(); }; this.metricsTimer = setInterval(() => { try { runMetrics(); } catch (e) { logger$2.warn("Failed to sync metrics", e); } }, TimeUtils.minute * 2); try { runMetrics(); } catch (e) { logger$2.warn("Failed to sync metrics", e); } } runAuxiliaryJobs() { try { this.tasksQueueDao .failStalled() .then((res) => { if (res.nonEmpty) { logger$2.info(`Marked stalled as failed: ${res.mkString(", ")}`); } }) .catch((e) => { logger$2.warn("Failed to process stalled tasks", e); }); this.tasksQueueDao.resetFailed().catch((e) => { logger$2.warn("Failed to reset failed tasks", e); }); this.tasksQueueDao.clearFinished().catch((e) => { logger$2.warn("Failed to clear finished tasks", e); }); } catch (e) { logger$2.warn("Failed to process stalled tasks", e); } } fetchMetrics() { this.manageTasksQueueService .tasksCount() .then((tasksCounts) => { this.queuesCounts = tasksCounts.groupBy((c) => c.queueName); tasksCounts.foreach((c) => { applicationMetrics.MetricsService.gauge(`tasks_queue_${c.queueName}_${c.status}`.replace(/[^a-zA-Z0-9_:]/g, "_"), () => { return this.queuesCounts .get(c.queueName) .getOrElseValue(scats.Nil) .find((x) => c.status === x.status) .map((c) => c.count) .getOrElseValue(0); }); }); }) .catch((e) => { logger$2.warn("Failed to sync metrics", e); }); } async stop() { scats.option(this.workerTimer).foreach((t) => clearTimeout(t)); scats.option(this.metricsTimer).foreach((t) => clearTimeout(t)); } } const logger$1 = log4js.getLogger("TasksQueueService"); class TasksQueueService { tasksQueueDao; worker; auxiliaryWorker; constructor(tasksQueueDao, manageTasksQueueService, config) { this.tasksQueueDao = tasksQueueDao; this.worker = new TasksQueueWorker(this.tasksQueueDao, config.concurrency, config.loopInterval); if (config.runAuxiliaryWorker) { this.auxiliaryWorker = scats.some(new TasksAuxiliaryWorker(tasksQueueDao, manageTasksQueueService)); } else { this.auxiliaryWorker = scats.none; } } async schedule(task) { await this.tasksQueueDao.schedule(task); this.taskScheduled(task.queue); } async scheduleAtFixedRate(task) { await this.tasksQueueDao.schedulePeriodic(task, exports.TaskPeriodType.fixed_rate); this.taskScheduled(task.queue); } async scheduleAtFixedDelay(task) { await this.tasksQueueDao.schedulePeriodic(task, exports.TaskPeriodType.fixed_delay); this.taskScheduled(task.queue); } taskScheduled(queueName) { this.worker.tasksScheduled(queueName); } registerWorker(queueName, worker) { this.worker.registerWorker(queueName, worker); } start() { try { this.worker.start(); this.auxiliaryWorker.foreach((w) => w.start()); } catch (e) { logger$1.warn("Failed to process stalled tasks", e); } } async stop() { await this.auxiliaryWorker.mapPromise((w) => w.stop()); await this.worker.stop(); } } const DEFAULT_POOL = "default"; const logger = log4js.getLogger("TasksPoolsService"); class TasksPoolsService { dao; pools; queuesPool = new scats.mutable.HashMap(); auxiliaryWorker; constructor(dao, manageTasksQueueService, runAuxiliaryWorker, pools = [ { name: DEFAULT_POOL, concurrency: 1, loopInterval: 60000, }, ]) { this.dao = dao; const poolsCollection = scats.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, }), ]); this.auxiliaryWorker = runAuxiliaryWorker ? scats.some(new TasksAuxiliaryWorker(dao, manageTasksQueueService)) : scats.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"); try { await Promise.race([ Promise.all([ this.auxiliaryWorker.mapPromise((w) => w.stop()), this.pools.values.mapPromise((p) => p.stop()), ]), new Promise((_, reject) => setTimeout(() => reject(new Error("Stop timeout")), timeoutMs)), ]); logger.info("TasksPoolsService stopped successfully"); } catch (e) { logger.error("Failed to stop TasksPoolsService gracefully", e); throw e; } } 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.taskScheduled(task.queue, taskId); } async scheduleAtFixedRate(task) { const taskId = await this.dao.schedulePeriodic(task, exports.TaskPeriodType.fixed_rate); this.taskScheduled(task.queue, taskId); } async scheduleAtFixedDelay(task) { const taskId = await this.dao.schedulePeriodic(task, exports.TaskPeriodType.fixed_delay); this.taskScheduled(task.queue, taskId); } taskScheduled(queue, taskId) { 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`); }, }); } } class TasksQueueDao { pool; constructor(pool) { this.pool = pool; } async withClient(cb) { const client = await this.pool.connect(); let res; try { res = await cb(client); } finally { client.release(); } return res; } async schedule(task) { const now = new Date(); return await this.withClient(async (cl) => { const res = await cl.query(`insert into tasks_queue (queue, created, status, priority, payload, timeout, max_attempts, start_after, initial_start, backoff, backoff_type) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) returning id`, [ task.queue, now, exports.TaskStatus.pending, scats.option(task.priority).getOrElseValue(0), scats.option(task.payload).orNull, scats.option(task.timeout).getOrElseValue(TimeUtils.hour), scats.option(task.retries).getOrElseValue(1), scats.option(task.startAfter).orNull, scats.option(task.startAfter).getOrElseValue(now), scats.option(task.backoff).getOrElseValue(TimeUtils.minute), scats.option(task.backoffType).getOrElseValue(exports.BackoffType.linear), ]); return scats.Collection.from(res.rows).headOption.map((r) => r.id); }); } async schedulePeriodic(task, periodType) { const now = new Date(); return await this.withClient(async (cl) => { const res = await cl.query(`insert into tasks_queue (queue, created, status, priority, payload, timeout, max_attempts, start_after, initial_start, name, repeat_interval, repeat_type, backoff, backoff_type, missed_runs_strategy) values ($1, $2, $3, $4, $5, $6, $7, $8, $8, $9, $10, $11, $12, $13, $14) on conflict (name) do nothing returning id`, [ task.queue, now, exports.TaskStatus.pending, scats.option(task.priority).getOrElseValue(0), scats.option(task.payload).orNull, scats.option(task.timeout).getOrElseValue(TimeUtils.hour), scats.option(task.retries).getOrElseValue(1), scats.option(task.startAfter).getOrElseValue(now), task.name, task.period, periodType, scats.option(task.backoff).getOrElseValue(TimeUtils.minute), scats.option(task.backoffType).getOrElseValue(exports.BackoffType.linear), scats.option(task.missedRunStrategy).getOrElseValue(exports.MissedRunStrategy.skip_missed), ]); return scats.Collection.from(res.rows).headOption.map((r) => r.id); }); } async nextPending(queueNames) { if (queueNames.isEmpty) { return scats.none; } const now = new Date(); const placeholders = queueNames.zipWithIndex .map(([_, idx]) => `$${idx + 4}`) .mkString(","); const paramsStatic = [ exports.TaskStatus.in_progress, now, exports.TaskStatus.pending, ]; const params = paramsStatic.concat(queueNames.toArray); return await this.withClient(async (cl) => { const query = ` WITH selected AS (SELECT id, payload, queue FROM tasks_queue WHERE status = $3 AND queue IN (${placeholders}) AND max_attempts > attempt AND (start_after IS NULL or start_after <= $2) ORDER BY priority DESC, id ASC FOR UPDATE SKIP LOCKED LIMIT 1) UPDATE tasks_queue SET status = $1, started = $2, finished = null, attempt = attempt + 1 FROM selected WHERE tasks_queue.id = selected.id RETURNING tasks_queue.id, tasks_queue.payload, tasks_queue.queue, tasks_queue.repeat_type, tasks_queue.attempt, tasks_queue.max_attempts `; const res = await cl.query(query, params); return scats.Collection.from(res.rows).headOption.map((r) => { return { id: r["id"], payload: scats.option(r["payload"]).orUndefined, queue: r["queue"], repeatType: scats.option(r["repeat_type"]).orUndefined, currentAttempt: r["attempt"], maxAttempts: r["max_attempts"], }; }); }); } async peekNextStartAfter(queueNames) { if (queueNames.isEmpty) { return scats.none; } const now = new Date(); const placeholders = queueNames.zipWithIndex .map(([_, idx]) => `$${idx + 4}`) .mkString(","); return this.withClient(async (cl) => { const paramsStatic = [exports.TaskStatus.pending, exports.TaskStatus.error, now]; const params = paramsStatic.concat(queueNames.toArray); const res = await cl.query(`SELECT MIN(start_after) AS min_start FROM tasks_queue WHERE status IN ($1, $2) AND queue IN (${placeholders}) AND start_after > $3`, params); return scats.Collection.from(res.rows) .headOption.flatMap((r) => scats.option(r["min_start"])) .map((ts) => new Date(ts)); }); } async finish(taskId) { const now = new Date(); await this.withClient(async (cl) => { await cl.query(`update tasks_queue set status=$1, finished=$2, error=null where id = $3 and status = $4`, [exports.TaskStatus.finished, now, taskId, exports.TaskStatus.in_progress]); }); } async rescheduleIfPeriodic(taskId) { await this.withClient(async (cl) => { const now = new Date(); await cl.query(` UPDATE tasks_queue SET status = $1, start_after = CASE WHEN repeat_type = 'fixed_rate' AND missed_runs_strategy = 'catch_up' THEN start_after + (repeat_interval * interval '1 millisecond') WHEN repeat_type = 'fixed_rate' AND missed_runs_strategy = 'skip_missed' THEN initial_start + (CEIL(EXTRACT(EPOCH FROM ($2 - initial_start)) * 1000 / repeat_interval) * repeat_interval * interval '1 millisecond') ELSE $2 + (repeat_interval * interval '1 millisecond') END, finished = $2, error = NULL, attempt = 0 WHERE id = $3 AND status = $4 AND repeat_interval IS NOT NULL AND missed_runs_strategy IS NOT NULL AND repeat_type IN ('${exports.TaskPeriodType.fixed_rate}', '${exports.TaskPeriodType.fixed_delay}'); `, [exports.TaskStatus.pending, now, taskId, exports.TaskStatus.in_progress]); }); } async fail(taskId, error, nextPayload) { const now = new Date(); return await this.withClient(async (cl) => { const res = await cl.query(` UPDATE tasks_queue SET finished = $1, error = $2, status = CASE WHEN attempt < max_attempts THEN $3 -- pending ELSE $4 -- error END, start_after = CASE WHEN attempt < max_attempts THEN $1::timestamp + ( CASE backoff_type WHEN 'constant' THEN backoff WHEN 'linear' THEN (backoff * attempt) WHEN 'exponential' THEN (backoff * POWER(2, (attempt - 1))) ELSE backoff END ) * interval '1 millisecond' ELSE NULL END, payload = $7 WHERE id = $5 AND status = $6 returning status `, [ now, error, exports.TaskStatus.pending, exports.TaskStatus.error, taskId, exports.TaskStatus.in_progress, scats.option(nextPayload).orNull, ]); return scats.Collection.from(res.rows) .headOption.map((r) => exports.TaskStatus[r.status]) .getOrElseValue(exports.TaskStatus.error); }); } async failStalled() { const now = new Date(); return await this.withClient(async (cl) => { const res = await cl.query(`update tasks_queue set status=$1, finished=$2, error=$3 where status = $4 and timeout is not null and started + timeout * interval '1 ms' < $5 returning id`, [exports.TaskStatus.error, now, "Timeout", exports.TaskStatus.in_progress, now]); return scats.Collection.from(res.rows).map((r) => r.id); }); } async resetFailed() { await this.withClient(async (cl) => { await cl.query(`update tasks_queue set status=$1, finished=null where status = $2 and timeout is not null and max_attempts > COALESCE(attempt, 0)`, [exports.TaskStatus.pending, exports.TaskStatus.error]); }); } async clearFinished(timeout = TimeUtils.day) { const expired = new Date(Date.now() - timeout); await this.withClient(async (cl) => { await cl.query(`delete from tasks_queue where status = $1 and finished < $2`, [exports.TaskStatus.finished, expired]); }); } async statusCount(queue, status) { return await this.withClient(async (cl) => { const res = await cl.query(`select count(id) as cnt from tasks_queue where queue = $1 and status = $2`, [queue, status]); return scats.Collection.from(res.rows) .headOption.map((r) => r["cnt"]) .getOrElseValue(0); }); } } tslib.__decorate([ applicationMetrics.Metric(), tslib.__metadata("design:type", Function), tslib.__metadata("design:paramtypes", [Object]), tslib.__metadata("design:returntype", Promise) ], TasksQueueDao.prototype, "schedule", null); tslib.__decorate([ applicationMetrics.Metric(), tslib.__metadata("design:type", Function), tslib.__metadata("design:paramtypes", [Object, String]), tslib.__metadata("design:returntype", Promise) ], TasksQueueDao.prototype, "schedulePeriodic", null); tslib.__decorate([ applicationMetrics.Metric(), tslib.__metadata("design:type", Function), tslib.__metadata("design:paramtypes", [scats.HashSet]), tslib.__metadata("design:returntype", Promise) ], TasksQueueDao.prototype, "nextPending", null); tslib.__decorate([ applicationMetrics.Metric(), tslib.__metadata("design:type", Function), tslib.__metadata("design:paramtypes", [scats.HashSet]), tslib.__metadata("design:returntype", Promise) ], TasksQueueDao.prototype, "peekNextStartAfter", null); tslib.__decorate([ applicationMetrics.Metric(), tslib.__metadata("design:type", Function), tslib.__metadata("design:paramtypes", [Number]), tslib.__metadata("design:returntype", Promise) ], TasksQueueDao.prototype, "finish", null); tslib.__decorate([ applicationMetrics.Metric(), tslib.__metadata("design:type", Function), tslib.__metadata("design:paramtypes", [Number]), tslib.__metadata("design:returntype", Promise) ], TasksQueueDao.prototype, "rescheduleIfPeriodic", null); tslib.__decorate([ applicationMetrics.Metric(), tslib.__metadata("design:type", Function), tslib.__metadata("design:paramtypes", [Number, String, Object]), tslib.__metadata("design:returntype", Promise) ], TasksQueueDao.prototype, "fail", null); tslib.__decorate([ applicationMetrics.Metric(), tslib.__metadata("design:type", Function), tslib.__metadata("design:paramtypes", []), tslib.__metadata("design:returntype", Promise) ], TasksQueueDao.prototype, "failStalled", null); tslib.__decorate([ applicationMetrics.Metric(), tslib.__metadata("design:type", Function), tslib.__metadata("design:paramtypes", []), tslib.__metadata("design:returntype", Promise) ], TasksQueueDao.prototype, "resetFailed", null); tslib.__decorate([ applicationMetrics.Metric(), tslib.__metadata("design:type", Function), tslib.__metadata("design:paramtypes", [Number]), tslib.__metadata("design:returntype", Promise) ], TasksQueueDao.prototype, "clearFinished", null); tslib.__decorate([ applicationMetrics.Metric(), tslib.__metadata("design:type", Function), tslib.__metadata("design:paramtypes", [String, String]), tslib.__metadata("design:returntype", Promise) ], TasksQueueDao.prototype, "statusCount", null); const TASKS_QUEUE_OPTIONS = Symbol("TASKS_QUEUE_OPTIONS"); class TaskDto { id; queue; created; initialStart; started; finished; status; missedRunStrategy; priority; error; backoff; backoffType; timeout; name; startAfter; repeatInterval; repeatType; maxAttempts; attempt; payload; constructor(id, queue, created, initialStart, started, finished, status, missedRunStrategy, priority, error, backoff, backoffType, timeout, name, startAfter, repeatInterval, repeatType, maxAttempts, attempt, payload) { this.id = id; this.queue = queue; this.created = created; this.initialStart = initialStart; this.started = started; this.finished = finished; this.status = status; this.missedRunStrategy = missedRunStrategy; this.priority = priority; this.error = error; this.backoff = backoff; this.backoffType = backoffType; this.timeout = timeout; this.name = name; this.startAfter = startAfter; this.repeatInterval = repeatInterval; this.repeatType = repeatType; this.maxAttempts = maxAttempts; this.attempt = attempt; this.payload = payload; } } class TasksResult { items; total; constructor(items, total) { this.items = items; this.total = total; } } class TaskView { id; queue; created; initialStart; started; finished; status; missedRunStrategy; priority; error; backoff; backoffType; timeout; name; startAfter; repeatInterval; repeatType; maxAttempts; attempt; payload; static fromDto(dto) { const res = new TaskView(); res.id = dto.id; res.queue = dto.queue; res.created = dto.created.getTime(); res.initialStart = dto.initialStart.getTime(); res.started = dto.started.map((d) => d.getTime()).orUndefined; res.finished = dto.finished.map((d) => d.getTime()).orUndefined; res.status = dto.status; res.missedRunStrategy = dto.missedRunStrategy; res.priority = dto.priority; res.error = dto.error.orUndefined; res.backoff = dto.backoff; res.backoffType = dto.backoffType; res.timeout = dto.timeout; res.name = dto.name.orUndefined; res.startAfter = dto.startAfter.map((d) => d.getTime()).orUndefined; res.repeatInterval = dto.repeatInterval.orUndefined; res.repeatType = dto.repeatType.orUndefined; res.maxAttempts = dto.maxAttempts; res.attempt = dto.attempt; res.payload = dto.payload; return res; } } tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", Number) ], TaskView.prototype, "id", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", String) ], TaskView.prototype, "queue", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", Number) ], TaskView.prototype, "created", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", Number) ], TaskView.prototype, "initialStart", void 0); tslib.__decorate([ swagger.ApiProperty({ required: false }), tslib.__metadata("design:type", Number) ], TaskView.prototype, "started", void 0); tslib.__decorate([ swagger.ApiProperty({ required: false }), tslib.__metadata("design:type", Number) ], TaskView.prototype, "finished", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", String) ], TaskView.prototype, "status", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", String) ], TaskView.prototype, "missedRunStrategy", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", Number) ], TaskView.prototype, "priority", void 0); tslib.__decorate([ swagger.ApiProperty({ required: false }), tslib.__metadata("design:type", String) ], TaskView.prototype, "error", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", Number) ], TaskView.prototype, "backoff", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", String) ], TaskView.prototype, "backoffType", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", Number) ], TaskView.prototype, "timeout", void 0); tslib.__decorate([ swagger.ApiProperty({ required: false }), tslib.__metadata("design:type", String) ], TaskView.prototype, "name", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", Number) ], TaskView.prototype, "startAfter", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", Number) ], TaskView.prototype, "repeatInterval", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", String) ], TaskView.prototype, "repeatType", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", Number) ], TaskView.prototype, "maxAttempts", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", Number) ], TaskView.prototype, "attempt", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", Object) ], TaskView.prototype, "payload", void 0); class TasksResultView { items; total; } tslib.__decorate([ swagger.ApiProperty({ type: TaskView, isArray: true }), tslib.__metadata("design:type", Array) ], TasksResultView.prototype, "items", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", Number) ], TasksResultView.prototype, "total", void 0); class QueueStat { queueName; p50; p75; p95; p99; p999; constructor(queueName, p50, p75, p95, p99, p999) { this.queueName = queueName; this.p50 = p50; this.p75 = p75; this.p95 = p95; this.p99 = p99; this.p999 = p999; } } class TasksCount { queueName; status; count; constructor(queueName, status, count) { this.queueName = queueName; this.status = status; this.count = count; } } class QueueStatView { queueName; p50; p75; p95; p99; p999; static fromDto(o) { const res = new QueueStatView(); res.queueName = o.queueName; res.p50 = o.p50; res.p75 = o.p75; res.p95 = o.p95; res.p99 = o.p99; res.p999 = o.p999; return res; } } tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", String) ], QueueStatView.prototype, "queueName", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", Number) ], QueueStatView.prototype, "p50", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", Number) ], QueueStatView.prototype, "p75", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", Number) ], QueueStatView.prototype, "p95", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", Number) ], QueueStatView.prototype, "p99", void 0); tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", Number) ], QueueStatView.prototype, "p999", void 0); class TasksCountView { queueName; status; count; static fromDto(o) { const res = new TasksCountView(); res.queueName = o.queueName; res.status = o.status; res.count = o.count; return res; } } tslib.__decorate([ swagger.ApiProperty(), tslib.__metadata("design:type", String) ], TasksCountView.prototype, "queueName", void 0); tslib.__decorate([ swagger.ApiProperty({ enum: exports.TaskStatus, enumName: "TaskStatus" }), tslib.__metadata("design:type", String) ], TasksCountView.prototype, "status", void 0); tslib.__decorate([ swagger.ApiProperty({ type: "integer" }), tslib.__metadata("design:type", Number) ], TasksCountView.prototype, "count", void 0); class QueuesStat { waitTime; workTime; tasksCount; } tslib.__decorate([ swagger.ApiProperty({ type: QueueStatView, isArray: true }), tslib.__metadata("design:type", Array) ], QueuesStat.prototype, "waitTime", void 0); tslib.__decorate([ swagger.ApiProperty({ type: QueueStatView, isArray: true }), tslib.__metadata("design:type", Array) ], QueuesStat.prototype, "workTime", void 0); tslib.__decorate([ swagger.ApiProperty({ type: TasksCountView, isArray: true }), tslib.__metadata("design:type", Array) ], QueuesStat.prototype, "tasksCount", void 0); class ManageTasksQueueService { pool; constructor(pool) { this.pool = pool; } async findByStatus(params) { const parts = new scats.mutable.ArrayBuffer(); scats.option(params.status).foreach((status) => { parts.append(`status='${status}'`); }); const where = parts.nonEmpty ? `where ${parts.toArray.join(" and ")}` : ""; const res = await this.pool.query(`select * from tasks_queue ${where} order by created desc limit $1 offset $2`, [params.limit, params.offset]); const total = await this.pool.query(`select count(*) as total from tasks_queue ${where}`); const items = scats.Collection.from(res.rows).map((row) => { return new TaskDto(row["id"], row["queue"], row["created"], row["initial_start"], scats.option(row["started"]), scats.option(row["finished"]), row["status"], row["missed_run_strategy"], row["priority"], scats.option(row["error"]), row["backoff"], row["backoff_type"], row["timeout"], scats.option(row["name"]), scats.option(row["start_after"]), scats.option(row["repeat_interval"]), scats.option(row["repeat_type"]), row["max_attempts"], row["attempt"], row["payload"]); }); return new TasksResult(items, total.rows[0]["total"]); } async failedCount() { const res = await this.pool.query(`select count(*) as total from tasks_queue where status = '${exports.TaskStatus.error}'`); return res.rows[0]["total"]; } clearFailed() { return this.pool.query(`delete from tasks_queue where status = '${exports.TaskStatus.error}'`); } async waitTimeByQueue() { const res = await this.pool.query(` SELECT queue, percentile_disc(0.50) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (started - created))) AS p50, percentile_disc(0.75) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (started - created))) AS p75, percentile_disc(0.95) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (started - created))) AS p95, percentile_disc(0.99) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (started - created))) AS p99, percentile_disc(0.999) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (started - created))) AS p999 FROM tasks_queue WHERE started IS NOT NULL and attempt = 1 GROUP BY queue ORDER BY queue `); return scats.Collection.from(res.rows).map((row) => new QueueStat(row["queue"], Number(row["p50"]), Number(row["p75"]), Number(row["p95"]), Number(row["p99"]), Number(row["p999"]))); } async restartFailedTask(taskId) { await this.pool.query(` update tasks_queue set status='${exports.TaskStatus.pending}', attempt=0 where id = $1 and status = '${exports.TaskStatus.error}' `, [taskId]); } async restartAllFailedInQueue(queue) { await this.pool.query(` update tasks_queue set status='${exports.TaskStatus.pending}', attempt=0 where queue = $1 and status = '${exports.TaskStatus.error}' `, [queue]); } async tasksCount() { const res = await this.pool.query(` SELECT queue, status, COUNT(*) AS task_count FROM tasks_queue GROUP BY queue, status ORDER BY queue, status `); return scats.Collection.from(res.rows).map((row) => new TasksCount(row["queue"], exports.TaskStatus[row["status"]], Number(row["task_count"]))); } async workTimeByQueue() { const res = await this.pool.query(` SELECT queue, percentile_disc(0.50) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (finished - started))) AS p50, percentile_disc(0.75) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (finished - started))) AS p75, percentile_disc(0.95) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (finished - started))) AS p95, percentile_disc(0.99) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (finished - started))) AS p99, percentile_disc(0.999) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (finished - started))) AS p999 FROM tasks_queue WHERE started IS NOT NULL AND finished IS NOT NULL GROUP BY queue ORDER BY queue `); return scats.Collection.from(res.rows).map((row) => new QueueStat(row["queue"], Number(row["p50"]), Number(row["p75"]), Number(row["p95"]), Number(row["p99"]), Number(row["p999"]))); } } var TasksQueueModule_1; exports.TasksQueueModule = TasksQueueModule_1 = class TasksQueueModule { moduleRef; constructor(moduleRef) { this.moduleRef = moduleRef; } static forRootAsync(options) { const asyncProviders = this.createAsyncProviders(options); return { module: TasksQueueModule_1, imports: options.imports, providers: [ ...asyncProviders, { provide: TasksQueueDao, inject: [TASKS_QUEUE_OPTIONS], useFactory: (opts) => new TasksQueueDao(opts.db), }, { provide: ManageTasksQueueService, inject: [TASKS_QUEUE_OPTIONS], useFactory: (opts) => new ManageTasksQueueService(opts.db), }, { provide: TasksPoolsService, inject: [TasksQueueDao, ManageTasksQueueService, TASKS_QUEUE_OPTIONS], useFactory: (dao, manageService, opts) => new TasksPoolsService(dao, manageService, scats.option(opts.runAuxiliaryWorker).forall(scats.identity), opts.pools), }, ], exports: [TasksPoolsService, ManageTasksQueueService], }; } static createAsyncProviders(options) { if (options.useExisting || options.useFactory) { return [this.createAsyncOptionsProvider(options)]; } const useClass = options.useClass; return [ this.createAsyncOptionsProvider(options), { provide: useClass, useClass, }, ]; } static createAsyncOptionsProvider(options) { if (options.useFactory) { return { provide: TASKS_QUEUE_OPTIONS, useFactory: options.useFactory, inject: options.inject || [], }; } const inject = [ (options.useClass || options.useExisting), ]; return { provide: TASKS_QUEUE_OPTIONS, useFactory: async (optionsFactory) => await optionsFactory.createTelegrafOptions(), inject, }; } onApplicationBootstrap() { const poolsService = this.moduleRef.get(TasksPoolsService); poolsService.start(); } async onApplicationShutdown() { const poolsService = this.moduleRef.get(TasksPoolsService); await poolsService.stop(TimeUtils.minute); } }; exports.