@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.
212 lines (211 loc) • 10.3 kB
TypeScript
import { Collection, HashSet, Option } from "scats";
import pg from "pg";
import type { ScheduleCronTaskDetails, SchedulePeriodicTaskDetails, ScheduleTaskDetails, SpawnChildTaskDetails } from "./tasks-model.js";
import { ScheduledTask, TaskStateSnapshot, TaskPeriodType, TaskStatus } from "./tasks-model.js";
export declare class TasksQueueDao {
private readonly pool;
constructor(pool: pg.Pool);
/**
* Insert a one-time task using an existing database client.
*
* This helper is shared by regular scheduling and parent-child orchestration flows,
* so child tasks can be created atomically together with parent status updates.
*
* @param cl active database client
* @param task one-time task parameters
* @param parentId optional parent task id
* @returns created task id if insert succeeded
*/
private insertOneTimeTask;
private withClient;
/**
* Add new task to a queue, that will be executed once upon successful competition.
* @param task parameters of the new task
* @return the id of the created task
*/
schedule(task: ScheduleTaskDetails, now?: Date): Promise<Option<number>>;
/**
* Atomically transition a parent task into `blocked` state and create its child task.
*
* This method is intended for one-time parent tasks only. Parent blocking and child creation
* happen in the same transaction, so the system never observes a blocked parent without a child
* or a child without its parent being blocked.
*
* @param parentTaskId parent task currently being processed
* @param childTask one-time child task details
* @returns created child task id
*/
blockParentAndScheduleChild(parentTaskId: number, childTask: SpawnChildTaskDetails, parentPayload: object, expectedStarted: Date, now?: Date): Promise<Option<number>>;
/**
* Add a new periodic task to the queue.
*
* Supported periodic modes:
* - `fixed_rate`: uses `period` in milliseconds.
* - `fixed_delay`: uses `period` in milliseconds.
* - `cron`: uses `cronExpression`.
*
* Cron expressions support both common formats:
* - 5-field format: `minute hour day-of-month month day-of-week`
* - 6-field format: `second minute hour day-of-month month day-of-week`
*
* @param task parameters of the periodic task
* @param periodType repeat type that defines how next execution is calculated
* @return the id of the created task or none if task was not scheduled (e.g. already exists)
*/
schedulePeriodic(task: SchedulePeriodicTaskDetails | ScheduleCronTaskDetails, periodType: TaskPeriodType, now?: Date): Promise<Option<number>>;
/**
* Fetches a new task to be processed from one of the specified queues.
*
* Conditions for fetching:
* - Task must have status = 'pending'
* - Task must belong to one of the specified queues
* - Task must have attempt count < max_attempts (default is 1 if unset)
* - Task must have start_after <= now() or be null
*
* Among the matching tasks, the one with the highest priority will be selected.
* If multiple tasks share the same priority, the one with the smallest id will be chosen.
*
* When fetched, the task's status will be set to 'in_progress', 'started' timestamp will be updated,
* and the attempt count will be incremented.
*
* @param queueNames the queues where the tasks are searched.
* @return the brief details of the fetched task, if any
*/
nextPending(queueNames: HashSet<string>, now?: Date): Promise<Option<ScheduledTask>>;
/**
* Load a minimal persistent snapshot of a task by id.
*
* The returned shape is intentionally compact and suitable for orchestration logic
* that needs to inspect child task status, runtime payload, and final result without
* depending on management APIs.
*
* @param taskId task id to load
* @returns task snapshot if the row exists
*/
findTaskState(taskId: number): Promise<Option<TaskStateSnapshot>>;
ping(taskId: number, expectedStarted: Date, now?: Date): Promise<boolean>;
/**
* Applies timeout failure semantics to a single task if its current attempt is already stalled.
*
* Returns `none` when the task is still healthy and remains `in_progress`.
* Returns the current final status when the task has already timed out or is no longer `in_progress`
* because timeout handling was already applied by another code path.
*/
failIfStalled(taskId: number, expectedStarted: Date, now?: Date): Promise<Option<TaskStatus>>;
/**
* Returns the earliest upcoming `start_after` timestamp from the task queue.
*
* This can be used to calculate how long to sleep before polling again
* without increasing the overall polling frequency.
*
* Only considers tasks with status `'pending'` or `'error'` and where `start_after > now`.
*
* Returns `none` if no such task exists.
*
* @returns The earliest future `start_after` timestamp, if any.
*/
peekNextStartAfter(queueNames: HashSet<string>, now?: Date): Promise<Option<Date>>;
/**
* Mark task as finished. Task status will be set to 'finished'. The 'error' field will be cleared.
* The 'finished' field will be set to current timestamp.
* Task should have status='in_progress' to be updated.
* @param taskId the id of the task
*/
finish(taskId: number, nextPayload?: object, result?: object, expectedStarted?: Date, now?: Date): Promise<boolean>;
/**
* Wake a blocked parent task after its child has reached a terminal state.
*
* The parent is moved back to `pending` and becomes immediately eligible for polling.
* No update is performed if the child has no parent or if the parent is not currently blocked.
*
* @param childTaskId child task that has just completed terminally
* @returns parent id and queue if a blocked parent was woken
*/
wakeParentOnChildTerminal(childTaskId: number, now?: Date): Promise<Option<{
id: number;
queue: string;
}>>;
/**
* Reschedule a periodic task by setting its status back to `pending` and updating `start_after`.
*
* This method runs in a transaction and performs two operations:
* 1) lock and read periodic scheduling parameters (`SELECT ... FOR UPDATE`)
* 2) update task state and `start_after` using the computed next execution time.
*
* Supported periodic sources:
* - `repeat_interval` for fixed rate / fixed delay modes
* - `cron_expression` for cron mode
*
* Cron expressions support both common formats:
* - 5-field format: `minute hour day-of-month month day-of-week`
* - 6-field format: `second minute hour day-of-month month day-of-week`
*
* @param taskId the id of the task
*/
rescheduleIfPeriodic(taskId: number, nextPayload?: object, result?: object, expectedStarted?: Date, now?: Date): Promise<boolean>;
private findPeriodicForReschedule;
/**
* Marks the task as failed.
*
* If the task has remaining attempts (i.e., attempt + 1 < max_attempts), it is rescheduled immediately:
* - Its status is set to 'pending'.
* - The 'start_after' field is set to a future time calculated using the 'backoff' and 'backoff_type' fields.
* - For 'constant' backoff: delay = backoff
* - For 'linear' backoff: delay = backoff * attempt
* - For 'exponential' backoff: delay = backoff * 2^attempt
* - The 'finished' timestamp is updated to the current time.
* - The 'error' field is updated with the provided message.
*
* If no attempts remain, the task is marked as permanently failed:
* - Its status is set to 'error'.
* - The 'start_after' field is cleared (set to NULL).
*
* This update will only take place if the task is currently in 'in_progress' status.
*
* @param taskId The ID of the task.
* @param error The error message to store in the task's 'error' field.
* @param nextPayload Replaces task payload, if set
* @returns The new status of the task after the update (`'pending'` or `'error'`).
*/
fail(taskId: number, error: string, nextPayload?: object, result?: object, expectedStarted?: Date, now?: Date): Promise<Option<TaskStatus>>;
/**
* Process all stalled in-progress tasks.
*
* For each stalled task:
* - if retries remain, it is requeued back to `pending` using the same backoff formula as regular failures
* - otherwise, it is marked as terminal `error` with `Timeout`
*
* If a terminally failed stalled task is a child task, its blocked parent is woken in the same transaction.
*
* To be updated the task should have
* - status='in_progress'
* - defined timeout and greatest(started, coalesce(last_heartbeat, started)) + timeout
* should be less than current timestamp
*
* @return ids of stalled tasks whose status was updated
*/
failStalled(now?: Date): Promise<Collection<number>>;
/**
* Requeues all failed tasks.
* The task should have status='error', the number of attempts for a task should be less
* than a number of maximum allowed attempts (which is set in 'retries' field, defaults to 1).
*
* All suitable tasks will have the 'status' field set to 'pending', the 'finished' field cleared.
*/
resetFailed(): Promise<void>;
/**
* Removes all finished tasks.
* The task should have status='finished' and current timestamp should be greater then
* the time, when the task was finished plus the timeout. Tasks with any unfinished
* ancestor in the parent chain are preserved.
*
* @param timeout the duration between the time, when the task was finished and current timestamp
*/
clearFinished(timeout?: number, now?: Date): Promise<void>;
/**
* Calculates the number of the tasks with specified status in a specified queue.
* @param queue the name of the queue.
* @param status the status of the task to be counted
*/
statusCount(queue: string, status: TaskStatus): Promise<number>;
}