firelease
Version:
Firebase queue consumer for Node with at-least-once semantics
210 lines (209 loc) • 10.8 kB
TypeScript
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;