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.
- package/README.md +1 -0
- package/dist/attorney.d.ts.map +1 -1
- package/dist/attorney.js +21 -0
- package/dist/boss.d.ts.map +1 -1
- package/dist/boss.js +35 -4
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +11 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -5
- package/dist/latency.d.ts +15 -0
- package/dist/latency.d.ts.map +1 -0
- package/dist/latency.js +45 -0
- package/dist/manager.d.ts +2 -1
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +216 -73
- package/dist/migrationStore.d.ts.map +1 -1
- package/dist/migrationStore.js +67 -0
- package/dist/nurse.d.ts +20 -0
- package/dist/nurse.d.ts.map +1 -0
- package/dist/nurse.js +287 -0
- package/dist/plans.d.ts +20 -0
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +500 -50
- package/dist/registrar.d.ts +16 -0
- package/dist/registrar.d.ts.map +1 -0
- package/dist/registrar.js +221 -0
- package/dist/schema.json +480 -1
- package/dist/telemetry.d.ts +65 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +349 -0
- package/dist/types.d.ts +201 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +17 -8
|
@@ -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
|
|
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:
|
|
692
|
-
*
|
|
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;
|