/// import { CompiledSharedOptions, EnhancedWithPgClient, LocalQueueMode, WorkerPoolOptions } from "."; import { Deferred } from "./deferred"; import { GetJobFunction, Job, TaskList, WorkerPool } from "./interfaces"; /** * The local queue exists to reduce strain on the database; it works by * fetching a batch of jobs from the database and distributing them to workers * as and when necessary. It is also responsible for polling when in use, * relieving the workers of this responsibility. * * The local queue trades latency for throughput: jobs may sit in the local * queue for a longer time (maximum `localQueue.size` jobs waiting maximum * `localQueue.ttl` milliseconds), but fewer requests to the database are made * for jobs since more jobs are fetched at once, enabling the worker to reach * higher levels of performance (and reducing read stress on the DB). * * The local queue is always in one of these modes: * * - STARTING mode * - POLLING mode * - WAITING mode * - TTL_EXPIRED mode * - RELEASED mode * * ## STARTING mode * * STARTING mode is the initial state of the local queue. * * Immediately move to POLLING mode. * * ## POLLING mode * * The queue will only be in POLLING mode when it contains no cached jobs. * * When the queue enters POLLING mode: * * - if any refetch delay has expired it will trigger a fetch of jobs from the * database, * - otherwise it will trigger a refetch to happen once the refetch delay has * completed. * * When jobs are fetched: * * - if fewer than `Math.ceil(Math.min(localQueueRefetchDelay.threshold, localQueueSize))` * jobs were returned then a refetch delay will be set (if configured). * - if jobs were returned then it will supply as many as possible to any * waiting workers (`workerQueue`) * - if all workers are busy and jobs still remain it will store them to * `jobQueue` and immediately enter WAITING mode * - otherwise (if no jobs remain: `jobQueue` is empty) we'll wait * `pollInterval` ms and then fetch again. * * When a "new job" notification is received, once any required refetch delay * has expired (or immediately if it has already expired) the timer will be * cancelled, and a fetch will be fired immediately. * * ## WAITING mode * * The local queue can only be in WAITING mode if there are cached jobs. * * Any waiting clients are issued any available cached jobs. * * If no cached jobs remain, then the local queue enters POLLING mode, * triggering a fetch. * * If cached jobs remain (even if there's just one, even if it has been 30 * minutes since the last fetch) then the local queue continues to wait for * a worker to claim the remaining jobs. Once no jobs remain, the local queue * reverts to POLLING mode, triggering a fetch. * * In WAITING mode, all "new job" announcements are ignored. * * The local queue can be in WAITING mode for at most `getJobBatchTime` * milliseconds (default: 30 minutes), after which all unclaimed jobs are * returned to the pool and the local queue enters TTL_EXPIRED mode. * * ## TTL_EXPIRED mode * * This mode is used when jobs were queued in WAITING mode for too long. The * local queue will sit in TTL_EXPIRED mode until a worker asks for a job, * whereupon the local queue will enter POLLING mode (triggering a fetch). * * ## RELEASED mode * * Triggered on shutdown. */ export declare class LocalQueue { private readonly ctx; private readonly tasks; private readonly withPgClient; readonly workerPool: WorkerPool; /** How many jobs to fetch at once */ private readonly getJobBatchSize; /** * If false, exit once the DB seems to have been exhausted of jobs, even if * for just a moment. (I.e. `runOnce()`) */ private readonly continuous; private readonly onMajorError; /** * The configured time (in milliseconds) that a job may sit unclaimed in the * local queue before being returned to the database. */ readonly ttl: number; /** * The time interval (in milliseconds) between fetch requests when in * `POLLING` mode. */ readonly pollInterval: number; /** * The jobs that have been pulled from the database that are waiting for a * worker to claim them. Once claimed, a job will be removed from this list. * This should be empty in POLLING and TTL_EXPIRED modes. */ readonly jobQueue: Job[]; /** * Workers waiting for jobs are represented by deferred promises in this * list. When a job becomes available, first it attempts to satisfy one of * these from the workerQueue, and only if this is empty does it then add the * job to the `jobQueue`. */ readonly workerQueue: Deferred[]; /** * Are we currently fetching jobs from the DB? Prevents double-fetches. */ fetchInProgress: boolean; /** * When we enter WAITING mode (i.e. there are jobs in `jobQueue`), we set up * this timer. When the timer fires, we will release any remaining jobs in * jobQueue back to the database (and enter TTL_EXPIRED mode). Note: all jobs * are fetched at once, and no further jobs are fetched, so the TTL for all * jobs will expire at the same time - we'll only return to POLLING mode once * all jobs have been executed. */ ttlExpiredTimer: NodeJS.Timeout | null; /** * The timer associated with the next fetch poll (see also `pollInterval`). */ fetchTimer: NodeJS.Timeout | null; /** * Should we fetch again once the current fetch is complete? This is * generally used to indicate that we received a "new job" notification (the * queue is "pulsed") whilst we were already fetching, so our fetch may not * have included that job. */ fetchAgain: boolean; /** * The mode that the queue is in; must only be changed via `setMode`, which * itself must only be called by the `setMode*()` methods. */ readonly mode: LocalQueueMode; /** * The promise that resolves/rejects when the local queue has been released. * Will not resolve until all locally queued jobs have been returned to the * pool (or may reject if this process fails) and all active fetches and * other background tasks are complete. This is important, otherwise we might * release the pg.Pool that we're using before jobs are returned to the * database, which would be something we couldn't recover from! * * If it rejects, may reject with a regular Error or an AggregateError * representing multiple failures. */ private _finPromise; /** * Errors that occurred causing the shutdown or during the shutdown of this * local queue instance. */ private errors; /** * A count of the number of "background" processes such as fetching or * returning jobs such that we can avoid exiting until all background tasks * have completed. */ private backgroundCount; /** * If `localQueueRefetchDelay` is configured; set this true if the fetch * resulted in a queue size lower than the threshold. */ private refetchDelayActive; /** * If true, when the refetch delay expires in POLLING mode (or when we next * enter POLLING mode after it expires), immediately trigger a fetch. If * false, just wait for the regular POLLING timeouts. */ private refetchDelayFetchOnComplete; /** The timer tracking when the refetch delay has expired. */ private refetchDelayTimer; /** * The number of new jobs received during the fetch or the resulting refetch * delay; see also `refetchDelayAbortThreshold`. */ private refetchDelayCounter; /** * A random number between 0 and either * `preset.worker.localQueue.refetchDelay.maxAbortThreshold` or * `5*preset.worker.localQueue.size`; when we've been informed of this many * jobs via pulse(), we must abort the refetch delay and trigger an immediate * fetch. */ private refetchDelayAbortThreshold; constructor(ctx: CompiledSharedOptions, tasks: TaskList, withPgClient: EnhancedWithPgClient, workerPool: WorkerPool, /** How many jobs to fetch at once */ getJobBatchSize: number, /** * If false, exit once the DB seems to have been exhausted of jobs, even if * for just a moment. (I.e. `runOnce()`) */ continuous: boolean, onMajorError: (e: unknown) => void); /** * Only call this from `setMode*()` helpers. */ private setMode; /** * Called when the LocalQueue is completely finished and released: no * background tasks, no jobs in job queue. Resolves (or rejects) * `_finPromise`. */ private fin; private decreaseBackgroundCount; private decreaseBackgroundCountWithError; /** * Track promises that happen in the background, but that we want to ensure are * handled before we release the queue (so that the database pool isn't * released too early). * * IMPORTANT: never raise an error from background unless mode === "RELEASED" - you * need to handle errors yourself! */ private background; private setModePolling; private setModeWaiting; private setModeTtlExpired; private returnJobs; private receivedJobs; private fetch; private _fetch; private refetchDelayCompleteOrAbort; /** * If no refetch delay is active, returns false; otherwise returns true and * checks to see if we need to abort the delay and trigger a fetch. */ private handleCheckRefetchDelayAbortThreshold; /** Called when a new job becomes available in the DB */ pulse(count: number): void; getJob: GetJobFunction; release(): Deferred; private setModeReleased; }