pg-boss 12.35.1 → 12.37.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 +2 -1
- package/dist/attorney.d.ts.map +1 -1
- package/dist/attorney.js +26 -0
- package/dist/boss.d.ts.map +1 -1
- package/dist/boss.js +35 -4
- package/dist/cli.js +8 -2
- package/dist/contractor.d.ts +1 -0
- package/dist/contractor.d.ts.map +1 -1
- package/dist/contractor.js +4 -0
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +14 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +88 -9
- 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.map +1 -1
- package/dist/manager.js +296 -75
- package/dist/migrationStore.d.ts.map +1 -1
- package/dist/migrationStore.js +194 -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 +27 -2
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +556 -55
- package/dist/registrar.d.ts +16 -0
- package/dist/registrar.d.ts.map +1 -0
- package/dist/registrar.js +226 -0
- package/dist/schema.json +514 -2
- package/dist/telemetry.d.ts +65 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +349 -0
- package/dist/timekeeper.d.ts +4 -1
- package/dist/timekeeper.d.ts.map +1 -1
- package/dist/timekeeper.js +22 -5
- package/dist/types.d.ts +213 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.d.ts +1 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +2 -0
- package/package.json +14 -5
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type * as types from './types.ts';
|
|
2
|
+
export declare const ATTR: {
|
|
3
|
+
readonly messagingSystem: "messaging.system";
|
|
4
|
+
readonly operationName: "messaging.operation.name";
|
|
5
|
+
readonly operationType: "messaging.operation.type";
|
|
6
|
+
readonly destinationName: "messaging.destination.name";
|
|
7
|
+
readonly messageId: "messaging.message.id";
|
|
8
|
+
readonly batchMessageCount: "messaging.batch.message_count";
|
|
9
|
+
readonly errorType: "error.type";
|
|
10
|
+
readonly dbNamespace: "db.namespace";
|
|
11
|
+
readonly schema: "pgboss.schema";
|
|
12
|
+
readonly retryCount: "pgboss.job.retry_count";
|
|
13
|
+
readonly jobState: "pgboss.job.state";
|
|
14
|
+
};
|
|
15
|
+
export declare const METRIC: {
|
|
16
|
+
readonly operationDuration: "messaging.client.operation.duration";
|
|
17
|
+
readonly sentMessages: "messaging.client.sent.messages";
|
|
18
|
+
readonly consumedMessages: "messaging.client.consumed.messages";
|
|
19
|
+
readonly processDuration: "messaging.process.duration";
|
|
20
|
+
readonly queueJobs: "pgboss.queue.jobs";
|
|
21
|
+
};
|
|
22
|
+
export declare const MESSAGING_SYSTEM = "pg-boss";
|
|
23
|
+
export declare const INSTRUMENTATION_SCOPE = "pg-boss";
|
|
24
|
+
export type TraceCarrier = Record<string, string>;
|
|
25
|
+
interface CarrierJob {
|
|
26
|
+
id: string;
|
|
27
|
+
retryCount?: number;
|
|
28
|
+
}
|
|
29
|
+
type QueueSnapshot = () => Record<string, types.QueueResult> | null;
|
|
30
|
+
declare class Telemetry {
|
|
31
|
+
#private;
|
|
32
|
+
readonly enabled: boolean;
|
|
33
|
+
constructor(options: types.OpenTelemetryOptions | undefined, schema: string, queues: QueueSnapshot);
|
|
34
|
+
setDatabase(database: string): void;
|
|
35
|
+
observeQueues(): void;
|
|
36
|
+
unobserveQueues(): void;
|
|
37
|
+
refreshInstruments(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Runs a job-creating operation in a PRODUCER span and hands `fn` the trace context to store on
|
|
40
|
+
* each job it inserts, or null when there is nothing to propagate. `attempted` is the number of
|
|
41
|
+
* jobs it creates itself, counted whether or not they land. A function instead counts from the
|
|
42
|
+
* result, and counts nothing when `fn` throws.
|
|
43
|
+
*/
|
|
44
|
+
send<T>(operation: string, destination: string | null, attempted: number | ((result: T) => number), fn: (carrier: TraceCarrier | null) => Promise<T>, idsOf?: (result: T) => string[] | null): Promise<T>;
|
|
45
|
+
/**
|
|
46
|
+
* Wraps a fetch() the application made itself in a CLIENT `receive` span, linked to the send of
|
|
47
|
+
* every job it returned.
|
|
48
|
+
*/
|
|
49
|
+
receive<J extends CarrierJob>(destination: string, fn: () => Promise<J[]>, carrierOf: (job: J) => TraceCarrier | null | undefined): Promise<J[]>;
|
|
50
|
+
consumed(destination: string, count: number): void;
|
|
51
|
+
/**
|
|
52
|
+
* Runs a worker's handler over a batch in a CONSUMER `process` span. A batch of one continues the
|
|
53
|
+
* trace its send() started; a larger batch starts a trace of its own and links to every send.
|
|
54
|
+
*
|
|
55
|
+
* `fn` resolves with the error the batch failed with, or undefined when it completed, because the
|
|
56
|
+
* worker settles a failed batch itself rather than letting the error escape.
|
|
57
|
+
*/
|
|
58
|
+
process<J extends CarrierJob>(destination: string, jobs: J[], carrierOf: (job: J) => TraceCarrier | null | undefined, fn: () => Promise<unknown>): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Wraps a call that settles or removes jobs (complete, fail, cancel, deleteJob) in a CLIENT span.
|
|
61
|
+
*/
|
|
62
|
+
settle<T>(operation: string, destination: string, ids: string[], fn: () => Promise<T>): Promise<T>;
|
|
63
|
+
}
|
|
64
|
+
export default Telemetry;
|
|
65
|
+
//# sourceMappingURL=telemetry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAMxC,eAAO,MAAM,IAAI;;;;;;;;;;;;CAYP,CAAA;AAEV,eAAO,MAAM,MAAM;;;;;;CAMT,CAAA;AAEV,eAAO,MAAM,gBAAgB,YAAY,CAAA;AACzC,eAAO,MAAM,qBAAqB,YAAY,CAAA;AAQ9C,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAUjD,UAAU,UAAU;IAClB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,KAAK,aAAa,GAAG,MAAM,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,WAAW,CAAC,GAAG,IAAI,CAAA;AA8BnE,cAAM,SAAS;;IACb,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;gBAYZ,OAAO,EAAE,KAAK,CAAC,oBAAoB,YAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa;IAa5F,WAAW,CAAE,QAAQ,EAAE,MAAM;IA+E7B,aAAa;IAMb,eAAe;IAQf,kBAAkB;IAIlB;;;;;OAKG;IACG,IAAI,CAAC,CAAC,EACV,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,SAAS,EAAE,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC,EAC3C,EAAE,EAAE,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,EAChD,KAAK,GAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,EAAE,GAAG,IAAiB,GACjD,OAAO,CAAC,CAAC,CAAC;IA4Cb;;;OAGG;IACG,OAAO,CAAC,CAAC,SAAS,UAAU,EAAG,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,YAAY,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAkCvJ,QAAQ,CAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAK5C;;;;;;OAMG;IACG,OAAO,CAAC,CAAC,SAAS,UAAU,EAAG,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,YAAY,GAAG,IAAI,GAAG,SAAS,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IA2CvK;;OAEG;IACG,MAAM,CAAC,CAAC,EAAG,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CA8C1G;AAED,eAAe,SAAS,CAAA"}
|
|
@@ -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/timekeeper.d.ts
CHANGED
|
@@ -76,6 +76,9 @@ declare class Timekeeper extends EventEmitter implements types.EventsMixin {
|
|
|
76
76
|
* labels every row from its default. Either way the row reads fine and never fires again. So when
|
|
77
77
|
* an expression cannot be read the way the column says, and is written the other way, it is read
|
|
78
78
|
* the way it is written: one regex, on a path that was already about to give up.
|
|
79
|
+
*
|
|
80
|
+
* An empty expression is refused first, as schedule() refuses it. cron-parser would read one as
|
|
81
|
+
* every minute, and a row stored before schedule() checked for it would keep firing that often.
|
|
79
82
|
*/
|
|
80
83
|
private dueOccurrences;
|
|
81
84
|
/**
|
|
@@ -170,7 +173,7 @@ declare class Timekeeper extends EventEmitter implements types.EventsMixin {
|
|
|
170
173
|
*/
|
|
171
174
|
previewSchedule(cron: string, options?: types.PreviewScheduleOptions): Date[];
|
|
172
175
|
schedule(name: string, cron: string, data?: unknown, options?: types.ScheduleOptions): Promise<void>;
|
|
173
|
-
unschedule(name: string, key?: string): Promise<void>;
|
|
176
|
+
unschedule(name: string, key?: string, options?: types.ConnectionOptions): Promise<void>;
|
|
174
177
|
}
|
|
175
178
|
export default Timekeeper;
|
|
176
179
|
//# sourceMappingURL=timekeeper.d.ts.map
|
package/dist/timekeeper.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timekeeper.d.ts","sourceRoot":"","sources":["../src/timekeeper.ts"],"names":[],"mappings":"AAEA,OAAO,YAAY,MAAM,aAAa,CAAA;AAItC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAKvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAGnC,eAAO,MAAM,MAAM;;CAElB,CAAA;AAwBD,kGAAkG;AAClG,eAAO,MAAM,iBAAiB,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"timekeeper.d.ts","sourceRoot":"","sources":["../src/timekeeper.ts"],"names":[],"mappings":"AAEA,OAAO,YAAY,MAAM,aAAa,CAAA;AAItC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAKvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAGnC,eAAO,MAAM,MAAM;;CAElB,CAAA;AAwBD,kGAAkG;AAClG,eAAO,MAAM,iBAAiB,OAAO,CAAA;AAuNrC,cAAM,UAAW,SAAQ,YAAa,YAAW,KAAK,CAAC,WAAW;IAChE,EAAE,EAAE,KAAK,CAAC,SAAS,CAAA;IACnB,MAAM,EAAE,KAAK,CAAC,0BAA0B,CAAA;IACxC,OAAO,EAAE,OAAO,CAAA;IAEhB,OAAO,CAAC,OAAO,CAAO;IACtB,OAAO,CAAC,gBAAgB,CAA+B;IACvD,OAAO,CAAC,gBAAgB,CAAqC;IAC7D,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,aAAa,CAAQ;IAS7B,OAAO,CAAC,eAAe,CAAoB;IAE3C,SAAS,SAAI;IACb,MAAM;;;;MAAS;gBAEF,EAAE,EAAE,KAAK,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,0BAA0B;IAQ5F,IAAI,YAAY,IAAK,OAAO,CAE3B;IAED,IAAI,WAAW,IAAK,OAAO,CAE1B;IAMD,OAAO,KAAK,YAAY,GAEvB;IAED,OAAO,KAAK,cAAc,GASzB;IAEK,KAAK;IA4BL,IAAI;IAmCV,OAAO,CAAC,mBAAmB;IAUrB,cAAc;IA8Cd,MAAM;IAuDZ;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,eAAe;IAQvB;;;;;OAKG;IACG,IAAI,CAAE,WAAW,GAAE,OAAc;IAoIvC,YAAY,CAAE,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,GAAE,KAAK,CAAC,YAAwC;IAIlG;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,cAAc;IAkBtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8BG;IACH,OAAO,CAAC,gBAAgB;IAgBxB;;;;;;;OAOG;IACH,OAAO,CAAC,sBAAsB;IAgB9B;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,eAAe;IAwBvB,OAAO,CAAC,iBAAiB;YAQX,QAAQ;YAwDR,aAAa;IAerB,YAAY,CAAE,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;IAiBrE,WAAW,CAAE,IAAI,EAAE,MAAM,EAAE,GAAG,SAAK,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC;IAe1E;;;;;;;OAOG;IACH,OAAO,CAAC,gBAAgB;IAcxB;;;;;;;;;;;;;;;;OAgBG;IACH,eAAe,CAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,KAAK,CAAC,sBAA2B,GAAG,IAAI,EAAE;IA0C5E,QAAQ,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,GAAE,KAAK,CAAC,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiDzG,UAAU,CAAE,IAAI,EAAE,MAAM,EAAE,GAAG,SAAK,EAAE,OAAO,GAAE,KAAK,CAAC,iBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC;CAIhG;AAED,eAAe,UAAU,CAAA"}
|
package/dist/timekeeper.js
CHANGED
|
@@ -178,11 +178,18 @@ function parseRecurrence(cron, tz, currentDate) {
|
|
|
178
178
|
assertTimezone(tz);
|
|
179
179
|
return CronExpressionParser.parse(cron, { tz, strict: false, currentDate });
|
|
180
180
|
}
|
|
181
|
+
// cron-parser reads an empty expression, and undefined or null, as `* * * * *`, and those are nearly
|
|
182
|
+
// always a missing value, not a request to run every minute. Checked before isRrule(), which reads a
|
|
183
|
+
// non-string as its string form and would send something like ['FREQ=DAILY'] down the rule path.
|
|
184
|
+
function assertExpression(expression) {
|
|
185
|
+
assert(typeof expression === 'string' && expression.trim() !== '', 'cron expression must be a non-empty string');
|
|
186
|
+
}
|
|
181
187
|
/**
|
|
182
188
|
* Validates a recurrence in whichever of the two formats it is written, so previewSchedule() and
|
|
183
189
|
* schedule() reject exactly the same expressions.
|
|
184
190
|
*/
|
|
185
191
|
function assertRecurrence(expression, tz, now) {
|
|
192
|
+
assertExpression(expression);
|
|
186
193
|
if (isRrule(expression)) {
|
|
187
194
|
assertRrule(expression, tz);
|
|
188
195
|
}
|
|
@@ -542,8 +549,12 @@ class Timekeeper extends EventEmitter {
|
|
|
542
549
|
* labels every row from its default. Either way the row reads fine and never fires again. So when
|
|
543
550
|
* an expression cannot be read the way the column says, and is written the other way, it is read
|
|
544
551
|
* the way it is written: one regex, on a path that was already about to give up.
|
|
552
|
+
*
|
|
553
|
+
* An empty expression is refused first, as schedule() refuses it. cron-parser would read one as
|
|
554
|
+
* every minute, and a row stored before schedule() checked for it would keep firing that often.
|
|
545
555
|
*/
|
|
546
556
|
dueOccurrences(expression, kind, tz, databaseTime = this.databaseTime) {
|
|
557
|
+
assertExpression(expression);
|
|
547
558
|
try {
|
|
548
559
|
return { kind, occurrences: this.readOccurrences(expression, kind, tz, databaseTime) };
|
|
549
560
|
}
|
|
@@ -670,7 +681,9 @@ class Timekeeper extends EventEmitter {
|
|
|
670
681
|
// key and slot are the pass's own bookkeeping, read below rather than sent: send() takes the
|
|
671
682
|
// request the schedule row described and nothing else.
|
|
672
683
|
const { key, slot, ...request } = data;
|
|
673
|
-
|
|
684
|
+
// A row written before 12.37.0 may still carry `db` from schedule(); it is never a send option here.
|
|
685
|
+
const { db, ...options } = request.options ?? {};
|
|
686
|
+
return await this.manager.send({ ...request, options });
|
|
674
687
|
}));
|
|
675
688
|
// Keyed on (name, key) so a batch holding more than one occurrence of the same schedule
|
|
676
689
|
// resolves to its latest. Feeding several to the UPDATE would let postgres pick any of the
|
|
@@ -756,6 +769,7 @@ class Timekeeper extends EventEmitter {
|
|
|
756
769
|
* occurrence after the last one handed back.
|
|
757
770
|
*/
|
|
758
771
|
occurrenceWalker(expression, tz, from) {
|
|
772
|
+
assertExpression(expression);
|
|
759
773
|
if (isRrule(expression)) {
|
|
760
774
|
assertRrule(expression, tz);
|
|
761
775
|
return rruleWalker(expression, tz, from);
|
|
@@ -810,9 +824,12 @@ class Timekeeper extends EventEmitter {
|
|
|
810
824
|
return occurrences;
|
|
811
825
|
}
|
|
812
826
|
async schedule(name, cron, data, options = {}) {
|
|
827
|
+
// `db` is how this call runs, not part of the schedule: it comes out first so the options blob
|
|
828
|
+
// stored on the row, and later handed to send() by the cron pass, never carries a connection.
|
|
829
|
+
const { db, ...persisted } = options;
|
|
813
830
|
// `missed` comes out with tz and key: it tells the pass what to do about a gap and is no more a
|
|
814
831
|
// send option than they are, so the send-option check below is not handed it.
|
|
815
|
-
const { tz: requestedTz, key = '', missed, ...rest } =
|
|
832
|
+
const { tz: requestedTz, key = '', missed, ...rest } = persisted;
|
|
816
833
|
// Any falsy zone is "none specified", not a zone to be judged: a destructuring default only
|
|
817
834
|
// covers `undefined`, and a value threaded out of a config object or read back off the nullable
|
|
818
835
|
// timezone column arrives as null. Coalescing it here is what keeps the two engines from
|
|
@@ -838,7 +855,7 @@ class Timekeeper extends EventEmitter {
|
|
|
838
855
|
assertMissedPolicy(missed);
|
|
839
856
|
try {
|
|
840
857
|
const sql = plans.schedule(this.config.schema);
|
|
841
|
-
await this.db.executeSql(sql, [name, key, kind, cron, tz, data,
|
|
858
|
+
await (db || this.db).executeSql(sql, [name, key, kind, cron, tz, data, persisted]);
|
|
842
859
|
}
|
|
843
860
|
catch (err) {
|
|
844
861
|
if (err.message.includes('foreign key')) {
|
|
@@ -847,9 +864,9 @@ class Timekeeper extends EventEmitter {
|
|
|
847
864
|
throw err;
|
|
848
865
|
}
|
|
849
866
|
}
|
|
850
|
-
async unschedule(name, key = '') {
|
|
867
|
+
async unschedule(name, key = '', options = {}) {
|
|
851
868
|
const sql = plans.unschedule(this.config.schema);
|
|
852
|
-
await this.db.executeSql(sql, [name, key]);
|
|
869
|
+
await (options.db || this.db).executeSql(sql, [name, key]);
|
|
853
870
|
}
|
|
854
871
|
}
|
|
855
872
|
export default Timekeeper;
|