@bymax-one/nest-queue 1.2.12 → 1.2.14
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/CHANGELOG.md +121 -1
- package/README.md +319 -345
- package/dist/server/index.cjs +273 -95
- package/dist/server/index.d.cts +87 -24
- package/dist/server/index.d.ts +87 -24
- package/dist/server/index.mjs +273 -95
- package/package.json +1 -1
package/dist/server/index.mjs
CHANGED
|
@@ -76,6 +76,12 @@ var QueueException = class extends HttpException {
|
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
+
// src/server/utils/timer-delay.ts
|
|
80
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
81
|
+
function isTimerDelayMs(value) {
|
|
82
|
+
return Number.isInteger(value) && value >= 1 && value <= MAX_TIMER_DELAY_MS;
|
|
83
|
+
}
|
|
84
|
+
|
|
79
85
|
// src/server/config/validate-options.ts
|
|
80
86
|
function invalid(reason) {
|
|
81
87
|
throw new QueueException(QUEUE_ERROR_CODES.INVALID_OPTIONS, HttpStatus.INTERNAL_SERVER_ERROR, {
|
|
@@ -126,6 +132,12 @@ function validateOptions(opts) {
|
|
|
126
132
|
if (cacheTtlMs !== void 0 && cacheTtlMs < 0) {
|
|
127
133
|
invalid("metrics.cacheTtlMs must be >= 0");
|
|
128
134
|
}
|
|
135
|
+
for (const key of ["commandTimeoutMs", "enqueueTimeoutMs"]) {
|
|
136
|
+
const value = opts[key];
|
|
137
|
+
if (value !== void 0 && !isTimerDelayMs(value)) {
|
|
138
|
+
invalid(`${key} must be a whole number from 1 to ${String(MAX_TIMER_DELAY_MS)}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
129
141
|
}
|
|
130
142
|
|
|
131
143
|
// src/server/constants/default-options.ts
|
|
@@ -133,11 +145,14 @@ var DEFAULT_WORKER_CONCURRENCY = 2;
|
|
|
133
145
|
var MAX_WORKER_CONCURRENCY = 1e3;
|
|
134
146
|
var DEFAULT_JOB_OPTIONS = {
|
|
135
147
|
attempts: 3,
|
|
136
|
-
backoff: { type: "exponential", delay: 2e3 },
|
|
148
|
+
backoff: { type: "exponential", delay: 2e3, jitter: 0.5 },
|
|
137
149
|
removeOnComplete: { age: 24 * 3600, count: 1e3 },
|
|
138
150
|
removeOnFail: { age: 7 * 24 * 3600, count: 5e3 }
|
|
139
151
|
};
|
|
140
152
|
var DEFAULT_CONNECTION_READY_TIMEOUT_MS = 1e4;
|
|
153
|
+
var DEFAULT_COMMAND_TIMEOUT_MS = 5e3;
|
|
154
|
+
var DEFAULT_ENQUEUE_TIMEOUT_MS = 1e4;
|
|
155
|
+
var MAX_JOBS_PAGE_SIZE = 1e3;
|
|
141
156
|
var DEFAULT_METRICS_CACHE_TTL_MS = 5e3;
|
|
142
157
|
var DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS = 3e4;
|
|
143
158
|
|
|
@@ -157,7 +172,9 @@ function applyDefaults(opts) {
|
|
|
157
172
|
drainTimeoutMs: opts.shutdown?.drainTimeoutMs ?? DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS,
|
|
158
173
|
drainOnShutdown: opts.shutdown?.drainOnShutdown ?? false
|
|
159
174
|
},
|
|
160
|
-
connectionReadyTimeoutMs: opts.connectionReadyTimeoutMs ?? DEFAULT_CONNECTION_READY_TIMEOUT_MS
|
|
175
|
+
connectionReadyTimeoutMs: opts.connectionReadyTimeoutMs ?? DEFAULT_CONNECTION_READY_TIMEOUT_MS,
|
|
176
|
+
commandTimeoutMs: opts.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS,
|
|
177
|
+
enqueueTimeoutMs: opts.enqueueTimeoutMs ?? DEFAULT_ENQUEUE_TIMEOUT_MS
|
|
161
178
|
};
|
|
162
179
|
if (opts.telemetry !== void 0) base.telemetry = opts.telemetry;
|
|
163
180
|
Object.defineProperty(base, "connection", {
|
|
@@ -222,7 +239,12 @@ function attachClientErrorListener(client, logger, role) {
|
|
|
222
239
|
|
|
223
240
|
// src/server/utils/duplicate-connection.ts
|
|
224
241
|
function duplicateConnection(client) {
|
|
225
|
-
|
|
242
|
+
const override = {
|
|
243
|
+
maxRetriesPerRequest: null,
|
|
244
|
+
enableOfflineQueue: true,
|
|
245
|
+
commandTimeout: void 0
|
|
246
|
+
};
|
|
247
|
+
return client.duplicate(override);
|
|
226
248
|
}
|
|
227
249
|
|
|
228
250
|
// src/server/services/connection-resolver.service.ts
|
|
@@ -244,12 +266,22 @@ var ConnectionResolver = class {
|
|
|
244
266
|
this.mode = "mode-b-owned";
|
|
245
267
|
__privateSet(this, _client, "url" in cfg ? new Redis(cfg.url, this.ownedOptions(cfg.options)) : new Redis(this.ownedOptions(cfg.options)));
|
|
246
268
|
attachClientErrorListener(__privateGet(this, _client), __privateGet(this, _logger), "owned");
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
269
|
+
try {
|
|
270
|
+
await this.waitReady(
|
|
271
|
+
__privateGet(this, _options).connectionReadyTimeoutMs ?? DEFAULT_CONNECTION_READY_TIMEOUT_MS
|
|
272
|
+
);
|
|
273
|
+
} catch (cause) {
|
|
274
|
+
__privateGet(this, _client).disconnect();
|
|
275
|
+
throw cause;
|
|
276
|
+
}
|
|
250
277
|
}
|
|
251
278
|
ownedOptions(options) {
|
|
252
|
-
return {
|
|
279
|
+
return {
|
|
280
|
+
commandTimeout: __privateGet(this, _options).commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS,
|
|
281
|
+
enableOfflineQueue: false,
|
|
282
|
+
...options ?? {},
|
|
283
|
+
lazyConnect: false
|
|
284
|
+
};
|
|
253
285
|
}
|
|
254
286
|
getClient() {
|
|
255
287
|
if (!__privateGet(this, _client)) {
|
|
@@ -296,6 +328,11 @@ var ConnectionResolver = class {
|
|
|
296
328
|
{ status: client.status }
|
|
297
329
|
);
|
|
298
330
|
}
|
|
331
|
+
if (client.options.maxRetriesPerRequest === null) {
|
|
332
|
+
__privateGet(this, _logger).warn(
|
|
333
|
+
"The shared client has maxRetriesPerRequest: null, so a producer call made while Redis is unreachable waits for as long as the outage lasts; enqueue is bounded by enqueueTimeoutMs, other QueueService calls are not. Give the client a finite limit to fail fast."
|
|
334
|
+
);
|
|
335
|
+
}
|
|
299
336
|
const probe = duplicateConnection(client);
|
|
300
337
|
attachClientErrorListener(probe, __privateGet(this, _logger), "probe");
|
|
301
338
|
try {
|
|
@@ -308,31 +345,35 @@ var ConnectionResolver = class {
|
|
|
308
345
|
const client = this.getClient();
|
|
309
346
|
if (client.status === "ready") return;
|
|
310
347
|
await new Promise((resolve, reject) => {
|
|
348
|
+
let lastErrorCode;
|
|
349
|
+
const details = (extra) => lastErrorCode === void 0 ? extra : { ...extra, lastErrorCode };
|
|
311
350
|
const cleanup = () => {
|
|
312
351
|
clearTimeout(timer);
|
|
313
352
|
client.off("ready", onReady);
|
|
314
353
|
client.off("error", onError);
|
|
354
|
+
client.off("end", onEnd);
|
|
355
|
+
};
|
|
356
|
+
const fail = (code, extra) => {
|
|
357
|
+
cleanup();
|
|
358
|
+
reject(new QueueException(code, HttpStatus.INTERNAL_SERVER_ERROR, details(extra)));
|
|
315
359
|
};
|
|
316
360
|
const onReady = () => {
|
|
317
361
|
cleanup();
|
|
318
362
|
resolve();
|
|
319
363
|
};
|
|
320
364
|
const onError = (err) => {
|
|
321
|
-
|
|
322
|
-
|
|
365
|
+
lastErrorCode = startupErrorCode(err);
|
|
366
|
+
if (isReplyError(err)) fail(QUEUE_ERROR_CODES.CONNECTION_INVALID, {});
|
|
367
|
+
};
|
|
368
|
+
const onEnd = () => {
|
|
369
|
+
fail(QUEUE_ERROR_CODES.CONNECTION_INVALID, {});
|
|
323
370
|
};
|
|
324
371
|
const timer = setTimeout(() => {
|
|
325
|
-
|
|
326
|
-
reject(
|
|
327
|
-
new QueueException(
|
|
328
|
-
QUEUE_ERROR_CODES.CONNECTION_TIMEOUT,
|
|
329
|
-
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
330
|
-
{ timeoutMs }
|
|
331
|
-
)
|
|
332
|
-
);
|
|
372
|
+
fail(QUEUE_ERROR_CODES.CONNECTION_TIMEOUT, { timeoutMs });
|
|
333
373
|
}, timeoutMs);
|
|
334
374
|
client.once("ready", onReady);
|
|
335
|
-
client.
|
|
375
|
+
client.on("error", onError);
|
|
376
|
+
client.once("end", onEnd);
|
|
336
377
|
});
|
|
337
378
|
}
|
|
338
379
|
};
|
|
@@ -343,6 +384,16 @@ ConnectionResolver = __decorateClass([
|
|
|
343
384
|
Injectable(),
|
|
344
385
|
__decorateParam(0, Inject(BYMAX_QUEUE_OPTIONS))
|
|
345
386
|
], ConnectionResolver);
|
|
387
|
+
function isReplyError(err) {
|
|
388
|
+
return typeof err === "object" && err !== null && err.name === "ReplyError";
|
|
389
|
+
}
|
|
390
|
+
function startupErrorCode(err) {
|
|
391
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
392
|
+
const { code, message } = err;
|
|
393
|
+
if (typeof code === "string") return code;
|
|
394
|
+
if (isReplyError(err) && typeof message === "string") return /^[A-Z][A-Z0-9_]*/.exec(message)?.[0];
|
|
395
|
+
return void 0;
|
|
396
|
+
}
|
|
346
397
|
function invalid2(reason) {
|
|
347
398
|
throw new QueueException(QUEUE_ERROR_CODES.INVALID_REPEAT_OPTIONS, HttpStatus.BAD_REQUEST, {
|
|
348
399
|
reason
|
|
@@ -382,6 +433,38 @@ function attachDefaultErrorListener(emitter, logger, kind, queueName) {
|
|
|
382
433
|
});
|
|
383
434
|
}
|
|
384
435
|
|
|
436
|
+
// src/server/utils/with-deadline.ts
|
|
437
|
+
function withDeadline(work, timeoutMs, onTimeout) {
|
|
438
|
+
let timer;
|
|
439
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
440
|
+
timer = setTimeout(() => {
|
|
441
|
+
reject(onTimeout());
|
|
442
|
+
}, timeoutMs);
|
|
443
|
+
});
|
|
444
|
+
return Promise.race([work, deadline]).finally(() => {
|
|
445
|
+
clearTimeout(timer);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
function timeoutException(operation, timeoutMs, cause) {
|
|
449
|
+
const exception = new QueueException(
|
|
450
|
+
QUEUE_ERROR_CODES.CONNECTION_TIMEOUT,
|
|
451
|
+
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
452
|
+
{ operation, timeoutMs }
|
|
453
|
+
);
|
|
454
|
+
if (cause !== void 0) exception.cause = cause;
|
|
455
|
+
return exception;
|
|
456
|
+
}
|
|
457
|
+
function isCommandTimeout(err) {
|
|
458
|
+
return err instanceof Error && err.message === "Command timed out";
|
|
459
|
+
}
|
|
460
|
+
async function withCommandTimeoutCode(operation, commandTimeoutMs, work) {
|
|
461
|
+
try {
|
|
462
|
+
return await work;
|
|
463
|
+
} catch (err) {
|
|
464
|
+
throw isCommandTimeout(err) ? timeoutException(operation, commandTimeoutMs(), err) : err;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
385
468
|
// src/server/services/queue.service.ts
|
|
386
469
|
var MAX_BULK_SIZE = 1e3;
|
|
387
470
|
function assertValidCron(repeat, queueOptions) {
|
|
@@ -419,11 +502,28 @@ var QueueService = class {
|
|
|
419
502
|
__privateGet(this, _queues).set(queueName, queue);
|
|
420
503
|
return queue;
|
|
421
504
|
}
|
|
505
|
+
enqueueTimeout(operation) {
|
|
506
|
+
return timeoutException(operation, this.options.enqueueTimeoutMs);
|
|
507
|
+
}
|
|
508
|
+
call(operation, work) {
|
|
509
|
+
return withCommandTimeoutCode(
|
|
510
|
+
operation,
|
|
511
|
+
() => this.connection.getClient().options.commandTimeout ?? this.options.commandTimeoutMs,
|
|
512
|
+
work
|
|
513
|
+
);
|
|
514
|
+
}
|
|
422
515
|
queueFor(queueName) {
|
|
423
516
|
return this.getOrCreateQueue(queueName);
|
|
424
517
|
}
|
|
425
518
|
async enqueue(queueName, jobName, data, options) {
|
|
426
|
-
const job = await this.
|
|
519
|
+
const job = await this.call(
|
|
520
|
+
"enqueue",
|
|
521
|
+
withDeadline(
|
|
522
|
+
this.queueFor(queueName).add(jobName, data, options),
|
|
523
|
+
this.options.enqueueTimeoutMs,
|
|
524
|
+
() => this.enqueueTimeout("enqueue")
|
|
525
|
+
)
|
|
526
|
+
);
|
|
427
527
|
return job;
|
|
428
528
|
}
|
|
429
529
|
async enqueueBulk(queueName, jobs) {
|
|
@@ -439,15 +539,25 @@ var QueueService = class {
|
|
|
439
539
|
);
|
|
440
540
|
}
|
|
441
541
|
try {
|
|
442
|
-
const created = await this.
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
542
|
+
const created = await this.call(
|
|
543
|
+
"enqueueBulk",
|
|
544
|
+
withDeadline(
|
|
545
|
+
this.queueFor(queueName).addBulk(
|
|
546
|
+
jobs.map((job) => ({
|
|
547
|
+
name: job.name,
|
|
548
|
+
data: job.data,
|
|
549
|
+
...job.options === void 0 ? {} : { opts: job.options }
|
|
550
|
+
}))
|
|
551
|
+
),
|
|
552
|
+
this.options.enqueueTimeoutMs,
|
|
553
|
+
() => this.enqueueTimeout("enqueueBulk")
|
|
554
|
+
)
|
|
448
555
|
);
|
|
449
556
|
return created;
|
|
450
557
|
} catch (err) {
|
|
558
|
+
if (err instanceof QueueException && errorCodeOf(err) === QUEUE_ERROR_CODES.CONNECTION_TIMEOUT) {
|
|
559
|
+
throw err;
|
|
560
|
+
}
|
|
451
561
|
throw new QueueException(
|
|
452
562
|
QUEUE_ERROR_CODES.BULK_ENQUEUE_FAILED,
|
|
453
563
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
@@ -458,20 +568,22 @@ var QueueService = class {
|
|
|
458
568
|
}
|
|
459
569
|
}
|
|
460
570
|
async getJob(queueName, jobId) {
|
|
461
|
-
const job = await this.queueFor(queueName).getJob(jobId);
|
|
571
|
+
const job = await this.call("getJob", this.queueFor(queueName).getJob(jobId));
|
|
462
572
|
return job ?? null;
|
|
463
573
|
}
|
|
464
574
|
async getJobs(queueName, status, start = 0, end = 50) {
|
|
465
|
-
|
|
575
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end - start >= MAX_JOBS_PAGE_SIZE) {
|
|
576
|
+
throw new QueueException(QUEUE_ERROR_CODES.INVALID_OPTIONS, HttpStatus.BAD_REQUEST, {
|
|
577
|
+
reason: `getJobs needs integers with 0 <= start <= end and at most ${String(MAX_JOBS_PAGE_SIZE)} jobs per call`
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
const jobs = await this.call("getJobs", this.queueFor(queueName).getJobs([status], start, end));
|
|
466
581
|
return jobs;
|
|
467
582
|
}
|
|
468
583
|
async getMetrics(queueName) {
|
|
469
|
-
const counts = await this.
|
|
470
|
-
"
|
|
471
|
-
"active",
|
|
472
|
-
"completed",
|
|
473
|
-
"failed",
|
|
474
|
-
"delayed"
|
|
584
|
+
const counts = await this.call(
|
|
585
|
+
"getMetrics",
|
|
586
|
+
this.queueFor(queueName).getJobCounts("waiting", "active", "completed", "failed", "delayed")
|
|
475
587
|
);
|
|
476
588
|
return {
|
|
477
589
|
queue: queueName,
|
|
@@ -488,23 +600,26 @@ var QueueService = class {
|
|
|
488
600
|
...template?.opts === void 0 ? {} : { opts: template.opts }
|
|
489
601
|
};
|
|
490
602
|
if ("pattern" in repeat) assertValidCron(repeat, this.options.queueOptions);
|
|
491
|
-
const job = await
|
|
603
|
+
const job = await this.call(
|
|
604
|
+
"upsertJobScheduler",
|
|
605
|
+
queue.upsertJobScheduler(schedulerId, repeat, jobTemplate)
|
|
606
|
+
);
|
|
492
607
|
return job;
|
|
493
608
|
}
|
|
494
609
|
async removeJobScheduler(queueName, schedulerId) {
|
|
495
|
-
return this.queueFor(queueName).removeJobScheduler(schedulerId);
|
|
610
|
+
return this.call("removeJobScheduler", this.queueFor(queueName).removeJobScheduler(schedulerId));
|
|
496
611
|
}
|
|
497
612
|
async getJobSchedulers(queueName, start = 0, end = 50, asc = true) {
|
|
498
|
-
return this.queueFor(queueName).getJobSchedulers(start, end, asc);
|
|
613
|
+
return this.call("getJobSchedulers", this.queueFor(queueName).getJobSchedulers(start, end, asc));
|
|
499
614
|
}
|
|
500
615
|
async pauseQueue(queueName) {
|
|
501
|
-
await this.queueFor(queueName).pause();
|
|
616
|
+
await this.call("pauseQueue", this.queueFor(queueName).pause());
|
|
502
617
|
}
|
|
503
618
|
async resumeQueue(queueName) {
|
|
504
|
-
await this.queueFor(queueName).resume();
|
|
619
|
+
await this.call("resumeQueue", this.queueFor(queueName).resume());
|
|
505
620
|
}
|
|
506
621
|
async cleanQueue(queueName, gracePeriodMs, limit, status) {
|
|
507
|
-
return this.queueFor(queueName).clean(gracePeriodMs, limit, status);
|
|
622
|
+
return this.call("cleanQueue", this.queueFor(queueName).clean(gracePeriodMs, limit, status));
|
|
508
623
|
}
|
|
509
624
|
getCachedQueues() {
|
|
510
625
|
return __privateGet(this, _queues);
|
|
@@ -522,10 +637,16 @@ QueueService = __decorateClass([
|
|
|
522
637
|
__decorateParam(0, Inject(ConnectionResolver)),
|
|
523
638
|
__decorateParam(1, Inject(BYMAX_QUEUE_RESOLVED_OPTIONS))
|
|
524
639
|
], QueueService);
|
|
640
|
+
function errorCodeOf(err) {
|
|
641
|
+
return err.getResponse().error.code;
|
|
642
|
+
}
|
|
643
|
+
var _connection;
|
|
525
644
|
var FlowService = class {
|
|
526
|
-
logger = new Logger(FlowService.name);
|
|
527
|
-
producer;
|
|
528
645
|
constructor(connection, enabled, prefix, telemetry) {
|
|
646
|
+
__publicField(this, "logger", new Logger(FlowService.name));
|
|
647
|
+
__publicField(this, "producer");
|
|
648
|
+
__privateAdd(this, _connection);
|
|
649
|
+
__privateSet(this, _connection, connection);
|
|
529
650
|
if (enabled) {
|
|
530
651
|
this.producer = new FlowProducer({
|
|
531
652
|
connection: connection.getClient(),
|
|
@@ -536,10 +657,21 @@ var FlowService = class {
|
|
|
536
657
|
}
|
|
537
658
|
}
|
|
538
659
|
async add(flow) {
|
|
539
|
-
return
|
|
660
|
+
return withCommandTimeoutCode(
|
|
661
|
+
"addFlow",
|
|
662
|
+
() => this.commandTimeout(),
|
|
663
|
+
this.requireProducer().add(flow)
|
|
664
|
+
);
|
|
540
665
|
}
|
|
541
666
|
async addBulk(flows) {
|
|
542
|
-
return
|
|
667
|
+
return withCommandTimeoutCode(
|
|
668
|
+
"addFlowBulk",
|
|
669
|
+
() => this.commandTimeout(),
|
|
670
|
+
this.requireProducer().addBulk([...flows])
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
commandTimeout() {
|
|
674
|
+
return __privateGet(this, _connection).getClient().options.commandTimeout ?? 0;
|
|
543
675
|
}
|
|
544
676
|
getProducer() {
|
|
545
677
|
return this.requireProducer();
|
|
@@ -556,6 +688,7 @@ var FlowService = class {
|
|
|
556
688
|
return this.producer;
|
|
557
689
|
}
|
|
558
690
|
};
|
|
691
|
+
_connection = new WeakMap();
|
|
559
692
|
FlowService = __decorateClass([
|
|
560
693
|
Injectable()
|
|
561
694
|
], FlowService);
|
|
@@ -619,7 +752,11 @@ var WorkerRegistry = class {
|
|
|
619
752
|
const bullOpts = this.buildBullOptions(options, conn);
|
|
620
753
|
let worker;
|
|
621
754
|
try {
|
|
622
|
-
worker = new Worker(
|
|
755
|
+
worker = new Worker(
|
|
756
|
+
queueName,
|
|
757
|
+
toProcessor(handler, options?.jobTimeoutMs),
|
|
758
|
+
bullOpts
|
|
759
|
+
);
|
|
623
760
|
} catch (cause) {
|
|
624
761
|
conn.disconnect();
|
|
625
762
|
throw new QueueException(
|
|
@@ -637,6 +774,13 @@ var WorkerRegistry = class {
|
|
|
637
774
|
}
|
|
638
775
|
registerSandboxed(config) {
|
|
639
776
|
const { queueName, processorFile, options } = config;
|
|
777
|
+
if (options?.jobTimeoutMs !== void 0) {
|
|
778
|
+
throw new QueueException(
|
|
779
|
+
QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
|
|
780
|
+
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
781
|
+
{ reason: "jobTimeoutMs is not available for sandboxed workers" }
|
|
782
|
+
);
|
|
783
|
+
}
|
|
640
784
|
this.validateProcessorFile(processorFile);
|
|
641
785
|
this.guardDuplicate(queueName);
|
|
642
786
|
this.validateOptions(options);
|
|
@@ -666,9 +810,12 @@ var WorkerRegistry = class {
|
|
|
666
810
|
async unregister(queueName) {
|
|
667
811
|
const entry = __privateGet(this, _entries).get(queueName);
|
|
668
812
|
if (!entry) return;
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
813
|
+
try {
|
|
814
|
+
await entry.worker.close();
|
|
815
|
+
} finally {
|
|
816
|
+
await this.closeConnection(entry.connection);
|
|
817
|
+
__privateGet(this, _entries).delete(queueName);
|
|
818
|
+
}
|
|
672
819
|
}
|
|
673
820
|
list() {
|
|
674
821
|
return Array.from(__privateGet(this, _entries).keys());
|
|
@@ -722,6 +869,7 @@ var WorkerRegistry = class {
|
|
|
722
869
|
);
|
|
723
870
|
}
|
|
724
871
|
}
|
|
872
|
+
this.validateJobTimeout(opts);
|
|
725
873
|
if (opts?.limiter !== void 0) {
|
|
726
874
|
if (opts.limiter.max < 1) {
|
|
727
875
|
throw new QueueException(
|
|
@@ -745,6 +893,18 @@ var WorkerRegistry = class {
|
|
|
745
893
|
}
|
|
746
894
|
}
|
|
747
895
|
}
|
|
896
|
+
validateJobTimeout(opts) {
|
|
897
|
+
if (opts?.jobTimeoutMs !== void 0 && !isTimerDelayMs(opts.jobTimeoutMs)) {
|
|
898
|
+
throw new QueueException(
|
|
899
|
+
QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
|
|
900
|
+
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
901
|
+
{
|
|
902
|
+
reason: "jobTimeoutMs must be a whole number from 1 to 2147483647",
|
|
903
|
+
actual: opts.jobTimeoutMs
|
|
904
|
+
}
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
748
908
|
validateProcessorFile(processorFile) {
|
|
749
909
|
if (processorFile instanceof URL) {
|
|
750
910
|
if (processorFile.protocol !== "file:") {
|
|
@@ -802,6 +962,34 @@ WorkerRegistry = __decorateClass([
|
|
|
802
962
|
__decorateParam(0, Inject(ConnectionResolver)),
|
|
803
963
|
__decorateParam(1, Inject(BYMAX_QUEUE_RESOLVED_OPTIONS))
|
|
804
964
|
], WorkerRegistry);
|
|
965
|
+
var JobTimeoutError = class extends Error {
|
|
966
|
+
constructor(timeoutMs) {
|
|
967
|
+
super(`queue.job_timeout: the handler did not settle within ${String(timeoutMs)}ms`);
|
|
968
|
+
this.name = "JobTimeoutError";
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
function toProcessor(handler, timeoutMs) {
|
|
972
|
+
if (timeoutMs === void 0) return (job, _token, signal) => handler(job, signal);
|
|
973
|
+
return (job, _token, signal) => {
|
|
974
|
+
const own = new AbortController();
|
|
975
|
+
const relay = () => {
|
|
976
|
+
own.abort(signal?.reason);
|
|
977
|
+
};
|
|
978
|
+
if (signal?.aborted === true) relay();
|
|
979
|
+
signal?.addEventListener("abort", relay);
|
|
980
|
+
return withDeadline(
|
|
981
|
+
Promise.resolve().then(() => handler(job, own.signal)),
|
|
982
|
+
timeoutMs,
|
|
983
|
+
() => {
|
|
984
|
+
const error = new JobTimeoutError(timeoutMs);
|
|
985
|
+
own.abort(error);
|
|
986
|
+
return error;
|
|
987
|
+
}
|
|
988
|
+
).finally(() => {
|
|
989
|
+
signal?.removeEventListener("abort", relay);
|
|
990
|
+
});
|
|
991
|
+
};
|
|
992
|
+
}
|
|
805
993
|
var _events, _connections;
|
|
806
994
|
var QueueEventsRegistry = class {
|
|
807
995
|
constructor(connection, options) {
|
|
@@ -891,10 +1079,10 @@ var ProcessorDiscoveryService = class {
|
|
|
891
1079
|
catchAll = fn;
|
|
892
1080
|
}
|
|
893
1081
|
}
|
|
894
|
-
return (job) => {
|
|
1082
|
+
return (job, signal) => {
|
|
895
1083
|
const specific = namedHandlers.get(job.name);
|
|
896
|
-
if (specific) return specific(job);
|
|
897
|
-
if (catchAll) return catchAll(job);
|
|
1084
|
+
if (specific) return specific(job, signal);
|
|
1085
|
+
if (catchAll) return catchAll(job, signal);
|
|
898
1086
|
return Promise.reject(
|
|
899
1087
|
new Error(
|
|
900
1088
|
`No handler registered for job name "${job.name}" on queue \u2014 add a matching @Process("name") or a catch-all @Process() method.`
|
|
@@ -1002,9 +1190,10 @@ var QueueLifecycle = class {
|
|
|
1002
1190
|
forcedWorkers = 0;
|
|
1003
1191
|
async onModuleDestroy() {
|
|
1004
1192
|
this.startedAt = Date.now();
|
|
1005
|
-
this.
|
|
1006
|
-
await this.
|
|
1007
|
-
await this.
|
|
1193
|
+
const deadline = this.startedAt + this.resolved.shutdown.drainTimeoutMs;
|
|
1194
|
+
this.forcedWorkers = await this.closeWorkers(deadline);
|
|
1195
|
+
await this.closeQueueEvents(deadline);
|
|
1196
|
+
await this.closeDuplicatedConnections(deadline);
|
|
1008
1197
|
}
|
|
1009
1198
|
async onApplicationShutdown() {
|
|
1010
1199
|
await this.drainQueuesIfRequested();
|
|
@@ -1015,56 +1204,42 @@ var QueueLifecycle = class {
|
|
|
1015
1204
|
`Queue shutdown complete in ${String(Date.now() - (this.startedAt ?? Date.now()))}ms (forced ${String(this.forcedWorkers)} worker(s))`
|
|
1016
1205
|
);
|
|
1017
1206
|
}
|
|
1018
|
-
async closeWorkers() {
|
|
1207
|
+
async closeWorkers(deadline) {
|
|
1019
1208
|
let forced = 0;
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1209
|
+
await Promise.all(
|
|
1210
|
+
Array.from(this.workers.getAll(), async ([name, worker]) => {
|
|
1211
|
+
try {
|
|
1212
|
+
await bounded(worker.close(), deadline);
|
|
1213
|
+
} catch (cause) {
|
|
1214
|
+
forced++;
|
|
1215
|
+
this.logger.warn(
|
|
1216
|
+
cause instanceof DrainTimeoutError ? `Worker "${name}" exceeded the ${String(this.resolved.shutdown.drainTimeoutMs)}ms drain budget \u2014 forcing close (${QUEUE_ERROR_CODES.SHUTDOWN_TIMEOUT_EXCEEDED}); the in-flight job becomes stalled and is retried` : `Worker "${name}" failed to close gracefully (${describeError(cause)}) \u2014 forcing close; the in-flight job becomes stalled and is retried`
|
|
1217
|
+
);
|
|
1218
|
+
void worker.close(true).catch(() => void 0);
|
|
1219
|
+
}
|
|
1220
|
+
})
|
|
1221
|
+
);
|
|
1032
1222
|
return forced;
|
|
1033
1223
|
}
|
|
1034
|
-
async
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
closed,
|
|
1041
|
-
new Promise((_resolve, reject) => {
|
|
1042
|
-
timer = setTimeout(() => {
|
|
1043
|
-
reject(new DrainTimeoutError());
|
|
1044
|
-
}, timeoutMs);
|
|
1045
|
-
})
|
|
1046
|
-
]);
|
|
1047
|
-
} finally {
|
|
1048
|
-
clearTimeout(timer);
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
async closeQueueEvents() {
|
|
1052
|
-
for (const [, queueEvents] of this.events.getAll()) {
|
|
1053
|
-
await queueEvents.close().catch(() => void 0);
|
|
1054
|
-
}
|
|
1224
|
+
async closeQueueEvents(deadline) {
|
|
1225
|
+
await Promise.all(
|
|
1226
|
+
Array.from(this.events.getAll().values(), async (queueEvents) => {
|
|
1227
|
+
await bounded(queueEvents.close(), deadline).catch(() => void 0);
|
|
1228
|
+
})
|
|
1229
|
+
);
|
|
1055
1230
|
}
|
|
1056
|
-
async closeDuplicatedConnections() {
|
|
1231
|
+
async closeDuplicatedConnections(deadline) {
|
|
1057
1232
|
const connections = [
|
|
1058
1233
|
...this.workers.getConnections().values(),
|
|
1059
1234
|
...this.events.getConnections().values()
|
|
1060
1235
|
];
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
await connection.quit()
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1236
|
+
await Promise.all(
|
|
1237
|
+
connections.map(async (connection) => {
|
|
1238
|
+
await bounded(connection.quit(), deadline).catch(() => {
|
|
1239
|
+
connection.disconnect();
|
|
1240
|
+
});
|
|
1241
|
+
})
|
|
1242
|
+
);
|
|
1068
1243
|
}
|
|
1069
1244
|
async drainQueuesIfRequested() {
|
|
1070
1245
|
if (!this.resolved.shutdown.drainOnShutdown) return;
|
|
@@ -1095,6 +1270,9 @@ QueueLifecycle = __decorateClass([
|
|
|
1095
1270
|
__decorateParam(4, Inject(ConnectionResolver)),
|
|
1096
1271
|
__decorateParam(5, Inject(BYMAX_QUEUE_RESOLVED_OPTIONS))
|
|
1097
1272
|
], QueueLifecycle);
|
|
1273
|
+
function bounded(work, deadline) {
|
|
1274
|
+
return withDeadline(work, Math.max(0, deadline - Date.now()), () => new DrainTimeoutError());
|
|
1275
|
+
}
|
|
1098
1276
|
|
|
1099
1277
|
// src/server/bymax-queue.module.ts
|
|
1100
1278
|
var { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = (
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bymax-one/nest-queue",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.14",
|
|
4
4
|
"description": "NestJS dynamic module wrapping BullMQ — typed jobs, flows, job schedulers, deduplication, OpenTelemetry, graceful shutdown",
|
|
5
5
|
"author": "Bymax One <support@bymax.one>",
|
|
6
6
|
"license": "MIT",
|