import type { BaseAdapter } from '@bull-board/api/baseAdapter'; import type { MetricsClient } from './connection'; import type { MetricsKeys } from './keys'; import type { LatencyStore } from './LatencyStore'; export interface LatencySamplerOptions { redis: MetricsClient; keys: MetricsKeys; store: LatencyStore; /** Recorder tick, used to size the lease and to bound a cold start. */ tickMs: number; /** Above this, the tick subsamples uniformly rather than fetching every job. */ maxSamplesPerTick?: number; /** * How far back from now a scan stops. Defaults to SAFETY_MARGIN_MS. Injectable so tests * can set it to 0 and sample jobs that just finished, rather than sleeping past the * margin in every case. */ safetyMarginMs?: number; /** * Called with anything `sample()` swallows. The default is silent, which keeps a broken * latency scan from taking the counter snapshot down with it, but also makes a collector * that is failing every tick look exactly like an idle board. Supply this to tell the two * apart. Errors thrown by the hook itself are ignored, since a throwing reporter would * undo the containment it was added to observe. */ onError?: (error: unknown, queueName: string) => void; } /** * Copies job durations out of BullMQ's finished sets on the recorder's tick. * * BullMQ's moveToFinished does `ZADD targetSet, timestamp, jobId` and writes the same value * to finishedOn, so the completed and failed sets are sorted sets scored by finish time. * Scanning past a stored watermark therefore returns exactly the jobs finished since the * last tick, with no gaps and no bias. The only loss is a queue whose removeOnComplete * trims faster than the tick runs. */ export declare class LatencySampler { private readonly redis; private readonly keys; private readonly store; private readonly tickMs; private readonly maxSamples; private readonly safetyMarginMs; private readonly onError?; private readonly id; private readonly redisBacked; constructor(opts: LatencySamplerOptions); static supports(adapter: BaseAdapter): boolean; /** * One queue, one tick. Swallows its own errors: the counter snapshot is the more * important metric and must not fail as collateral damage from a latency scan. Pass * `onError` to see what was swallowed; without it a collector failing every tick is * indistinguishable from a queue with nothing to sample. */ sample(adapter: BaseAdapter): Promise; /** `getQueueKey` answers for a PostgreSQL-backed queue too, with keys no Redis holds. */ private isRedisBacked; /** * Increments are not idempotent the way the counter upsert is, so two recorders scanning * the same range would double every histogram. Only the lease holder scans. * * The TTL is a crash ceiling, not the normal lifetime: a process that dies mid-scan must * not lock the queue out forever. The normal path releases in a finally, because a lease * outliving its scan would make the next tick no-op and halve the sampling rate. */ private acquireLease; /** Compare and delete, so a lease that already expired and was retaken is left alone. */ private releaseLease; /** * Bounded rather than eternal: an unexpiring watermark would leave one key behind per * queue forever once that queue is purged or decommissioned, which is exactly the * unbounded-storage failure this package exists to avoid. The day retention is already the * horizon everything else here is bounded by. Losing the watermark just means the next * tick cold starts, which is already a supported path. */ private watermarkTtlSeconds; private sampleDurations; private flush; /** * The backlog is not all in one place. `wait` holds it while the queue is running, but * pausing RENAMEs that list to `paused` and routes new jobs there, and anything added with * a priority goes to the `prioritized` sorted set instead, which can leave `wait` * permanently empty. Reading only `wait` reports a healthy zero for a queue that is badly * backed up, which is the opposite of what this gauge is for, so all three are consulted * and the worst age wins. */ private sampleQueueAge; }