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.

212 lines (211 loc) 10.3 kB
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>; }