@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.
99 lines (98 loc) • 4.94 kB
JavaScript
import { Collection, mutable, option } from "scats";
import { TaskStatus } from "./tasks-model.js";
import { QueueStat, TaskDto, TasksCount, TasksResult } from "./manage.model.js";
export class ManageTasksQueueService {
pool;
constructor(pool) {
this.pool = pool;
}
async findByStatus(params) {
const parts = new mutable.ArrayBuffer();
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 = Collection.from(res.rows).map((row) => {
return new TaskDto(row["id"], row["queue"], row["created"], row["initial_start"], option(row["started"]), option(row["finished"]), row["status"], row["missed_run_strategy"], row["priority"], option(row["error"]), row["backoff"], row["backoff_type"], row["timeout"], option(row["name"]), option(row["start_after"]), option(row["repeat_interval"]), 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 = '${TaskStatus.error}'`);
return res.rows[0]["total"];
}
clearFailed() {
return this.pool.query(`delete
from tasks_queue
where status = '${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 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='${TaskStatus.pending}',
attempt=0
where id = $1
and status = '${TaskStatus.error}'
`, [taskId]);
}
async restartAllFailedInQueue(queue) {
await this.pool.query(`
update tasks_queue
set status='${TaskStatus.pending}',
attempt=0
where queue = $1
and status = '${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 Collection.from(res.rows).map((row) => new TasksCount(row["queue"], 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 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"])));
}
}