UNPKG

firelease

Version:

Firebase queue consumer for Node with at-least-once semantics

210 lines (209 loc) 10.8 kB
import NodeFire from 'nodefire'; import { FireleaseStats, type QueueSourceMode } from './stats'; export declare const TESTABLES: { resetBetweenTests: typeof resetBetweenTests; waitUntilDeleted: typeof waitUntilDeleted; getQueueCheckCooldown: typeof getQueueCheckCooldown; }; declare const RETRY_DIRECTIVE: unique symbol; export type Duration = number | string; export interface Lease { expiry?: number; time?: number; attempts?: number; initial?: number; busy?: boolean; timeNeeded?: number; extendLeasePromise?: Promise<void>; } export type AcquiredLease = Lease & { expiry: number; time: number; attempts: number; initial: number; readonly firstAcquisition?: true; }; export interface LeaseItem { _lease?: Lease; [key: string]: any; } export interface RetryDirective { readonly [RETRY_DIRECTIVE]: true; } export interface WorkerItem extends LeaseItem { _lease: AcquiredLease; readonly $ref: NodeFire; readonly $leaseTimeRemaining: number; } export type FireleaseErrorLevel = 'fatal' | 'error' | 'warning' | 'log' | 'info' | 'debug'; export interface FireleaseErrorDetails { cause?: string; code?: string; count?: number; delta?: number; description?: string; itemKey?: string; listenerLimit?: number; liveCount?: number; mode?: QueueSourceMode; phase?: string; queue?: string; reason?: string; source?: string; timeNeeded?: Duration; timeout?: Duration; } export interface FireleaseError extends Error { firelease?: FireleaseErrorDetails; level?: FireleaseErrorLevel; } export type LeaseTransactionOutcome = 'acquired' | 'contended' | 'failed'; export type CaptureLeaseTransactionMetrics = (outcome: LeaseTransactionOutcome, tries: number, duration: number) => undefined; export interface QueueOptions { maxConcurrent?: number; bufferSize?: number; minLease?: Duration; maxLease?: Duration; healthyPingLatency?: Duration; preprocess?: (item: LeaseItem) => LeaseItem; captureLeaseTransactionMetrics?: CaptureLeaseTransactionMetrics; } export type PingReport = FireleaseStats; export type QueueRef = NodeFire | NodeFire[]; export type WorkerResult = RetryDirective | Duration | Lease | null | void | ((item: LeaseItem) => RetryDirective | Duration | Lease | null | void); export type Worker = (item: WorkerItem) => WorkerResult | PromiseLike<WorkerResult>; export interface FireleaseSettings { globalMaxConcurrent: number; safeQueueSize: number; queueCheckInterval: Duration; queueLoadTimeout: Duration; captureError: (error: FireleaseError) => void; } export interface FireleaseApi { readonly RETRY: RetryDirective; readonly settings: FireleaseSettings; readonly defaults: QueueOptions; readonly stats: FireleaseStats; attachWorker: { (refOrRefs: QueueRef, worker: Worker): void; (refOrRefs: QueueRef, options: QueueOptions, worker: Worker): void; }; pingQueues(callback?: ((report: PingReport) => void) | null, interval?: Duration): void; extendLease(item: WorkerItem, timeNeeded: Duration): Promise<void>; blacklist(taskKey: string): boolean; shutdown(): Promise<void>; listTasksInProgress(): string[]; } /** * Return this from a worker to retry after the current lease expires, and to reset the lease * backoff to zero. */ export declare const RETRY: RetryDirective; /** Default option values for all subsequent attachWorker calls. */ export declare const defaults: QueueOptions; export declare const stats: FireleaseStats; export declare const settings: FireleaseSettings; declare const firelease: FireleaseApi; declare function getQueueCheckCooldown(previousDuration: number): number; /** * Attaches a worker function to consume tasks from a queue. You should normally attach no more * than one worker per path in any given process, but it's OK to run multiple processes on the same * paths concurrently. * * All durations can be specified as either a human-readable string, or a number of milliseconds. * * @param {NodeFire | NodeFire[]} refOrRefs One or more NodeFire refs to queue roots in Firebase. * Individual tasks will be children of these roots and must be objects. All refs form one * logical queue and share the same worker and concurrency limits. The '_lease' key is * reserved for use by Firelease in each task. * @param {Object} options Optional options, supporting the following values: * maxConcurrent: {number} max number of tasks to handle concurrently for this worker. * bufferSize: {number} upper bound on how many tasks to keep buffered from each source and * potentially go through leasing transactions in parallel. It defaults to `Infinity`, * which is preferred for efficiency and correctness unless the queue will usually remain * above `settings.safeQueueSize`. `Infinity` adapts between a full listener and a safe * listener limited to `settings.safeQueueSize` tasks. Use a finite value only to keep an * ordinarily large queue permanently on a limited listener. An explicit finite value is * used as-is and may be greater than `settings.safeQueueSize`. * minLease: {number | string} minimum duration of each lease, which should equal the maximum * expected time a worker will take to handle a task. * maxLease: {number | string} maximum duration of each lease; the lease duration is doubled * each time a task fails until it reaches maxLease. * preprocess: {function(Object):Object} a function to use to preprocess each item during the * leasing transaction. This function must be fast, synchronous, idempotent, and * should return the modified item (passed as the sole argument, OK to mutate). One use * for preprocessing is to clean up items written to a queue by a process outside your * control (e.g., webhooks). * healthyPingLatency: {number | string} the maximum response latency to pings that is * considered "healthy" for this queue. * captureLeaseTransactionMetrics: {function(string, number, number)} a callback invoked * after each acquired, contended, or failed task lease transaction with its outcome, * NodeFire transaction tries, and duration in milliseconds. The callback must be * synchronous. * @param {function(Object):RETRY | number | string | undefined} worker The worker function that * handles enqueued tasks. It will be given a task object as argument, with a special $ref * attribute set to the Nodefire ref of that task. On a task's first acquisition its _lease * also has a non-enumerable firstAcquisition property set to true; it is not saved to * Firebase and is absent on subsequent acquisitions. The worker can perform arbitrary * computation whose duration should not exceed the queue's minLease value. It can * manipulate the task itself in Firebase as well, e.g. to delete it (to get at-most-once * queue semantics) or otherwise modify it. The worker can return any of the following: * * undefined or null to cause the task to be retired from the queue. * * firelease.RETRY to cause the task to be retried after the current lease expires (and * reset the lease backoff counter). * * A duration after which the task should be retried relative to when it was started. * * An epoch in milliseconds greater than 1000000000000 at which the task should be tried. * * A complete _lease object, to be saved as-is. * * A function that takes the task as argument and returns one of the values above. This * function will be executed in a transaction to ensure atomicity. * All of these values can also be wrapped in a promise. */ export declare function attachWorker(refOrRefs: QueueRef, worker: Worker): void; export declare function attachWorker(refOrRefs: QueueRef, options: QueueOptions, worker: Worker): void; /** * Sets up regular pinging of all queues. Can be called either before or after workers are * attached, and will always ping all queues. Can be called more than once to change the * parameters. * * All durations can be specified as either a human-readable string, or a number of milliseconds. * * @param {Function(Object) | null} callback The callback to invoke with the live `stats` object * each time we ping all the queues. It retains the existing global fields and adds * structured results for every logical queue and physical source. If not specified, * reports are silently dropped. * @param {number | string} interval The interval at which to ping queues, to both check the * current response latency and make sure no tasks are stuck. Defaults to 1 minute. */ export declare function pingQueues(callback?: ((report: PingReport) => void) | null, interval?: Duration): void; declare function waitUntilDeleted(ref: NodeFire, timeout: number): Promise<void>; /** * Extends the lease on a task to give the worker more time to finish. Checks a bunch of validity * constraints along the way and throws an error if the worker needs to abort. * * All durations can be specified as either a human-readable string, or a number of milliseconds. * * @param {Object} item The original task object provided to a worker function. * @param {number | string} timeNeeded The minimum time needed counting from the current time. The actual lease may be extended by up to twice this amount, to prevent excessive churn. * @return {Promise} A promise that will be resolved when the lease has been extended, and rejected * if something went wrong and the worker should abort. */ export declare function extendLease(item: WorkerItem, timeNeeded: Duration): Promise<void>; /** * Blacklist the given task key from ever being processed again. * @param {string} taskKey The task key to blacklist. This is the full Firebase URL of the task and * can be obtained from an error using `error.firelease.itemKey`. * @return {boolean} True if the task key was added to the list, false if it was already present. */ export declare function blacklist(taskKey: string): boolean; /** * Shuts down firelease by refusing to take new tasks. * @return {Promise<void>} A promise that resolves when the shutdown is complete. */ export declare function shutdown(): Promise<void>; /** * Lists the URLs of all tasks that are currently being worked on. */ export declare function listTasksInProgress(): string[]; declare function resetBetweenTests(): void; export default firelease;