import { EventEmitter } from 'events'; import { DependenciesOpts, IQueueBackend, JobJson, MinimalJob, MoveToDelayedOpts, MoveToWaitingChildrenOpts, ParentKeyOpts, QueueBaseOptions, RepeatableOptions, RetryJobOpts, RetryOptions, StreamReadRaw } from '../interfaces'; import { FinishedStatus, JobProgress, JobsOptions, JobState, JobType, KeepJobs } from '../types'; import { KeysMap } from '../classes/queue-keys'; import { PostgresConnection } from './postgres-connection'; /** * PostgreSQL implementation of {@link IQueueBackend}. * * Fulfils the same database-agnostic contract as {@link RedisQueueBackend}, but * backed by a PostgreSQL database: queue operations are expressed as SQL / * PL/pgSQL functions (created by the migrations), job state lives in a single * `job` table keyed by `(queue, id)` with a `state` column and partial * indexes, claiming uses `FOR UPDATE SKIP LOCKED`, and the blocking * "wait for job" primitive uses `LISTEN`/`NOTIFY`. * * The class owns its {@link PostgresConnection}; the high-level classes (Queue, * Worker, FlowProducer) depend only on {@link IQueueBackend} and never touch a * `pg` client directly. */ export declare class PostgresQueueBackend extends EventEmitter implements IQueueBackend { connection: PostgresConnection; protected readonly queueName: string; protected readonly opts: QueueBaseOptions; protected readonly ownsConnection: boolean; /** * When set, the name applied to this backend's dedicated connection (its * `application_name`) so getWorkers can discover it — the PostgreSQL * analogue of the Redis worker's named blocking connection. Only workers * pass it; QueueEvents name themselves via {@link setName}. */ private readonly listenClientName?; closing: Promise | undefined; /** * The PostgreSQL schema (namespace) this backend's queue lives in, taken from * the connection. All runtime SQL is qualified with it. BullMQ's per-queue * `prefix` is a Redis keyspace concern and is intentionally not part of the * SQL data model. */ protected readonly schema: string; /** Whether the dedicated LISTEN client is subscribed to the jobs channel. */ private listening; /** Whether the dedicated LISTEN client is subscribed to the events channel. */ private listeningEvents; /** * Memoizes {@link PostgresQueueBackend.waitUntilReady} so every caller awaits * the same readiness — including the one-time connection naming it performs. */ private readyPromise; /** Cancels the in-flight {@link waitForJob}, if any (used by close/interrupt). */ private cancelWait; /** * Set by {@link disconnectBlocking} to interrupt the blocking wait. Unlike * {@link cancelWait} (which only fires the *current* wait), this flag also * short-circuits a {@link waitForJob} that starts during/after the disconnect * — closing the race where the worker re-enters `waitForJob` (still awaiting * `ensureListening`) just as `close()` interrupts it, leaving it blocked on a * timer that, under faked timers, never fires. Cleared by * {@link reconnectBlocking}. (The Redis backend gets this for free: tearing * down the blocking socket interrupts even a freshly-issued `BZPOPMIN`.) */ private blockingDisconnected; /** Cancels the in-flight {@link readEvents} wait, if any. */ private cancelEventWait; constructor(connection: PostgresConnection, queueName: string, opts: QueueBaseOptions, ownsConnection?: boolean, /** * When set, the name applied to this backend's dedicated connection (its * `application_name`) so getWorkers can discover it — the PostgreSQL * analogue of the Redis worker's named blocking connection. Only workers * pass it; QueueEvents name themselves via {@link setName}. */ listenClientName?: string); waitUntilReady(): Promise; close(force?: boolean): Promise; disconnect(): Promise; setName(name: string): Promise; /** * PostgreSQL `LISTEN`/`NOTIFY` has no minimum block granularity, so any * positive timeout is fine; we mirror the Redis backend's smallest unit. */ get minimumBlockTimeout(): number; forQueue(queueName: string, _prefix?: string): IQueueBackend; /** * The queue's qualified name. With a schema-based namespace there is no * prefix, so the qualified name is simply the queue name. */ get qualifiedName(): string; /** * Backends that don't address jobs by key return an empty map; PostgreSQL * addresses rows by `(queue, id)` columns instead. */ get keys(): KeysMap; /** * Builds a namespaced identifier of the given `type` (`":"`), * used e.g. for flow dependency identifiers. No prefix is involved. */ toKey(type: string): string; /** * Parses a PostgreSQL flow child key (`":"`) into its components. * There is no keyspace prefix, so `prefix` is always empty. Inverse of * {@link toKey}. */ parseNodeKey(key: string): { prefix: string; queueName: string; id: string; }; /** * Returns a backend identifier used by the generic API; PostgreSQL discovery * relies on {@link setName} setting `application_name` on the dedicated * LISTEN client. */ clientName(suffix?: string): string; /** * Runs a query on the connection's pool, first awaiting the connection's * (memoized) readiness so the schema/functions exist. This mirrors how the * ioredis client buffers commands until connected, letting callers (e.g. a * Worker's autorun loop) issue operations before `waitUntilReady` resolves. */ private query; /** * Loads a named `.sql` command file and runs it. The files contain no * schema/namespace references — the connection's `search_path` selects the * namespace — so they are portable verbatim to the other language ports. */ private run; /** * The processing worker's name (when this backend belongs to a Worker), used * to stamp `processedBy` on the next job fetched during a finish op. */ private get workerName(); /** * Re-throws a finish-op error (SQLSTATE `BM001`, whose DETAIL carries the * numeric `ErrorCode`) as the shared canonical error; passes anything else * through unchanged. */ private mapFinishError; addJob(job: JobJson, jobId: string, parentKeyOpts?: ParentKeyOpts): Promise; addJobs(entries: { job: JobJson; jobId: string; parentKeyOpts?: ParentKeyOpts; }[]): Promise; /** Builds one entry of the JSONB batch consumed by `add_flow`. */ private toBatchEntry; addFlow(entries: { jobData: JobJson; jobId: string; parentKeyOpts: ParentKeyOpts; prefix: string; queueName: string; }[]): Promise<[Error | null, string | number][]>; addJobScheduler(jobSchedulerId: string, nextMillis: number, templateData: string, templateOpts: JobsOptions, opts: RepeatableOptions, delayedJobOpts: JobsOptions, producerId?: string): Promise<[string, number]>; moveToActive(token: string, name?: string): Promise; /** * Shapes a job-claim result (from `move_to_active` or the fused finish+fetch) * into the worker's `[jobData, id, rateLimitDelay, delayUntil]` tuple. When no * job was claimed, a follow-up `next_signal` reports the rate-limit ttl or the * next delayed wake-up so the worker can block until then. */ private buildNextJobResult; moveToCompleted(job: MinimalJob, returnValue: R, removeOnComplete: boolean | number | KeepJobs, token: string, fetchNext: boolean): Promise<{ result: void | any[]; finishedOn: number; }>; moveToFailed(job: MinimalJob, failedReason: string, removeOnFail: boolean | number | KeepJobs, token: string, fetchNext: boolean, fieldsToUpdate?: Record): Promise<{ result: void | any[]; finishedOn: number; }>; moveToDelayed(jobId: string, timestamp: number, delay: number, token?: string, opts?: MoveToDelayedOpts): Promise; moveToWaitingChildren(jobId: string, token: string, _opts?: MoveToWaitingChildrenOpts): Promise; moveJobFromActiveToWait(jobId: string, token?: string): Promise; retryJob(jobId: string, lifo: boolean, token?: string, opts?: RetryJobOpts): Promise; retryFinishedJob(job: MinimalJob, state: 'failed' | 'completed', opts?: RetryOptions): Promise; promote(jobId: string): Promise; moveStalledJobsToWait(): Promise; retryFinishedJobs(state?: FinishedStatus, count?: number, timestamp?: number): Promise; promoteJobs(count?: number): Promise; pause(pause: boolean): Promise; drain(delayed: boolean): Promise; cleanJobsByState(state: string, timestamp: number, limit?: number): Promise; obliterate(opts: { force: boolean; count: number; }): Promise; /** * Removes orphaned job hashes (job data present but not referenced by any * state set). This is a Redis keyspace-maintenance concern: on PostgreSQL a * job is a single relational row inserted transactionally with its state, so * orphans cannot exist and there is nothing to remove. Always returns 0. */ removeOrphanedJobs(_count?: number, _limit?: number): Promise; extendLock(jobId: string, token: string, duration: number): Promise; extendLocks(jobIds: string[], tokens: string[], duration: number): Promise; updateData(job: MinimalJob, data: T): Promise; updateProgress(jobId: string, progress: JobProgress): Promise; addLog(jobId: string, logRow: string, keepLogs?: number): Promise; clearLogs(jobId: string, keepLogs?: number): Promise; changeDelay(jobId: string, delay: number): Promise; changePriority(jobId: string, priority?: number, lifo?: boolean): Promise; remove(jobId: string, removeChildren: boolean): Promise; removeUnprocessedChildren(jobId: string): Promise; removeChildDependency(jobId: string, parentKey: string): Promise; removeDeduplicationKey(deduplicationId: string, jobId: string): Promise; deleteDeduplicationKey(deduplicationId: string): Promise; updateJobSchedulerNextMillis(jobSchedulerId: string, nextMillis: number, templateData: string, delayedJobOpts: JobsOptions, producerId?: string): Promise; removeJobScheduler(jobSchedulerId: string): Promise; getJobScheduler(id: string): Promise<[any, string | null]>; isJobScheduler(id: string): Promise; getJobSchedulerData(key: string): Promise>; getJobSchedulersRange(start: number, end: number, asc: boolean): Promise; getJobSchedulersCount(): Promise; getState(jobId: string): Promise; isFinished(jobId: string, returnValue?: boolean): Promise; isMaxed(): Promise; isJobInState(state: string, jobId: string): Promise; getJobData(jobId: string): Promise; getDeduplicationJobId(deduplicationId: string): Promise; getJobLogs(jobId: string, start: number, end: number, asc: boolean): Promise<{ logs: string[]; count: number; }>; getRateLimitTtl(maxJobs?: number): Promise; getCounts(types: JobType[]): Promise; getCountsPerPriority(priorities: number[]): Promise; getRanges(types: JobType[], start?: number, end?: number, asc?: boolean): Promise<[string][]>; getDependencyCounts(jobId: string, types: string[]): Promise; getDependencies(jobId: string, opts: DependenciesOpts): Promise<{ nextFailedCursor?: number; failed?: string[]; nextIgnoredCursor?: number; ignored?: Record; nextProcessedCursor?: number; processed?: Record; nextUnprocessedCursor?: number; unprocessed?: string[]; }>; getProcessedChildrenValues(jobId: string): Promise>; getIgnoredChildrenFailures(jobId: string): Promise>; /** * Records one finished job into the per-minute metrics for the given `kind`, * when the worker was created with a `metrics.maxDataPoints`. Mirrors the * `collectMetrics` step of Redis's moveToFinished; kept as a separate query * (metrics are best-effort, so strict atomicity with the finish is not * required). */ private collectMetrics; getMetrics(type: 'completed' | 'failed', start?: number, end?: number): Promise<[string[], string[], number]>; getClientList(): Promise; paginate(key: string, opts: { start: number; end: number; fetchJobs?: boolean; }): Promise<{ cursor: string; items: { id: string; v?: any; err?: string; }[]; total: number; jobs?: JobJson[]; }>; setQueueMeta(values: Record): Promise; getQueueMetaField(field: string): Promise; getQueueMetaFields(fields: string[]): Promise<(string | null)[]>; getQueueMeta(): Promise>; removeQueueMetaFields(fields: string[]): Promise; hasQueueMetaField(field: string): Promise; setRateLimit(expireTimeMs: number): Promise; removeRateLimitKey(): Promise; removeDeprecatedPriorityKey(): Promise; trimEvents(_maxLength: number): Promise; publishEvent(fields: Record, _maxEvents: number): Promise; readEvents(id: string, blockTimeout: number): Promise; private fetchEvents; /** The shared notify channel all producers post to (see `add_job`). */ private static readonly NOTIFY_CHANNEL; /** The shared event-stream channel (see `publish_event`). */ private static readonly EVENTS_CHANNEL; /** Subscribes the dedicated client to the shared jobs channel (once). */ private ensureListening; /** Subscribes the dedicated client to the shared events channel (once). */ private ensureListeningEvents; /** * Blocks (up to `blockTimeout` ms) until a new event is published for this * queue (via `LISTEN`/`NOTIFY` on the events channel), or the timeout * elapses. Used by {@link readEvents} between polls. */ private waitForEvent; /** * Blocks (up to `blockTimeout` seconds) until a job for this queue may be * available, via `LISTEN`/`NOTIFY`. Producers notify the shared `bullmq_jobs` * channel with the queue name as payload (in `add_job`), so a producer * in any process wakes a blocked worker immediately. Returns a marker * (`score` 0 = "check now") or `null` on timeout. The Redis backend * implements this with `BZPOPMIN`. */ waitForJob(blockTimeout: number): Promise<{ member: string; score: number; } | null>; disconnectBlocking(_wait?: boolean): Promise; reconnectBlocking(): Promise; }