@gobing-ai/ts-infra 0.4.59 → 0.4.62
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 +14 -4
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +3 -0
- package/dist/application/types.d.ts +11 -2
- package/dist/application/types.d.ts.map +1 -1
- package/dist/application-node.d.ts.map +1 -1
- package/dist/application-node.js +34 -4
- package/dist/execution-policy.d.ts +76 -0
- package/dist/execution-policy.d.ts.map +1 -0
- package/dist/execution-policy.js +123 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/job-queue/db-job-queue.d.ts +14 -1
- package/dist/job-queue/db-job-queue.d.ts.map +1 -1
- package/dist/job-queue/db-job-queue.js +165 -26
- package/dist/job-queue/types.d.ts +47 -7
- package/dist/job-queue/types.d.ts.map +1 -1
- package/dist/scheduler/cloudflare.d.ts +4 -0
- package/dist/scheduler/cloudflare.d.ts.map +1 -1
- package/dist/scheduler/cloudflare.js +2 -1
- package/dist/scheduler/index.d.ts +1 -1
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/node.d.ts +10 -1
- package/dist/scheduler/node.d.ts.map +1 -1
- package/dist/scheduler/node.js +28 -6
- package/dist/scheduler/types.d.ts +20 -3
- package/dist/scheduler/types.d.ts.map +1 -1
- package/dist/scheduler/wrap-handler.d.ts.map +1 -1
- package/dist/scheduler/wrap-handler.js +2 -2
- package/package.json +6 -6
- package/src/application/index.ts +3 -0
- package/src/application/types.ts +16 -3
- package/src/application-node.ts +40 -4
- package/src/execution-policy.ts +168 -0
- package/src/index.ts +3 -1
- package/src/job-queue/db-job-queue.ts +184 -25
- package/src/job-queue/types.ts +47 -7
- package/src/scheduler/cloudflare.ts +3 -1
- package/src/scheduler/index.ts +6 -1
- package/src/scheduler/node.ts +39 -6
- package/src/scheduler/types.ts +34 -5
- package/src/scheduler/wrap-handler.ts +3 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveExecutionTimeoutMs, runWithExecutionDeadline } from '../execution-policy.js';
|
|
1
2
|
import { settleWithin } from '../internals/drain.js';
|
|
2
3
|
import { getLogger } from '../logger.js';
|
|
3
4
|
import { getQueueJobCompletedTotal, getQueueJobEnqueuedTotal, getQueueJobFailedTotal, getQueueJobProcessingDuration, } from '../telemetry/metrics.js';
|
|
@@ -51,7 +52,7 @@ function enqueuedDetail(jobId, type, options) {
|
|
|
51
52
|
detail.ttlMs = options.ttlMs;
|
|
52
53
|
return detail;
|
|
53
54
|
}
|
|
54
|
-
/** DB-backed queue consumer with polling, retry, and
|
|
55
|
+
/** DB-backed queue consumer with polling, retry, lease ownership, and execution-deadline handling. */
|
|
55
56
|
export class DBQueueConsumer {
|
|
56
57
|
dao;
|
|
57
58
|
handlers = new Map();
|
|
@@ -62,6 +63,11 @@ export class DBQueueConsumer {
|
|
|
62
63
|
baseDelay;
|
|
63
64
|
maxDelay;
|
|
64
65
|
drainTimeoutMs;
|
|
66
|
+
drainPolicy;
|
|
67
|
+
/** Default execution policy for jobs without their own persisted decision; `null` = unlimited. */
|
|
68
|
+
defaultTimeoutMs;
|
|
69
|
+
/** Live attempts owned by this consumer, keyed by job id — the target of `cancel()`. */
|
|
70
|
+
activeAttempts = new Map();
|
|
65
71
|
/**
|
|
66
72
|
* Validated event sink: the bus plus the non-empty queue name its lifecycle rows
|
|
67
73
|
* require (ADR-068). `undefined` for silent consumers — no `events`, no identity.
|
|
@@ -80,6 +86,15 @@ export class DBQueueConsumer {
|
|
|
80
86
|
this.baseDelay = nonNegativeFiniteConfig('baseDelay', config.baseDelay ?? 1_000);
|
|
81
87
|
this.maxDelay = nonNegativeFiniteConfig('maxDelay', config.maxDelay ?? 60_000);
|
|
82
88
|
this.drainTimeoutMs = nonNegativeFiniteConfig('drainTimeoutMs', config.drainTimeoutMs ?? 30_000);
|
|
89
|
+
this.drainPolicy = config.drainPolicy ?? 'bounded';
|
|
90
|
+
if (config.defaultTimeoutMs !== undefined &&
|
|
91
|
+
config.defaultTimeoutMs !== null &&
|
|
92
|
+
(typeof config.defaultTimeoutMs !== 'number' ||
|
|
93
|
+
!Number.isInteger(config.defaultTimeoutMs) ||
|
|
94
|
+
config.defaultTimeoutMs <= 0)) {
|
|
95
|
+
throw new RangeError(`Queue consumer defaultTimeoutMs must be a positive integer or null; received ${String(config.defaultTimeoutMs)}`);
|
|
96
|
+
}
|
|
97
|
+
this.defaultTimeoutMs = config.defaultTimeoutMs ?? null;
|
|
83
98
|
this.eventSink = resolveEventSink(config);
|
|
84
99
|
}
|
|
85
100
|
register(type, handler) {
|
|
@@ -110,6 +125,7 @@ export class DBQueueConsumer {
|
|
|
110
125
|
clearTimeout(this.timer);
|
|
111
126
|
this.timer = null;
|
|
112
127
|
}
|
|
128
|
+
const drainToCompletion = this.drainPolicy === 'drain-to-completion';
|
|
113
129
|
const deadline = Date.now() + this.drainTimeoutMs;
|
|
114
130
|
// Wait for an already-running poll cycle before consulting `inFlight`.
|
|
115
131
|
// `inFlight` is only incremented once claimReady() has resolved, so a stop()
|
|
@@ -118,10 +134,26 @@ export class DBQueueConsumer {
|
|
|
118
134
|
// to claim and process jobs after stop() resolved.
|
|
119
135
|
const pending = this.pollPromise;
|
|
120
136
|
if (pending !== null) {
|
|
121
|
-
|
|
137
|
+
if (drainToCompletion) {
|
|
138
|
+
await pending;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
await settleWithin(pending, deadline);
|
|
142
|
+
}
|
|
122
143
|
}
|
|
123
|
-
|
|
124
|
-
|
|
144
|
+
if (drainToCompletion) {
|
|
145
|
+
// Shutdown policy (A21): wait for every in-flight attempt to settle,
|
|
146
|
+
// however long that takes. Leases keep renewing while we wait.
|
|
147
|
+
while (this.inFlight > 0) {
|
|
148
|
+
await sleep(10);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
while (this.inFlight > 0 && Date.now() < deadline) {
|
|
153
|
+
await sleep(10);
|
|
154
|
+
}
|
|
155
|
+
// Bounded drain expiry (A21): attempts still running keep ownership —
|
|
156
|
+
// their leases keep renewing and they are acknowledged when they settle.
|
|
125
157
|
}
|
|
126
158
|
if (wasRunning) {
|
|
127
159
|
const drained = this.inFlight === 0;
|
|
@@ -138,6 +170,14 @@ export class DBQueueConsumer {
|
|
|
138
170
|
}
|
|
139
171
|
}
|
|
140
172
|
}
|
|
173
|
+
async cancel(jobId) {
|
|
174
|
+
const attempt = this.activeAttempts.get(jobId);
|
|
175
|
+
if (attempt === undefined)
|
|
176
|
+
return false;
|
|
177
|
+
attempt.state.cancelled = true;
|
|
178
|
+
attempt.controller.abort();
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
141
181
|
async stats() {
|
|
142
182
|
return this.dao.getStats();
|
|
143
183
|
}
|
|
@@ -146,7 +186,7 @@ export class DBQueueConsumer {
|
|
|
146
186
|
return traceAsync('queue.poll', async () => {
|
|
147
187
|
await this.dao.resetStuckJobs(this.visibilityTimeout);
|
|
148
188
|
await this.dao.failExpiredJobs();
|
|
149
|
-
const jobs = await this.dao.claimReady(this.batchSize);
|
|
189
|
+
const jobs = await this.dao.claimReady(this.batchSize, { leaseMs: this.visibilityTimeout });
|
|
150
190
|
let processed = 0;
|
|
151
191
|
for (let index = 0; index < jobs.length; index += this.maxConcurrency) {
|
|
152
192
|
const batch = jobs.slice(index, index + this.maxConcurrency);
|
|
@@ -203,7 +243,7 @@ export class DBQueueConsumer {
|
|
|
203
243
|
catch (error) {
|
|
204
244
|
// Corrupt payload — the job can never parse; route it through the
|
|
205
245
|
// retry/fail path instead of rejecting the whole batch.
|
|
206
|
-
await this.failOrRetry(record, error, 0);
|
|
246
|
+
await this.failOrRetry(record, error, 0, record.attemptToken ?? undefined);
|
|
207
247
|
return;
|
|
208
248
|
}
|
|
209
249
|
return traceAsync('queue.job.process', async () => {
|
|
@@ -213,38 +253,135 @@ export class DBQueueConsumer {
|
|
|
213
253
|
'queue.job_attempt': job.attempts,
|
|
214
254
|
});
|
|
215
255
|
const handler = this.handlers.get(job.type);
|
|
256
|
+
const token = record.attemptToken ?? undefined;
|
|
216
257
|
if (handler === undefined) {
|
|
217
|
-
await this.failOrRetry(job, new Error(`No handler registered for job type "${job.type}"`), 0);
|
|
258
|
+
await this.failOrRetry(job, new Error(`No handler registered for job type "${job.type}"`), 0, token);
|
|
218
259
|
return;
|
|
219
260
|
}
|
|
220
|
-
|
|
261
|
+
// Per-attempt ownership (A21): the fresh claim token fences every
|
|
262
|
+
// acknowledgement, and the external controller carries lease-loss and
|
|
263
|
+
// manual cancellation into the shared deadline clock.
|
|
264
|
+
let attemptWake;
|
|
265
|
+
const attempt = { lost: false, cancelled: false, settled: false };
|
|
266
|
+
const attemptAbort = new AbortController();
|
|
267
|
+
if (token !== undefined) {
|
|
268
|
+
const { promise: settledSignal, resolve: resolveSettled } = Promise.withResolvers();
|
|
269
|
+
attempt.settledSignal = settledSignal;
|
|
270
|
+
attemptWake = resolveSettled;
|
|
271
|
+
// Shares the `attempt` object reference so cancel() mutations are
|
|
272
|
+
// visible in the outcome dispatch below.
|
|
273
|
+
this.activeAttempts.set(record.id, { controller: attemptAbort, state: attempt });
|
|
274
|
+
}
|
|
275
|
+
const renewal = token === undefined
|
|
276
|
+
? undefined
|
|
277
|
+
: this.renewUntilSettled(record.id, token, attempt, attemptAbort).catch(() => { });
|
|
221
278
|
try {
|
|
222
|
-
|
|
223
|
-
|
|
279
|
+
const resolvedTimeout = resolveExecutionTimeoutMs(`job "${job.type}"`, record.timeoutUnlimited === 1 ? null : (record.timeoutMs ?? undefined), this.defaultTimeoutMs);
|
|
280
|
+
const startMs = performance.now();
|
|
281
|
+
const outcome = await runWithExecutionDeadline((context) => handler(job, context), {
|
|
282
|
+
timeoutMs: resolvedTimeout,
|
|
283
|
+
signal: attemptAbort.signal,
|
|
284
|
+
});
|
|
285
|
+
attempt.settled = true;
|
|
286
|
+
attemptWake?.();
|
|
224
287
|
const durationMs = performance.now() - startMs;
|
|
225
|
-
|
|
226
|
-
getQueueJobProcessingDuration().record(durationMs, { type: job.type });
|
|
227
|
-
const completed = {
|
|
228
|
-
jobId: job.id,
|
|
288
|
+
getQueueJobProcessingDuration().record(Number.isFinite(durationMs) ? durationMs : 0, {
|
|
229
289
|
type: job.type,
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
290
|
+
});
|
|
291
|
+
if (attempt.lost) {
|
|
292
|
+
// Ownership was lost: the row now belongs to a replacement attempt
|
|
293
|
+
// claimed through the expired lease. Fence every acknowledgement —
|
|
294
|
+
// a stale ack must never mutate the replacement's work (R4).
|
|
295
|
+
queueLogger().warn('queue attempt lost lease ownership; acknowledgement fenced', {
|
|
296
|
+
jobId: record.id,
|
|
297
|
+
type: job.type,
|
|
298
|
+
outcome: outcome.outcome,
|
|
299
|
+
});
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (outcome.outcome === 'error') {
|
|
303
|
+
await this.failOrRetry(job, outcome.error, Number.isFinite(durationMs) ? durationMs : 0, token);
|
|
304
|
+
}
|
|
305
|
+
else if (outcome.timedOut) {
|
|
306
|
+
// Deadline expiry is a failure like any other, reported only after
|
|
307
|
+
// the handler has settled — even if it ignored the abort.
|
|
308
|
+
await this.failOrRetry(job, outcome.error, Number.isFinite(durationMs) ? durationMs : 0, token);
|
|
309
|
+
}
|
|
310
|
+
else if (attempt.cancelled) {
|
|
311
|
+
// Manual cancellation is terminal: settle, then fail without retry.
|
|
312
|
+
const message = 'job cancelled';
|
|
313
|
+
const applied = await this.dao.markFailed(record.id, job.attempts + 1, message, token);
|
|
314
|
+
if (applied) {
|
|
315
|
+
getQueueJobFailedTotal().add(1, { type: job.type });
|
|
316
|
+
await this.eventSink?.bus.emit('queue.job.failed', {
|
|
317
|
+
jobId: job.id,
|
|
318
|
+
type: job.type,
|
|
319
|
+
error: message,
|
|
320
|
+
attempt: job.attempts + 1,
|
|
321
|
+
maxRetries: job.maxRetries,
|
|
322
|
+
durationMs: Number.isFinite(durationMs) ? durationMs : 0,
|
|
323
|
+
severity: 'warning',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
await this.dao.markCompleted(record.id, token);
|
|
329
|
+
getQueueJobCompletedTotal().add(1, { type: job.type });
|
|
330
|
+
const completed = {
|
|
331
|
+
jobId: job.id,
|
|
332
|
+
type: job.type,
|
|
333
|
+
durationMs: Number.isFinite(durationMs) ? durationMs : 0,
|
|
334
|
+
attempt: job.attempts,
|
|
335
|
+
severity: 'info',
|
|
336
|
+
};
|
|
337
|
+
await this.eventSink?.bus.emit('queue.job.completed', completed);
|
|
338
|
+
}
|
|
235
339
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
340
|
+
finally {
|
|
341
|
+
attempt.settled = true;
|
|
342
|
+
attemptWake?.();
|
|
343
|
+
if (renewal !== undefined)
|
|
344
|
+
await renewal;
|
|
345
|
+
if (token !== undefined)
|
|
346
|
+
this.activeAttempts.delete(record.id);
|
|
240
347
|
}
|
|
241
348
|
});
|
|
242
349
|
}
|
|
243
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Extend the attempt's lease on an interval derived from the visibility
|
|
352
|
+
* timeout, proving this consumer is still alive and working. A failed or
|
|
353
|
+
* erroring renewal means ownership was lost: abort the attempt so the
|
|
354
|
+
* handler can stop, and mark the attempt fenced.
|
|
355
|
+
*/
|
|
356
|
+
async renewUntilSettled(id, token, attempt, attemptAbort) {
|
|
357
|
+
const intervalMs = Math.max(1, Math.floor(this.visibilityTimeout / 3));
|
|
358
|
+
while (!attempt.settled) {
|
|
359
|
+
// Settle-aware wait: wakes early when the attempt finishes so a long
|
|
360
|
+
// visibility timeout never delays acknowledgement past the handler.
|
|
361
|
+
await Promise.race([sleep(intervalMs), attempt.settledSignal]);
|
|
362
|
+
if (attempt.settled)
|
|
363
|
+
return;
|
|
364
|
+
let renewed = false;
|
|
365
|
+
try {
|
|
366
|
+
renewed = await this.dao.renewLease(id, token, this.visibilityTimeout);
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
renewed = false; // DB unavailability is treated like ownership loss
|
|
370
|
+
}
|
|
371
|
+
if (attempt.settled)
|
|
372
|
+
return;
|
|
373
|
+
if (!renewed) {
|
|
374
|
+
attempt.lost = true;
|
|
375
|
+
attemptAbort.abort();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async failOrRetry(job, error, durationMs, attemptToken) {
|
|
244
381
|
const attempts = job.attempts + 1;
|
|
245
382
|
const message = error instanceof Error ? error.message : String(error);
|
|
246
383
|
if (attempts >= job.maxRetries) {
|
|
247
|
-
await this.dao.markFailed(job.id, attempts, message);
|
|
384
|
+
await this.dao.markFailed(job.id, attempts, message, attemptToken);
|
|
248
385
|
getQueueJobFailedTotal().add(1, { type: job.type });
|
|
249
386
|
const failed = {
|
|
250
387
|
jobId: job.id,
|
|
@@ -260,7 +397,7 @@ export class DBQueueConsumer {
|
|
|
260
397
|
}
|
|
261
398
|
const delay = Math.min(this.maxDelay, this.baseDelay * 2 ** Math.max(0, attempts - 1));
|
|
262
399
|
const nextRetryAt = Date.now() + delay;
|
|
263
|
-
await this.dao.markForRetry(job.id, attempts, message, nextRetryAt);
|
|
400
|
+
await this.dao.markForRetry(job.id, attempts, message, nextRetryAt, attemptToken);
|
|
264
401
|
const retrying = {
|
|
265
402
|
jobId: job.id,
|
|
266
403
|
type: job.type,
|
|
@@ -286,6 +423,8 @@ function toJob(record) {
|
|
|
286
423
|
nextRetryAt: record.nextRetryAt,
|
|
287
424
|
lastError: record.lastError,
|
|
288
425
|
processingAt: record.processingAt,
|
|
426
|
+
// Persisted job policy: explicit unlimited stays `null`, absence stays undefined.
|
|
427
|
+
timeoutMs: record.timeoutUnlimited === 1 ? null : (record.timeoutMs ?? undefined),
|
|
289
428
|
};
|
|
290
429
|
}
|
|
291
430
|
function sleep(ms) {
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { EventBus } from '../event-bus/event-bus';
|
|
8
8
|
import type { QueueEvents } from '../events';
|
|
9
|
-
|
|
9
|
+
import type { ExecutionContext } from '../execution-policy';
|
|
10
|
+
/** A queued job with status tracking, retry metadata, timestamps, and its persisted execution policy. */
|
|
10
11
|
export interface Job<T = unknown> {
|
|
11
12
|
id: string;
|
|
12
13
|
type: string;
|
|
@@ -20,13 +21,26 @@ export interface Job<T = unknown> {
|
|
|
20
21
|
nextRetryAt: number | null;
|
|
21
22
|
lastError: string | null;
|
|
22
23
|
processingAt: number | null;
|
|
24
|
+
/**
|
|
25
|
+
* Job-level execution policy as persisted: a positive integer ms deadline,
|
|
26
|
+
* explicit `null` = unlimited, or `undefined` = no job-level decision (the
|
|
27
|
+
* consumer default applies). Never the resolved per-attempt value.
|
|
28
|
+
*/
|
|
29
|
+
timeoutMs?: number | null;
|
|
23
30
|
}
|
|
24
|
-
/** Options for enqueuing a job: retry policy, delay, and
|
|
31
|
+
/** Options for enqueuing a job: retry policy, delay, TTL, and execution policy. */
|
|
25
32
|
export interface EnqueueOptions {
|
|
26
33
|
/** Total attempts allowed (default 3) — `maxRetries: 1` runs the job once with no retry. */
|
|
27
34
|
maxRetries?: number;
|
|
28
35
|
delay?: number;
|
|
29
36
|
ttlMs?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Job execution policy persisted with the row: a positive integer ms
|
|
39
|
+
* deadline, explicit `null` = unlimited (disables this job's deadline),
|
|
40
|
+
* omitted = no job-level decision (consumers apply their own default).
|
|
41
|
+
* Invalid explicit values are rejected before the row is created.
|
|
42
|
+
*/
|
|
43
|
+
timeoutMs?: number | null;
|
|
30
44
|
}
|
|
31
45
|
/** Producer interface for the job queue — enqueue single jobs or batches. */
|
|
32
46
|
export interface JobQueue<T = unknown> {
|
|
@@ -37,8 +51,12 @@ export interface JobQueue<T = unknown> {
|
|
|
37
51
|
} & EnqueueOptions>): Promise<string[]>;
|
|
38
52
|
stats(): Promise<QueueStats>;
|
|
39
53
|
}
|
|
40
|
-
/**
|
|
41
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Async handler that processes a single job. The context carries the shared
|
|
56
|
+
* execution deadline clock for this attempt; single-argument handlers stay
|
|
57
|
+
* assignable.
|
|
58
|
+
*/
|
|
59
|
+
export type JobHandler<T = unknown> = (job: Job<T>, context: ExecutionContext) => Promise<void>;
|
|
42
60
|
/** Aggregate statistics for a job queue: counts by status. */
|
|
43
61
|
export interface QueueStats {
|
|
44
62
|
pending: number;
|
|
@@ -46,7 +64,7 @@ export interface QueueStats {
|
|
|
46
64
|
completed: number;
|
|
47
65
|
failed: number;
|
|
48
66
|
}
|
|
49
|
-
/** Configuration for a queue consumer: polling, concurrency, and
|
|
67
|
+
/** Configuration for a queue consumer: polling, concurrency, backoff, and the default execution policy. */
|
|
50
68
|
export interface QueueConsumerConfig {
|
|
51
69
|
pollInterval?: number;
|
|
52
70
|
batchSize?: number;
|
|
@@ -56,6 +74,20 @@ export interface QueueConsumerConfig {
|
|
|
56
74
|
maxDelay?: number;
|
|
57
75
|
/** Upper bound (ms, default 30_000) on how long `stop()` waits for in-flight work to drain. */
|
|
58
76
|
drainTimeoutMs?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Shutdown drain policy (A21). `'bounded'` (default): `stop()` gives up after
|
|
79
|
+
* `drainTimeoutMs`; attempts still running keep their leases, keep renewing, and
|
|
80
|
+
* are acknowledged when they settle. `'drain-to-completion'`: `stop()` waits for
|
|
81
|
+
* all in-flight attempts to settle, however long that takes.
|
|
82
|
+
*/
|
|
83
|
+
drainPolicy?: 'bounded' | 'drain-to-completion';
|
|
84
|
+
/**
|
|
85
|
+
* Default execution policy for jobs without their own persisted decision
|
|
86
|
+
* (A21): a positive integer ms deadline or explicit `null` = unlimited.
|
|
87
|
+
* First-match-wins: an explicit job option — including job `null` — beats
|
|
88
|
+
* this default; absence inherits it.
|
|
89
|
+
*/
|
|
90
|
+
defaultTimeoutMs?: number | null;
|
|
59
91
|
/**
|
|
60
92
|
* Identity of the queue this consumer polls. Runtime-required whenever `events` is
|
|
61
93
|
* configured (ADR-068): an observable consumer must never emit an anonymous lifecycle
|
|
@@ -76,10 +108,18 @@ export interface QueueConsumerConfig {
|
|
|
76
108
|
export interface QueueConsumer<T = unknown> {
|
|
77
109
|
register(type: string, handler: JobHandler<T>): void;
|
|
78
110
|
start(): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Request cooperative cancellation of a claimed attempt (A21). The context
|
|
113
|
+
* signal aborts with reason `'cancelled'`; the attempt is allowed to settle
|
|
114
|
+
* and is then failed without retry. Resolves `false` when the job is not
|
|
115
|
+
* currently owned by this consumer.
|
|
116
|
+
*/
|
|
117
|
+
cancel(jobId: string): Promise<boolean>;
|
|
79
118
|
/**
|
|
80
119
|
* Stop polling and drain work already in flight, including a poll cycle that has
|
|
81
|
-
* claimed nothing yet.
|
|
82
|
-
*
|
|
120
|
+
* claimed nothing yet. Under the default `'bounded'` policy this resolves once the
|
|
121
|
+
* drain completes or `drainTimeoutMs` elapses; `'drain-to-completion'` ignores the
|
|
122
|
+
* bound and waits for every in-flight attempt.
|
|
83
123
|
*/
|
|
84
124
|
stop(): Promise<void>;
|
|
85
125
|
stats(): Promise<QueueStats>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/job-queue/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/job-queue/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Workers scheduler adapter using Cron Triggers.
|
|
3
|
+
* Uses minimal local type declarations — no @cloudflare/workers-types dependency.
|
|
4
|
+
*/
|
|
1
5
|
import type { ScheduledAction, SchedulerAdapter } from './types';
|
|
2
6
|
interface CfScheduledEvent {
|
|
3
7
|
cron: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/scheduler/cloudflare.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/scheduler/cloudflare.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEjE,UAAU,gBAAgB;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9C;AAED,UAAU,cAAc;IACpB,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9C;AAED;;;;;GAKG;AACH,qBAAa,0BAA2B,YAAW,gBAAgB;IAC/D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;;IAI9D,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI;IAI/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B;;;;;;OAMG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B;;;OAGG;IACH,oBAAoB,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI;CAmB3E"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Cloudflare Workers scheduler adapter using Cron Triggers.
|
|
3
3
|
* Uses minimal local type declarations — no @cloudflare/workers-types dependency.
|
|
4
4
|
*/
|
|
5
|
+
import { unlimitedExecutionContext } from '../execution-policy.js';
|
|
5
6
|
import { getSchedulerJobDuration, getSchedulerJobExecutedTotal, getSchedulerJobFailedTotal, } from '../telemetry/metrics.js';
|
|
6
7
|
/**
|
|
7
8
|
* Scheduler adapter for Cloudflare Workers Cron Triggers.
|
|
@@ -38,7 +39,7 @@ export class CloudflareSchedulerAdapter {
|
|
|
38
39
|
if (action) {
|
|
39
40
|
const startMs = performance.now();
|
|
40
41
|
getSchedulerJobExecutedTotal().add(1, { cron: event.cron });
|
|
41
|
-
ctx.waitUntil(action()
|
|
42
|
+
ctx.waitUntil(action(unlimitedExecutionContext())
|
|
42
43
|
.catch((error) => {
|
|
43
44
|
getSchedulerJobFailedTotal().add(1, { cron: event.cron });
|
|
44
45
|
throw error;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { ActionRegistry, type CreateDefaultRegistryOptions, createDefaultRegistry, HealthPingAction, type HealthPingWriter, LogAction, QueueStatsAction, type QueueStatsDaoProvider, type SchedulerAction, toScheduledAction, } from './action';
|
|
2
2
|
export { initScheduler } from './factory';
|
|
3
3
|
export { NoopSchedulerAdapter } from './noop';
|
|
4
|
-
export type { ScheduledAction, SchedulerAdapter, SchedulerJobConfig } from './types';
|
|
4
|
+
export type { ScheduledAction, ScheduledActionOptions, SchedulerAdapter, SchedulerJobConfig, } from './types';
|
|
5
5
|
export { wrapScheduledHandler } from './wrap-handler';
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scheduler/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,cAAc,EACd,KAAK,4BAA4B,EACjC,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,SAAS,EACT,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,iBAAiB,GACpB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,QAAQ,CAAC;AAC9C,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scheduler/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,cAAc,EACd,KAAK,4BAA4B,EACjC,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,SAAS,EACT,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,iBAAiB,GACpB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,QAAQ,CAAC;AAC9C,YAAY,EACR,eAAe,EACf,sBAAsB,EACtB,gBAAgB,EAChB,kBAAkB,GACrB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/scheduler/node.d.ts
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
import type { ScheduledAction, SchedulerAdapter } from './types';
|
|
9
9
|
/** Constructor options for {@link NodeSchedulerAdapter}. */
|
|
10
10
|
export interface NodeSchedulerAdapterConfig {
|
|
11
|
+
/**
|
|
12
|
+
* Default execution policy (A21) for entries registered without their own
|
|
13
|
+
* `timeoutMs` option: a positive integer ms deadline or explicit `null` =
|
|
14
|
+
* unlimited. Invalid values throw at construction.
|
|
15
|
+
*/
|
|
16
|
+
readonly timeoutMs?: number | null;
|
|
11
17
|
/**
|
|
12
18
|
* Upper bound (ms) on how long `stop()` waits for an in-flight tick to settle.
|
|
13
19
|
* A hung action is abandoned at this deadline so it cannot block shutdown.
|
|
@@ -32,11 +38,14 @@ export interface NodeSchedulerAdapterConfig {
|
|
|
32
38
|
export declare class NodeSchedulerAdapter implements SchedulerAdapter {
|
|
33
39
|
private readonly entries;
|
|
34
40
|
private readonly drainTimeoutMs;
|
|
41
|
+
private readonly defaultTimeoutMs;
|
|
35
42
|
private readonly now;
|
|
36
43
|
private running;
|
|
37
44
|
private readonly inflight;
|
|
38
45
|
constructor(config?: NodeSchedulerAdapterConfig);
|
|
39
|
-
register(cron: string, action: ScheduledAction
|
|
46
|
+
register(cron: string, action: ScheduledAction, options?: {
|
|
47
|
+
timeoutMs?: number | null;
|
|
48
|
+
}): void;
|
|
40
49
|
private parseEntry;
|
|
41
50
|
start(): Promise<void>;
|
|
42
51
|
stop(): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/scheduler/node.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/scheduler/node.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AA4DjE,4DAA4D;AAC5D,MAAM,WAAW,0BAA0B;IACvC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,qBAAa,oBAAqB,YAAW,gBAAgB;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAsB;IACvD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;gBAEzC,MAAM,GAAE,0BAA+B;IAYnD,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI;IAW9F,OAAO,CAAC,UAAU;IAoBZ,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAStB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAwB3B,OAAO,CAAC,UAAU;IAiBlB;;;;OAIG;IACH,OAAO,CAAC,OAAO;IAwBf;;;;;OAKG;YACW,QAAQ;YAsBR,gBAAgB;CAyBjC"}
|
package/dist/scheduler/node.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* No external cron library dependency — cron expressions are parsed by the
|
|
6
6
|
* internal `scheduler/cron.ts` grammar shared with `application-node.ts`.
|
|
7
7
|
*/
|
|
8
|
+
import { resolveExecutionTimeoutMs, runWithExecutionDeadline } from '../execution-policy.js';
|
|
8
9
|
import { settleWithin } from '../internals/drain.js';
|
|
10
|
+
import { getLogger } from '../logger.js';
|
|
9
11
|
import { getSchedulerJobDuration, getSchedulerJobExecutedTotal, getSchedulerJobFailedTotal, } from '../telemetry/metrics.js';
|
|
10
12
|
import { nextCronTime, parseCronExpression } from './cron.js';
|
|
11
13
|
/** Platform timer maximum for `setTimeout` (2^31 - 1 ms). Longer delays are chunked. */
|
|
@@ -53,6 +55,7 @@ function parseInterval(cron) {
|
|
|
53
55
|
export class NodeSchedulerAdapter {
|
|
54
56
|
entries = [];
|
|
55
57
|
drainTimeoutMs;
|
|
58
|
+
defaultTimeoutMs;
|
|
56
59
|
now;
|
|
57
60
|
running = false;
|
|
58
61
|
inflight = new Set();
|
|
@@ -62,28 +65,33 @@ export class NodeSchedulerAdapter {
|
|
|
62
65
|
throw new RangeError(`NodeSchedulerAdapter drainTimeoutMs must be a non-negative finite number; received ${drainTimeoutMs}`);
|
|
63
66
|
}
|
|
64
67
|
this.drainTimeoutMs = drainTimeoutMs ?? 30_000;
|
|
68
|
+
this.defaultTimeoutMs = resolveExecutionTimeoutMs('NodeSchedulerAdapter', config.timeoutMs ?? undefined);
|
|
65
69
|
this.now = now ?? (() => Date.now());
|
|
66
70
|
}
|
|
67
|
-
register(cron, action) {
|
|
71
|
+
register(cron, action, options) {
|
|
68
72
|
// Fail at registration time: an unsupported expression must never reach
|
|
69
73
|
// start(), where it would otherwise create a silently-wrong interval
|
|
70
74
|
// (task 0060 F7) or a never-firing cron (task 0734 R1).
|
|
71
|
-
const entry = this.parseEntry(cron, action);
|
|
75
|
+
const entry = this.parseEntry(cron, action, options);
|
|
72
76
|
this.entries.push(entry);
|
|
73
77
|
if (this.running) {
|
|
74
78
|
this.startEntry(entry);
|
|
75
79
|
}
|
|
76
80
|
}
|
|
77
|
-
parseEntry(cron, action) {
|
|
81
|
+
parseEntry(cron, action, options) {
|
|
82
|
+
// Resolve the entry's execution policy now: an explicit option (including
|
|
83
|
+
// explicit null) wins, absence inherits the adapter default. Invalid
|
|
84
|
+
// options throw here rather than leaving a tick to discover it mid-flight.
|
|
85
|
+
const timeoutMs = resolveExecutionTimeoutMs(`scheduler entry "${cron}"`, options?.timeoutMs, this.defaultTimeoutMs);
|
|
78
86
|
const intervalMs = parseInterval(cron);
|
|
79
87
|
if (intervalMs !== undefined) {
|
|
80
|
-
return { kind: 'interval', cron, action, intervalMs };
|
|
88
|
+
return { kind: 'interval', cron, action, intervalMs, timeoutMs };
|
|
81
89
|
}
|
|
82
90
|
// Real five-field cron. Validate and verify a next occurrence exists at
|
|
83
91
|
// registration (bounded scan) so an unsatisfiable expression fails here.
|
|
84
92
|
const expr = parseCronExpression(cron);
|
|
85
93
|
const target = nextCronTime(expr, this.now()).getTime();
|
|
86
|
-
return { kind: 'cron', cron, action, expr, target };
|
|
94
|
+
return { kind: 'cron', cron, action, expr, target, timeoutMs };
|
|
87
95
|
}
|
|
88
96
|
async start() {
|
|
89
97
|
if (this.running)
|
|
@@ -187,7 +195,21 @@ export class NodeSchedulerAdapter {
|
|
|
187
195
|
const startMs = performance.now();
|
|
188
196
|
getSchedulerJobExecutedTotal().add(1, { cron: entry.cron });
|
|
189
197
|
try {
|
|
190
|
-
|
|
198
|
+
// One shared deadline clock per tick (A21): expiry aborts the
|
|
199
|
+
// context and the action is always awaited to settlement. Timeout
|
|
200
|
+
// counts as a failure; the adapter keeps ticking.
|
|
201
|
+
const outcome = await runWithExecutionDeadline(entry.action, { timeoutMs: entry.timeoutMs });
|
|
202
|
+
if (outcome.timedOut) {
|
|
203
|
+
getSchedulerJobFailedTotal().add(1, { cron: entry.cron });
|
|
204
|
+
getLogger('scheduler').warn('scheduled tick exceeded its execution deadline', {
|
|
205
|
+
cron: entry.cron,
|
|
206
|
+
timeoutMs: entry.timeoutMs,
|
|
207
|
+
elapsedMs: Math.round(outcome.elapsedMs),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
else if (outcome.outcome === 'error') {
|
|
211
|
+
getSchedulerJobFailedTotal().add(1, { cron: entry.cron });
|
|
212
|
+
}
|
|
191
213
|
}
|
|
192
214
|
catch {
|
|
193
215
|
// Swallow — scheduler errors should not crash the process
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scheduler types and interface.
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import type { ExecutionContext } from '../execution-policy';
|
|
5
|
+
/**
|
|
6
|
+
* Signature for scheduled action handlers. The context carries the shared
|
|
7
|
+
* execution-deadline clock for this tick (A21); zero-argument handlers stay
|
|
8
|
+
* assignable — notably the Cloudflare adapter, which invokes actions with an
|
|
9
|
+
* unlimited execution context (no wall-clock deadline on that runtime).
|
|
10
|
+
*/
|
|
11
|
+
export type ScheduledAction = (context: ExecutionContext) => Promise<void>;
|
|
12
|
+
/** Per-entry execution policy options for {@link SchedulerAdapter.register}. */
|
|
13
|
+
export interface ScheduledActionOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Execution policy for this entry's ticks: a positive integer ms deadline,
|
|
16
|
+
* explicit `null` = unlimited, omitted = inherit the adapter default.
|
|
17
|
+
* Invalid explicit values throw at registration time.
|
|
18
|
+
*/
|
|
19
|
+
timeoutMs?: number | null;
|
|
20
|
+
}
|
|
6
21
|
/**
|
|
7
22
|
* Abstract scheduler interface — implementations for Node and Cloudflare.
|
|
8
23
|
*
|
|
@@ -11,7 +26,7 @@ export type ScheduledAction = () => Promise<void>;
|
|
|
11
26
|
* action is abandoned at the deadline rather than blocking shutdown forever.
|
|
12
27
|
*/
|
|
13
28
|
export interface SchedulerAdapter {
|
|
14
|
-
register(cron: string, action: ScheduledAction): void;
|
|
29
|
+
register(cron: string, action: ScheduledAction, options?: ScheduledActionOptions): void;
|
|
15
30
|
start(): Promise<void>;
|
|
16
31
|
stop(): Promise<void>;
|
|
17
32
|
}
|
|
@@ -29,10 +44,12 @@ export type SchedulerJobConfig = {
|
|
|
29
44
|
readonly command: string;
|
|
30
45
|
readonly intervalMinutes: number;
|
|
31
46
|
readonly cron?: never;
|
|
47
|
+
readonly timeoutMs?: number | null;
|
|
32
48
|
} | {
|
|
33
49
|
readonly name: string;
|
|
34
50
|
readonly command: string;
|
|
35
51
|
readonly cron: string;
|
|
36
52
|
readonly intervalMinutes?: never;
|
|
53
|
+
readonly timeoutMs?: number | null;
|
|
37
54
|
};
|
|
38
55
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/scheduler/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/scheduler/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE3E,gFAAgF;AAChF,MAAM,WAAW,sBAAsB;IACnC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACxF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GACxB;IACI,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC,GACD;IACI,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,eAAe,CAAC,EAAE,KAAK,CAAC;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrap-handler.d.ts","sourceRoot":"","sources":["../../src/scheduler/wrap-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"wrap-handler.d.ts","sourceRoot":"","sources":["../../src/scheduler/wrap-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAGjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAChC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,eAAe,EACvB,SAAS,CAAC,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,IAAI,GAC7C,eAAe,CA4BjB"}
|
|
@@ -17,13 +17,13 @@ import { addSpanAttributes, addSpanEvent, traceAsync } from '../telemetry/tracin
|
|
|
17
17
|
* @param systemBus - Optional bus to emit `scheduler.job.executed` on.
|
|
18
18
|
*/
|
|
19
19
|
export function wrapScheduledHandler(name, action, systemBus) {
|
|
20
|
-
return async () => {
|
|
20
|
+
return async (context) => {
|
|
21
21
|
const startTime = performance.now();
|
|
22
22
|
return traceAsync('scheduler.job', async (span) => {
|
|
23
23
|
addSpanAttributes({ 'scheduler.job_name': name });
|
|
24
24
|
let execError;
|
|
25
25
|
try {
|
|
26
|
-
await action();
|
|
26
|
+
await action(context);
|
|
27
27
|
}
|
|
28
28
|
catch (error) {
|
|
29
29
|
execError = error instanceof Error ? error.message : String(error);
|