pg-boss 12.35.0 → 12.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,349 @@
1
+ import { context, defaultTextMapGetter, defaultTextMapSetter, metrics, propagation, ROOT_CONTEXT, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
2
+ import packageJson from '../package.json' with { type: 'json' };
3
+ // The OpenTelemetry messaging semantic conventions, copied rather than imported: the messaging
4
+ // attributes still live in the unstable "incubating" entry point of @opentelemetry/semantic-conventions,
5
+ // which is not meant to be depended on by libraries.
6
+ // https://opentelemetry.io/docs/specs/semconv/messaging/
7
+ export const ATTR = {
8
+ messagingSystem: 'messaging.system',
9
+ operationName: 'messaging.operation.name',
10
+ operationType: 'messaging.operation.type',
11
+ destinationName: 'messaging.destination.name',
12
+ messageId: 'messaging.message.id',
13
+ batchMessageCount: 'messaging.batch.message_count',
14
+ errorType: 'error.type',
15
+ dbNamespace: 'db.namespace',
16
+ schema: 'pgboss.schema',
17
+ retryCount: 'pgboss.job.retry_count',
18
+ jobState: 'pgboss.job.state'
19
+ };
20
+ export const METRIC = {
21
+ operationDuration: 'messaging.client.operation.duration',
22
+ sentMessages: 'messaging.client.sent.messages',
23
+ consumedMessages: 'messaging.client.consumed.messages',
24
+ processDuration: 'messaging.process.duration',
25
+ queueJobs: 'pgboss.queue.jobs'
26
+ };
27
+ export const MESSAGING_SYSTEM = 'pg-boss';
28
+ export const INSTRUMENTATION_SCOPE = 'pg-boss';
29
+ // The bucket boundaries the messaging conventions recommend for their duration histograms.
30
+ const DURATION_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10];
31
+ function errorType(err) {
32
+ if (err instanceof Error)
33
+ return err.constructor.name;
34
+ return '_OTHER';
35
+ }
36
+ function spanName(operation, destination) {
37
+ return destination ? `${operation} ${destination}` : operation;
38
+ }
39
+ function metricAttributes(attributes, err) {
40
+ const { [ATTR.messageId]: _id, [ATTR.batchMessageCount]: _count, [ATTR.retryCount]: _retry, ...rest } = attributes;
41
+ return err === undefined ? rest : { ...rest, [ATTR.errorType]: errorType(err) };
42
+ }
43
+ function recordError(span, err) {
44
+ span.setAttribute(ATTR.errorType, errorType(err));
45
+ if (err instanceof Error) {
46
+ span.recordException(err);
47
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
48
+ }
49
+ else {
50
+ span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
51
+ }
52
+ }
53
+ function seconds(startedAt) {
54
+ return (performance.now() - startedAt) / 1000;
55
+ }
56
+ class Telemetry {
57
+ enabled;
58
+ #propagate;
59
+ #propagator;
60
+ #schema;
61
+ #namespace;
62
+ #tracer;
63
+ #meterProvider;
64
+ #queues;
65
+ #instrumentsFor = null;
66
+ #instruments = null;
67
+ #observing = false;
68
+ constructor(options = {}, schema, queues) {
69
+ this.enabled = options.enabled !== false;
70
+ this.#propagate = this.enabled && options.propagateContext === true;
71
+ this.#propagator = options.propagator ?? propagation;
72
+ this.#schema = schema;
73
+ this.#namespace = { [ATTR.schema]: schema };
74
+ this.#tracer = (options.tracerProvider ?? trace.getTracerProvider()).getTracer(INSTRUMENTATION_SCOPE, packageJson.version);
75
+ this.#meterProvider = options.meterProvider;
76
+ this.#queues = queues;
77
+ }
78
+ // db.namespace follows the PostgreSQL convention, {database}|{schema}, so the same schema in two
79
+ // databases stays apart. The database is only known once start() has asked for it.
80
+ setDatabase(database) {
81
+ this.#namespace = { [ATTR.dbNamespace]: `${database}|${this.#schema}`, [ATTR.schema]: this.#schema };
82
+ }
83
+ #baseAttributes(operation, type, destination) {
84
+ const attributes = {
85
+ [ATTR.messagingSystem]: MESSAGING_SYSTEM,
86
+ [ATTR.operationName]: operation,
87
+ [ATTR.operationType]: type,
88
+ ...this.#namespace
89
+ };
90
+ if (destination)
91
+ attributes[ATTR.destinationName] = destination;
92
+ return attributes;
93
+ }
94
+ // The global meter provider has no proxy the way the tracer provider does: a meter taken before
95
+ // an SDK registers stays a no-op forever. Resolving the provider on every use and rebuilding the
96
+ // instruments when it changes is what lets an SDK started after pg-boss still receive metrics.
97
+ #getInstruments() {
98
+ if (!this.enabled)
99
+ return null;
100
+ const provider = this.#meterProvider ?? metrics.getMeterProvider();
101
+ if (provider === this.#instrumentsFor && this.#instruments)
102
+ return this.#instruments;
103
+ this.#instruments?.queueJobs.removeCallback(this.#observeQueues);
104
+ const meter = provider.getMeter(INSTRUMENTATION_SCOPE, packageJson.version);
105
+ this.#instrumentsFor = provider;
106
+ this.#instruments = {
107
+ operationDuration: meter.createHistogram(METRIC.operationDuration, {
108
+ description: 'Duration of messaging operation initiated by a producer or consumer client.',
109
+ unit: 's',
110
+ advice: { explicitBucketBoundaries: DURATION_BUCKETS }
111
+ }),
112
+ sentMessages: meter.createCounter(METRIC.sentMessages, {
113
+ description: 'Number of messages producer attempted to send to the broker.',
114
+ unit: '{message}'
115
+ }),
116
+ consumedMessages: meter.createCounter(METRIC.consumedMessages, {
117
+ description: 'Number of messages that were delivered to the application.',
118
+ unit: '{message}'
119
+ }),
120
+ processDuration: meter.createHistogram(METRIC.processDuration, {
121
+ description: 'Duration of processing operation.',
122
+ unit: 's',
123
+ advice: { explicitBucketBoundaries: DURATION_BUCKETS }
124
+ }),
125
+ queueJobs: meter.createObservableGauge(METRIC.queueJobs, {
126
+ description: 'Jobs in a queue by state, as of the last queue cache refresh.',
127
+ unit: '{job}'
128
+ })
129
+ };
130
+ if (this.#observing) {
131
+ this.#instruments.queueJobs.addCallback(this.#observeQueues);
132
+ }
133
+ return this.#instruments;
134
+ }
135
+ #observeQueues = (result) => {
136
+ const queues = this.#queues();
137
+ if (!queues)
138
+ return;
139
+ for (const queue of Object.values(queues)) {
140
+ const attributes = { [ATTR.messagingSystem]: MESSAGING_SYSTEM, [ATTR.destinationName]: queue.name, ...this.#namespace };
141
+ result.observe(queue.deferredCount ?? 0, { ...attributes, [ATTR.jobState]: 'deferred' });
142
+ result.observe(queue.readyCount ?? 0, { ...attributes, [ATTR.jobState]: 'ready' });
143
+ result.observe(queue.blockedCount ?? 0, { ...attributes, [ATTR.jobState]: 'blocked' });
144
+ result.observe(queue.activeCount ?? 0, { ...attributes, [ATTR.jobState]: 'active' });
145
+ result.observe(queue.failedCount ?? 0, { ...attributes, [ATTR.jobState]: 'failed' });
146
+ }
147
+ };
148
+ // Starts reporting pgboss.queue.jobs from the queue cache. Called by start(), undone by stop().
149
+ observeQueues() {
150
+ if (!this.enabled || this.#observing)
151
+ return;
152
+ this.#observing = true;
153
+ this.#getInstruments()?.queueJobs.addCallback(this.#observeQueues);
154
+ }
155
+ unobserveQueues() {
156
+ if (!this.#observing)
157
+ return;
158
+ this.#observing = false;
159
+ this.#instruments?.queueJobs.removeCallback(this.#observeQueues);
160
+ }
161
+ // Moves the queue gauge to a meter provider registered after start(), which otherwise waits for
162
+ // the next send, fetch or settle. Called on each queue cache refresh.
163
+ refreshInstruments() {
164
+ if (this.#observing)
165
+ this.#getInstruments();
166
+ }
167
+ /**
168
+ * Runs a job-creating operation in a PRODUCER span and hands `fn` the trace context to store on
169
+ * each job it inserts, or null when there is nothing to propagate. `attempted` is the number of
170
+ * jobs it creates itself, counted whether or not they land. A function instead counts from the
171
+ * result, and counts nothing when `fn` throws.
172
+ */
173
+ async send(operation, destination, attempted, fn, idsOf = () => null) {
174
+ if (!this.enabled)
175
+ return fn(null);
176
+ const attributes = this.#baseAttributes(operation, 'send', destination);
177
+ if (typeof attempted === 'number' && attempted > 1)
178
+ attributes[ATTR.batchMessageCount] = attempted;
179
+ const span = this.#tracer.startSpan(spanName(operation, destination), { kind: SpanKind.PRODUCER, attributes });
180
+ const spanContext = trace.setSpan(context.active(), span);
181
+ const startedAt = performance.now();
182
+ let carrier = null;
183
+ if (this.#propagate) {
184
+ const injected = {};
185
+ this.#propagator.inject(spanContext, injected, defaultTextMapSetter);
186
+ carrier = Object.keys(injected).length > 0 ? injected : null;
187
+ }
188
+ try {
189
+ const result = await context.with(spanContext, () => fn(carrier));
190
+ const ids = idsOf(result);
191
+ const sent = typeof attempted === 'number' ? attempted : attempted(result);
192
+ if (sent === 1 && ids?.length === 1) {
193
+ span.setAttribute(ATTR.messageId, ids[0]);
194
+ }
195
+ this.#recordSend(startedAt, sent, metricAttributes(attributes));
196
+ return result;
197
+ }
198
+ catch (err) {
199
+ recordError(span, err);
200
+ this.#recordSend(startedAt, typeof attempted === 'number' ? attempted : 0, metricAttributes(attributes, err));
201
+ throw err;
202
+ }
203
+ finally {
204
+ span.end();
205
+ }
206
+ }
207
+ #recordSend(startedAt, attempted, attributes) {
208
+ const instruments = this.#getInstruments();
209
+ instruments?.operationDuration.record(seconds(startedAt), attributes);
210
+ if (attempted > 0)
211
+ instruments?.sentMessages.add(attempted, attributes);
212
+ }
213
+ /**
214
+ * Wraps a fetch() the application made itself in a CLIENT `receive` span, linked to the send of
215
+ * every job it returned.
216
+ */
217
+ async receive(destination, fn, carrierOf) {
218
+ if (!this.enabled)
219
+ return fn();
220
+ const attributes = this.#baseAttributes('receive', 'receive', destination);
221
+ const span = this.#tracer.startSpan(spanName('receive', destination), { kind: SpanKind.CLIENT, attributes });
222
+ const startedAt = performance.now();
223
+ try {
224
+ const jobs = await context.with(trace.setSpan(context.active(), span), fn);
225
+ if (jobs.length === 1) {
226
+ span.setAttribute(ATTR.messageId, jobs[0].id);
227
+ }
228
+ else {
229
+ span.setAttribute(ATTR.batchMessageCount, jobs.length);
230
+ }
231
+ span.addLinks(this.#links(jobs, carrierOf));
232
+ const instruments = this.#getInstruments();
233
+ instruments?.operationDuration.record(seconds(startedAt), metricAttributes(attributes));
234
+ if (jobs.length > 0)
235
+ instruments?.consumedMessages.add(jobs.length, metricAttributes(attributes));
236
+ return jobs;
237
+ }
238
+ catch (err) {
239
+ recordError(span, err);
240
+ this.#getInstruments()?.operationDuration.record(seconds(startedAt), metricAttributes(attributes, err));
241
+ throw err;
242
+ }
243
+ finally {
244
+ span.end();
245
+ }
246
+ }
247
+ // Jobs a worker claimed. The worker's own fetch has no span: it polls on a timer, and a span per
248
+ // empty poll would bury the traces that matter. The process span covers what it delivers.
249
+ consumed(destination, count) {
250
+ if (count === 0)
251
+ return;
252
+ this.#getInstruments()?.consumedMessages.add(count, metricAttributes(this.#baseAttributes('process', 'process', destination)));
253
+ }
254
+ /**
255
+ * Runs a worker's handler over a batch in a CONSUMER `process` span. A batch of one continues the
256
+ * trace its send() started; a larger batch starts a trace of its own and links to every send.
257
+ *
258
+ * `fn` resolves with the error the batch failed with, or undefined when it completed, because the
259
+ * worker settles a failed batch itself rather than letting the error escape.
260
+ */
261
+ async process(destination, jobs, carrierOf, fn) {
262
+ if (!this.enabled) {
263
+ await fn();
264
+ return;
265
+ }
266
+ const attributes = this.#baseAttributes('process', 'process', destination);
267
+ let parent = ROOT_CONTEXT;
268
+ let links = [];
269
+ if (jobs.length === 1) {
270
+ attributes[ATTR.messageId] = jobs[0].id;
271
+ if (jobs[0].retryCount !== undefined)
272
+ attributes[ATTR.retryCount] = jobs[0].retryCount;
273
+ // The whole extracted context, so baggage sent with the job reaches the handler too.
274
+ const carrier = carrierOf(jobs[0]);
275
+ if (carrier)
276
+ parent = this.#extract(carrier);
277
+ }
278
+ else {
279
+ attributes[ATTR.batchMessageCount] = jobs.length;
280
+ links = this.#links(jobs, carrierOf);
281
+ }
282
+ // The parent is chosen explicitly, never taken from context.active(): the worker loop inherits
283
+ // whatever context work() was called in, and a job has nothing to do with that caller's trace.
284
+ const span = this.#tracer.startSpan(spanName('process', destination), { kind: SpanKind.CONSUMER, attributes, links }, parent);
285
+ const startedAt = performance.now();
286
+ let failure;
287
+ try {
288
+ failure = await context.with(trace.setSpan(parent, span), fn);
289
+ }
290
+ catch (err) {
291
+ failure = err ?? new Error('undefined error');
292
+ throw err;
293
+ }
294
+ finally {
295
+ if (failure !== undefined)
296
+ recordError(span, failure);
297
+ this.#getInstruments()?.processDuration.record(seconds(startedAt), metricAttributes(attributes, failure));
298
+ span.end();
299
+ }
300
+ }
301
+ /**
302
+ * Wraps a call that settles or removes jobs (complete, fail, cancel, deleteJob) in a CLIENT span.
303
+ */
304
+ async settle(operation, destination, ids, fn) {
305
+ if (!this.enabled)
306
+ return fn();
307
+ const attributes = this.#baseAttributes(operation, 'settle', destination);
308
+ if (ids.length === 1) {
309
+ attributes[ATTR.messageId] = ids[0];
310
+ }
311
+ else {
312
+ attributes[ATTR.batchMessageCount] = ids.length;
313
+ }
314
+ const span = this.#tracer.startSpan(spanName(operation, destination), { kind: SpanKind.CLIENT, attributes });
315
+ const startedAt = performance.now();
316
+ try {
317
+ const result = await context.with(trace.setSpan(context.active(), span), fn);
318
+ this.#getInstruments()?.operationDuration.record(seconds(startedAt), metricAttributes(attributes));
319
+ return result;
320
+ }
321
+ catch (err) {
322
+ recordError(span, err);
323
+ this.#getInstruments()?.operationDuration.record(seconds(startedAt), metricAttributes(attributes, err));
324
+ throw err;
325
+ }
326
+ finally {
327
+ span.end();
328
+ }
329
+ }
330
+ #links(jobs, carrierOf) {
331
+ const links = [];
332
+ for (const job of jobs) {
333
+ const spanContext = this.#extractSpanContext(carrierOf(job));
334
+ if (spanContext)
335
+ links.push({ context: spanContext });
336
+ }
337
+ return links;
338
+ }
339
+ #extract(carrier) {
340
+ return this.#propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
341
+ }
342
+ #extractSpanContext(carrier) {
343
+ if (!carrier)
344
+ return null;
345
+ const spanContext = trace.getSpanContext(this.#extract(carrier));
346
+ return spanContext && trace.isSpanContextValid(spanContext) ? spanContext : null;
347
+ }
348
+ }
349
+ export default Telemetry;
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { MeterProvider, TextMapPropagator, TracerProvider } from '@opentelemetry/api';
1
2
  export type JobStates = {
2
3
  created: 'created';
3
4
  retry: 'retry';
@@ -50,6 +51,18 @@ export interface IDatabase {
50
51
  * does not implement this.
51
52
  */
52
53
  setSessionStatements?(statements: string[]): Promise<void>;
54
+ /**
55
+ * Optional capability for the instance registry: the pool's size and use, recorded on each
56
+ * heartbeat. The built-in pool-based Db implements it; without it the pool columns stay null.
57
+ */
58
+ poolCounts?(): PoolCounts | null;
59
+ }
60
+ /** A connection pool's size and use at one moment, as node-postgres counts them. */
61
+ export interface PoolCounts {
62
+ max: number;
63
+ total: number;
64
+ idle: number;
65
+ waiting: number;
53
66
  }
54
67
  export interface ListenHandle {
55
68
  close(): Promise<void>;
@@ -255,8 +268,30 @@ export interface QueueStats {
255
268
  deltaSeconds: number | null;
256
269
  /** When the interval the deltas cover ends, 10 seconds behind `capturedOn`. Plot the deltas at this time. */
257
270
  deltaOn: Date | null;
271
+ /**
272
+ * Wait times of the jobs that finished in the deltas' window, as a histogram of 48 counts in
273
+ * log-spaced bins; sum histograms to read a percentile over any span. Null wherever the deltas are.
274
+ * @see https://pgboss.io/api/queues#getqueues-names
275
+ */
276
+ waitBins: number[] | null;
277
+ /** Run times of the same jobs, in the same bins as `waitBins`. */
278
+ runBins: number[] | null;
279
+ /** How long the oldest job ready to run had waited when the snapshot was captured. */
280
+ readyOldestSeconds: number | null;
281
+ /**
282
+ * With the `percentiles` option: one entry per percentile asked for, read from `waitBins` and `runBins`.
283
+ * @see https://pgboss.io/api/queues#getqueuestats-name-options
284
+ */
285
+ percentiles?: QueueStatsPercentile[];
258
286
  capturedOn: Date;
259
287
  }
288
+ /** One percentile of a snapshot's wait and run times, in seconds. Null where the snapshot has no histograms. */
289
+ export interface QueueStatsPercentile {
290
+ /** The percentile asked for, from 1 to 100. */
291
+ p: number;
292
+ waitSeconds: number | null;
293
+ runSeconds: number | null;
294
+ }
260
295
  export interface QueueStatsOptions {
261
296
  /** persistQueueStats on: only return snapshots captured at or after this time. */
262
297
  from?: Date;
@@ -288,6 +323,12 @@ export interface QueueStatsOptions {
288
323
  * @default 'max'
289
324
  */
290
325
  aggregate?: 'max' | 'min' | 'avg';
326
+ /**
327
+ * Percentiles to read from each snapshot's wait and run histograms, as percents from 1 to 100, such
328
+ * as `[50, 95, 99.9]`. Each snapshot then carries a `percentiles` list, one entry per distinct value.
329
+ * @see https://pgboss.io/api/queues#getqueuestats-name-options
330
+ */
331
+ percentiles?: number[];
291
332
  /**
292
333
  * persistQueueStats off: return a fresh reading. Recomputes the counts from the job table and
293
334
  * refreshes the queue-table cache rather than serving the regular (up to ~1h) cache, but still
@@ -392,7 +433,57 @@ export interface AttachableClock extends Clock {
392
433
  idle?: () => Promise<boolean>;
393
434
  }): Promise<AsyncDisposable>;
394
435
  }
395
- export interface ConstructorOptions extends DatabaseOptions, SchedulingOptions, MaintenanceOptions, BackendOptions {
436
+ export interface InstanceOptions {
437
+ /**
438
+ * Record this instance in the database's instance registry, readable with `getInstances()`.
439
+ * @see https://pgboss.io/api/ops#getinstances
440
+ * @default true
441
+ */
442
+ registerInstance?: boolean;
443
+ /**
444
+ * A name for this instance in the registry, such as `api` or `billing-worker`.
445
+ * @see https://pgboss.io/api/constructor#instancename
446
+ */
447
+ instanceName?: string;
448
+ /**
449
+ * How often this instance refreshes its registry row, in seconds. It reads as quiet after three
450
+ * missed heartbeats.
451
+ * @default 30
452
+ */
453
+ instanceHeartbeatSeconds?: number;
454
+ }
455
+ export interface OpenTelemetryOptions {
456
+ /**
457
+ * Set to false to emit no spans or metrics and store no trace context on jobs.
458
+ * @default true
459
+ */
460
+ enabled?: boolean;
461
+ /**
462
+ * Store the trace context active at `send()` on the job, so the span that processes it continues
463
+ * the producer's trace. With the SDK's default propagators this stores W3C Baggage as well as the
464
+ * trace id; pass `propagator` to store less.
465
+ * @default false
466
+ * @see https://pgboss.io/opentelemetry#options
467
+ */
468
+ propagateContext?: boolean;
469
+ /**
470
+ * Propagator that writes the trace context stored on a job and reads it back when the job is
471
+ * processed, for example `new W3CTraceContextPropagator()` to store trace ids only.
472
+ * @default the propagator registered globally with the OpenTelemetry API
473
+ */
474
+ propagator?: TextMapPropagator;
475
+ /**
476
+ * Tracer provider to create pg-boss spans with.
477
+ * @default the global tracer provider
478
+ */
479
+ tracerProvider?: TracerProvider;
480
+ /**
481
+ * Meter provider to create pg-boss instruments with.
482
+ * @default the global meter provider
483
+ */
484
+ meterProvider?: MeterProvider;
485
+ }
486
+ export interface ConstructorOptions extends DatabaseOptions, SchedulingOptions, MaintenanceOptions, BackendOptions, InstanceOptions {
396
487
  /**
397
488
  * Source of time and timers for this instance. Defaults to the system clock (`Date.now` and the
398
489
  * global timer functions).
@@ -410,6 +501,12 @@ export interface ConstructorOptions extends DatabaseOptions, SchedulingOptions,
410
501
  * @default false
411
502
  */
412
503
  useListenNotify?: boolean;
504
+ /**
505
+ * OpenTelemetry tracing and metrics. On by default and free until an OpenTelemetry SDK is
506
+ * registered: without one every span and instrument is a no-op.
507
+ * @see https://pgboss.io/opentelemetry
508
+ */
509
+ openTelemetry?: OpenTelemetryOptions;
413
510
  /**
414
511
  * Enables job spies for deterministic testing (see `getSpy`). Adds per-transition
415
512
  * tracking overhead, **NOT for production.**
@@ -577,6 +674,10 @@ export interface RedrivePreview {
577
674
  unroutable: number;
578
675
  }
579
676
  export type InsertOptions = ConnectionOptions & {
677
+ /**
678
+ * Resolve to the ids of the inserted jobs instead of `null`. Defaults to `false`.
679
+ * @see https://pgboss.io/api/jobs#insert-name-job-options
680
+ */
580
681
  returnId?: boolean;
581
682
  };
582
683
  export type SendOptions = JobOptions & QueueOptions & ConnectionOptions;
@@ -685,11 +786,14 @@ export interface Queue extends QueueOptions {
685
786
  notify?: boolean;
686
787
  }
687
788
  export interface QueueResult extends Queue {
789
+ /** Queued jobs whose `startAfter` is still ahead and that are not blocked. */
688
790
  deferredCount: number;
791
+ /** Queued jobs waiting on a flow parent, whatever their `startAfter`. */
792
+ blockedCount: number;
689
793
  queuedCount: number;
690
794
  /**
691
- * Jobs ready to be processed now: `queuedCount - deferredCount` (clamped at 0). This is the
692
- * true backlog, `queuedCount` includes deferred (future-dated) jobs that are not yet runnable.
795
+ * Jobs ready to be processed now: queued, not deferred and not blocked, so `queuedCount` is
796
+ * `deferredCount + blockedCount + readyCount`.
693
797
  */
694
798
  readyCount: number;
695
799
  activeCount: number;
@@ -713,6 +817,12 @@ export interface QueueResult extends Queue {
713
817
  deltaSeconds: number | null;
714
818
  /** When that interval ends, 10 seconds behind the pass. See `QueueStats.deltaOn`. Null until a pass counts. */
715
819
  deltaOn: Date | null;
820
+ /** Wait times of the jobs counted in the deltas, as 48 counts. See `QueueStats.waitBins`. Null until a pass counts. */
821
+ waitBins: number[] | null;
822
+ /** Run times of the same jobs, in the same bins. Null until a pass counts. */
823
+ runBins: number[] | null;
824
+ /** How long the oldest ready job had waited at the pass. See `QueueStats.readyOldestSeconds`. Null until a pass counts. */
825
+ readyOldestSeconds: number | null;
716
826
  table: string;
717
827
  createdOn: Date;
718
828
  updatedOn: Date;
@@ -1092,6 +1202,94 @@ export interface WipData {
1092
1202
  lastError: object | null;
1093
1203
  lastErrorOn: number | null;
1094
1204
  }
1205
+ /** One `work()` call of a registered instance, as its last heartbeat recorded it. */
1206
+ export interface InstanceWorker {
1207
+ /** The id `work()` returned. */
1208
+ id: string;
1209
+ queue: string;
1210
+ localConcurrency: number;
1211
+ batchSize: number;
1212
+ pollingIntervalSeconds: number | null;
1213
+ /** Jobs in hand at the heartbeat. */
1214
+ active: number;
1215
+ lastFetchedOn: string | null;
1216
+ lastJobEndedOn: string | null;
1217
+ lastErrorOn: string | null;
1218
+ /**
1219
+ * The other `work()` options this call set; one left at its default is absent.
1220
+ * @see https://pgboss.io/api/ops#getinstances
1221
+ */
1222
+ options?: Partial<WorkOptions>;
1223
+ }
1224
+ /**
1225
+ * A registered instance's process at its last heartbeat. Limits are its container's where one is set
1226
+ * (cgroup v1 or v2), else the host's; rates cover the time since the previous heartbeat.
1227
+ * @see https://pgboss.io/api/ops#getinstances
1228
+ */
1229
+ export interface InstanceMetrics {
1230
+ /** 1 or 2 when the limits were read from a cgroup, null when they are the host's. */
1231
+ cgroup: 1 | 2 | null;
1232
+ /** CPU cores this process used; null on the first sample. */
1233
+ cpu: number | null;
1234
+ /** Cores it may use: its container's CPU quota, or the CPUs it may run on. */
1235
+ cpuLimit: number;
1236
+ /** Share of CPU periods its container was throttled for, 0 to 1; null without a CPU quota. */
1237
+ cpuThrottled: number | null;
1238
+ /** Resident set size in bytes. */
1239
+ rss: number | null;
1240
+ heapUsed: number | null;
1241
+ heapLimit: number | null;
1242
+ /** Its container's working set in bytes; null without a memory limit. */
1243
+ memoryUsed: number | null;
1244
+ /** Its container's memory limit in bytes, or the host's total memory. */
1245
+ memoryLimit: number;
1246
+ /** Event loop delay, p99 and worst, in milliseconds. */
1247
+ loopDelay: number | null;
1248
+ loopDelayMax: number | null;
1249
+ /** Share of the time the event loop was busy, 0 to 1. */
1250
+ loopUtilization: number | null;
1251
+ }
1252
+ /**
1253
+ * A PgBoss object that registered itself in this database.
1254
+ * @see https://pgboss.io/api/ops#getinstances
1255
+ */
1256
+ export interface Instance {
1257
+ id: string;
1258
+ name: string | null;
1259
+ host: string;
1260
+ pid: number;
1261
+ /** pg-boss version. */
1262
+ version: string;
1263
+ nodeVersion: string;
1264
+ /** The `application_name` its connections carry, for joining `pg_stat_activity`. */
1265
+ applicationName: string | null;
1266
+ heartbeatSeconds: number;
1267
+ supervise: boolean;
1268
+ schedule: boolean;
1269
+ migrate: boolean;
1270
+ persistQueueStats: boolean;
1271
+ persistWarnings: boolean;
1272
+ /** Pool counts at the last heartbeat; null for a pool pg-boss did not create. */
1273
+ poolMax: number | null;
1274
+ poolTotal: number | null;
1275
+ poolIdle: number | null;
1276
+ poolWaiting: number | null;
1277
+ workers: InstanceWorker[];
1278
+ /** Its process at the last heartbeat; null before the first sample. */
1279
+ metrics: InstanceMetrics | null;
1280
+ /** The options it runs with: timings, roles, retention and backend, never connection details. */
1281
+ config: Record<string, unknown>;
1282
+ /** Lives in a row on this name and host that ended without `stop()` before this one started. */
1283
+ crashRestarts: number;
1284
+ /** When the first of those crashed; null when there were none. */
1285
+ crashRestartsSince: Date | null;
1286
+ startedOn: Date;
1287
+ heartbeatOn: Date;
1288
+ /** Set by a graceful `stop()`; a crashed instance goes quiet instead. */
1289
+ stoppedOn: Date | null;
1290
+ /** Not stopped, and heard from within three heartbeats. */
1291
+ live: boolean;
1292
+ }
1095
1293
  export interface StopOptions {
1096
1294
  close?: boolean;
1097
1295
  graceful?: boolean;