@bymax-one/nest-queue 1.2.14 → 1.2.16
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 +50 -1
- package/README.md +77 -68
- package/dist/server/index.cjs +219 -107
- package/dist/server/index.d.cts +57 -23
- package/dist/server/index.d.ts +57 -23
- package/dist/server/index.mjs +219 -107
- package/package.json +1 -1
package/dist/server/index.mjs
CHANGED
|
@@ -88,6 +88,11 @@ function invalid(reason) {
|
|
|
88
88
|
reason
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
|
+
function checkTimerDelay(name, value) {
|
|
92
|
+
if (value !== void 0 && !isTimerDelayMs(value)) {
|
|
93
|
+
invalid(`${name} must be a whole number from 1 to ${String(MAX_TIMER_DELAY_MS)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
91
96
|
function isParseableUrl(url) {
|
|
92
97
|
const candidate = /^rediss?:\/\//i.test(url) ? url : url.startsWith("//") ? `redis:${url}` : `redis://${url}`;
|
|
93
98
|
if (!URL.canParse(candidate)) return false;
|
|
@@ -128,15 +133,13 @@ function validateOptions(opts) {
|
|
|
128
133
|
if (drainTimeoutMs !== void 0 && drainTimeoutMs <= 0) {
|
|
129
134
|
invalid("shutdown.drainTimeoutMs must be > 0");
|
|
130
135
|
}
|
|
136
|
+
checkTimerDelay("shutdown.drainTimeoutMs", drainTimeoutMs);
|
|
131
137
|
const cacheTtlMs = opts.metrics?.cacheTtlMs;
|
|
132
138
|
if (cacheTtlMs !== void 0 && cacheTtlMs < 0) {
|
|
133
139
|
invalid("metrics.cacheTtlMs must be >= 0");
|
|
134
140
|
}
|
|
135
|
-
for (const key of ["commandTimeoutMs", "enqueueTimeoutMs"]) {
|
|
136
|
-
|
|
137
|
-
if (value !== void 0 && !isTimerDelayMs(value)) {
|
|
138
|
-
invalid(`${key} must be a whole number from 1 to ${String(MAX_TIMER_DELAY_MS)}`);
|
|
139
|
-
}
|
|
141
|
+
for (const key of ["commandTimeoutMs", "enqueueTimeoutMs", "connectionReadyTimeoutMs"]) {
|
|
142
|
+
checkTimerDelay(key, opts[key]);
|
|
140
143
|
}
|
|
141
144
|
}
|
|
142
145
|
|
|
@@ -155,6 +158,7 @@ var DEFAULT_ENQUEUE_TIMEOUT_MS = 1e4;
|
|
|
155
158
|
var MAX_JOBS_PAGE_SIZE = 1e3;
|
|
156
159
|
var DEFAULT_METRICS_CACHE_TTL_MS = 5e3;
|
|
157
160
|
var DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS = 3e4;
|
|
161
|
+
var MAX_BULK_SIZE = 1e3;
|
|
158
162
|
|
|
159
163
|
// src/server/config/resolved-options.ts
|
|
160
164
|
function applyDefaults(opts) {
|
|
@@ -248,13 +252,14 @@ function duplicateConnection(client) {
|
|
|
248
252
|
}
|
|
249
253
|
|
|
250
254
|
// src/server/services/connection-resolver.service.ts
|
|
251
|
-
var _logger, _client, _options;
|
|
255
|
+
var _logger, _client, _options, _pendingReady;
|
|
252
256
|
var ConnectionResolver = class {
|
|
253
257
|
constructor(options) {
|
|
254
258
|
__privateAdd(this, _logger, new Logger(ConnectionResolver.name));
|
|
255
259
|
__privateAdd(this, _client);
|
|
256
260
|
__privateAdd(this, _options);
|
|
257
261
|
__publicField(this, "mode");
|
|
262
|
+
__privateAdd(this, _pendingReady);
|
|
258
263
|
__privateSet(this, _options, options);
|
|
259
264
|
}
|
|
260
265
|
async init() {
|
|
@@ -310,6 +315,21 @@ var ConnectionResolver = class {
|
|
|
310
315
|
isOwned() {
|
|
311
316
|
return this.mode === "mode-b-owned";
|
|
312
317
|
}
|
|
318
|
+
awaitReady() {
|
|
319
|
+
const client = this.getClient();
|
|
320
|
+
if (!WAITING_STATUSES.has(client.status)) return Promise.resolve();
|
|
321
|
+
__privateGet(this, _pendingReady) ?? __privateSet(this, _pendingReady, new Promise((resolve) => {
|
|
322
|
+
const release = () => {
|
|
323
|
+
client.off("ready", release);
|
|
324
|
+
client.off("end", release);
|
|
325
|
+
__privateSet(this, _pendingReady, void 0);
|
|
326
|
+
resolve();
|
|
327
|
+
};
|
|
328
|
+
client.on("ready", release);
|
|
329
|
+
client.on("end", release);
|
|
330
|
+
}));
|
|
331
|
+
return __privateGet(this, _pendingReady);
|
|
332
|
+
}
|
|
313
333
|
async teardown() {
|
|
314
334
|
if (this.isOwned() && __privateGet(this, _client)) {
|
|
315
335
|
const client = __privateGet(this, _client);
|
|
@@ -330,7 +350,7 @@ var ConnectionResolver = class {
|
|
|
330
350
|
}
|
|
331
351
|
if (client.options.maxRetriesPerRequest === null) {
|
|
332
352
|
__privateGet(this, _logger).warn(
|
|
333
|
-
"The shared client has maxRetriesPerRequest: null, so a
|
|
353
|
+
"The shared client has maxRetriesPerRequest: null, so a command issued while Redis is unreachable stays queued in the client for as long as the outage lasts. QueueService calls are bounded by enqueueTimeoutMs and the caller is released, but the command is not cancelled. Give the client a finite limit to fail fast."
|
|
334
354
|
);
|
|
335
355
|
}
|
|
336
356
|
const probe = duplicateConnection(client);
|
|
@@ -380,10 +400,17 @@ var ConnectionResolver = class {
|
|
|
380
400
|
_logger = new WeakMap();
|
|
381
401
|
_client = new WeakMap();
|
|
382
402
|
_options = new WeakMap();
|
|
403
|
+
_pendingReady = new WeakMap();
|
|
383
404
|
ConnectionResolver = __decorateClass([
|
|
384
405
|
Injectable(),
|
|
385
406
|
__decorateParam(0, Inject(BYMAX_QUEUE_OPTIONS))
|
|
386
407
|
], ConnectionResolver);
|
|
408
|
+
var WAITING_STATUSES = /* @__PURE__ */ new Set([
|
|
409
|
+
"connecting",
|
|
410
|
+
"connect",
|
|
411
|
+
"reconnecting",
|
|
412
|
+
"close"
|
|
413
|
+
]);
|
|
387
414
|
function isReplyError(err) {
|
|
388
415
|
return typeof err === "object" && err !== null && err.name === "ReplyError";
|
|
389
416
|
}
|
|
@@ -432,6 +459,13 @@ function attachDefaultErrorListener(emitter, logger, kind, queueName) {
|
|
|
432
459
|
logger.error(`${kind} for queue "${queueName}" emitted an error: ${describeError(err)}`);
|
|
433
460
|
});
|
|
434
461
|
}
|
|
462
|
+
function assertPageWindow(operation, start, end) {
|
|
463
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end - start >= MAX_JOBS_PAGE_SIZE) {
|
|
464
|
+
throw new QueueException(QUEUE_ERROR_CODES.INVALID_OPTIONS, HttpStatus.BAD_REQUEST, {
|
|
465
|
+
reason: `${operation} needs integers with 0 <= start <= end and at most ${String(MAX_JOBS_PAGE_SIZE)} jobs per call`
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
435
469
|
|
|
436
470
|
// src/server/utils/with-deadline.ts
|
|
437
471
|
function withDeadline(work, timeoutMs, onTimeout) {
|
|
@@ -454,6 +488,18 @@ function timeoutException(operation, timeoutMs, cause) {
|
|
|
454
488
|
if (cause !== void 0) exception.cause = cause;
|
|
455
489
|
return exception;
|
|
456
490
|
}
|
|
491
|
+
function unavailableException(operation, cause) {
|
|
492
|
+
const exception = new QueueException(
|
|
493
|
+
QUEUE_ERROR_CODES.CONNECTION_TIMEOUT,
|
|
494
|
+
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
495
|
+
{ operation, reason: "offline" }
|
|
496
|
+
);
|
|
497
|
+
exception.cause = cause;
|
|
498
|
+
return exception;
|
|
499
|
+
}
|
|
500
|
+
function isOfflineRejection(err) {
|
|
501
|
+
return err instanceof Error && err.message === "Stream isn't writeable and enableOfflineQueue options is false";
|
|
502
|
+
}
|
|
457
503
|
function isCommandTimeout(err) {
|
|
458
504
|
return err instanceof Error && err.message === "Command timed out";
|
|
459
505
|
}
|
|
@@ -461,12 +507,12 @@ async function withCommandTimeoutCode(operation, commandTimeoutMs, work) {
|
|
|
461
507
|
try {
|
|
462
508
|
return await work;
|
|
463
509
|
} catch (err) {
|
|
464
|
-
|
|
510
|
+
if (isCommandTimeout(err)) throw timeoutException(operation, commandTimeoutMs(), err);
|
|
511
|
+
throw isOfflineRejection(err) ? unavailableException(operation, err) : err;
|
|
465
512
|
}
|
|
466
513
|
}
|
|
467
514
|
|
|
468
515
|
// src/server/services/queue.service.ts
|
|
469
|
-
var MAX_BULK_SIZE = 1e3;
|
|
470
516
|
function assertValidCron(repeat, queueOptions) {
|
|
471
517
|
if (queueOptions.settings?.repeatStrategy !== void 0) return;
|
|
472
518
|
try {
|
|
@@ -477,13 +523,14 @@ function assertValidCron(repeat, queueOptions) {
|
|
|
477
523
|
});
|
|
478
524
|
}
|
|
479
525
|
}
|
|
480
|
-
var _queues;
|
|
526
|
+
var _queues, _warm;
|
|
481
527
|
var QueueService = class {
|
|
482
528
|
constructor(connection, options) {
|
|
483
529
|
this.connection = connection;
|
|
484
530
|
this.options = options;
|
|
485
531
|
__publicField(this, "logger", new Logger(QueueService.name));
|
|
486
532
|
__privateAdd(this, _queues, /* @__PURE__ */ new Map());
|
|
533
|
+
__privateAdd(this, _warm, /* @__PURE__ */ new Set());
|
|
487
534
|
}
|
|
488
535
|
connection;
|
|
489
536
|
options;
|
|
@@ -502,27 +549,38 @@ var QueueService = class {
|
|
|
502
549
|
__privateGet(this, _queues).set(queueName, queue);
|
|
503
550
|
return queue;
|
|
504
551
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
552
|
+
call(operation, queueName, run) {
|
|
553
|
+
let expired = false;
|
|
554
|
+
const guarded = withDeadline(
|
|
555
|
+
this.afterReady(queueName).then(() => {
|
|
556
|
+
if (expired) throw new Error("the call had already timed out");
|
|
557
|
+
return run();
|
|
558
|
+
}),
|
|
559
|
+
this.options.enqueueTimeoutMs,
|
|
560
|
+
() => {
|
|
561
|
+
expired = true;
|
|
562
|
+
return timeoutException(operation, this.options.enqueueTimeoutMs);
|
|
563
|
+
}
|
|
564
|
+
);
|
|
509
565
|
return withCommandTimeoutCode(
|
|
510
566
|
operation,
|
|
511
567
|
() => this.connection.getClient().options.commandTimeout ?? this.options.commandTimeoutMs,
|
|
512
|
-
|
|
568
|
+
guarded
|
|
513
569
|
);
|
|
514
570
|
}
|
|
571
|
+
async afterReady(queueName) {
|
|
572
|
+
if (__privateGet(this, _warm).has(queueName)) return;
|
|
573
|
+
await this.connection.awaitReady();
|
|
574
|
+
__privateGet(this, _warm).add(queueName);
|
|
575
|
+
}
|
|
515
576
|
queueFor(queueName) {
|
|
516
577
|
return this.getOrCreateQueue(queueName);
|
|
517
578
|
}
|
|
518
579
|
async enqueue(queueName, jobName, data, options) {
|
|
519
580
|
const job = await this.call(
|
|
520
581
|
"enqueue",
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
this.options.enqueueTimeoutMs,
|
|
524
|
-
() => this.enqueueTimeout("enqueue")
|
|
525
|
-
)
|
|
582
|
+
queueName,
|
|
583
|
+
() => this.queueFor(queueName).add(jobName, data, options)
|
|
526
584
|
);
|
|
527
585
|
return job;
|
|
528
586
|
}
|
|
@@ -541,16 +599,13 @@ var QueueService = class {
|
|
|
541
599
|
try {
|
|
542
600
|
const created = await this.call(
|
|
543
601
|
"enqueueBulk",
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
),
|
|
552
|
-
this.options.enqueueTimeoutMs,
|
|
553
|
-
() => this.enqueueTimeout("enqueueBulk")
|
|
602
|
+
queueName,
|
|
603
|
+
() => this.queueFor(queueName).addBulk(
|
|
604
|
+
jobs.map((job) => ({
|
|
605
|
+
name: job.name,
|
|
606
|
+
data: job.data,
|
|
607
|
+
...job.options === void 0 ? {} : { opts: job.options }
|
|
608
|
+
}))
|
|
554
609
|
)
|
|
555
610
|
);
|
|
556
611
|
return created;
|
|
@@ -568,22 +623,23 @@ var QueueService = class {
|
|
|
568
623
|
}
|
|
569
624
|
}
|
|
570
625
|
async getJob(queueName, jobId) {
|
|
571
|
-
const job = await this.call("getJob", this.queueFor(queueName).getJob(jobId));
|
|
626
|
+
const job = await this.call("getJob", queueName, () => this.queueFor(queueName).getJob(jobId));
|
|
572
627
|
return job ?? null;
|
|
573
628
|
}
|
|
574
629
|
async getJobs(queueName, status, start = 0, end = 50) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
630
|
+
assertPageWindow("getJobs", start, end);
|
|
631
|
+
const jobs = await this.call(
|
|
632
|
+
"getJobs",
|
|
633
|
+
queueName,
|
|
634
|
+
() => this.queueFor(queueName).getJobs([status], start, end)
|
|
635
|
+
);
|
|
581
636
|
return jobs;
|
|
582
637
|
}
|
|
583
638
|
async getMetrics(queueName) {
|
|
584
639
|
const counts = await this.call(
|
|
585
640
|
"getMetrics",
|
|
586
|
-
|
|
641
|
+
queueName,
|
|
642
|
+
() => this.queueFor(queueName).getJobCounts("waiting", "active", "completed", "failed", "delayed")
|
|
587
643
|
);
|
|
588
644
|
return {
|
|
589
645
|
queue: queueName,
|
|
@@ -602,24 +658,38 @@ var QueueService = class {
|
|
|
602
658
|
if ("pattern" in repeat) assertValidCron(repeat, this.options.queueOptions);
|
|
603
659
|
const job = await this.call(
|
|
604
660
|
"upsertJobScheduler",
|
|
605
|
-
|
|
661
|
+
queueName,
|
|
662
|
+
() => queue.upsertJobScheduler(schedulerId, repeat, jobTemplate)
|
|
606
663
|
);
|
|
607
664
|
return job;
|
|
608
665
|
}
|
|
609
666
|
async removeJobScheduler(queueName, schedulerId) {
|
|
610
|
-
return this.call(
|
|
667
|
+
return this.call(
|
|
668
|
+
"removeJobScheduler",
|
|
669
|
+
queueName,
|
|
670
|
+
() => this.queueFor(queueName).removeJobScheduler(schedulerId)
|
|
671
|
+
);
|
|
611
672
|
}
|
|
612
673
|
async getJobSchedulers(queueName, start = 0, end = 50, asc = true) {
|
|
613
|
-
|
|
674
|
+
assertPageWindow("getJobSchedulers", start, end);
|
|
675
|
+
return this.call(
|
|
676
|
+
"getJobSchedulers",
|
|
677
|
+
queueName,
|
|
678
|
+
() => this.queueFor(queueName).getJobSchedulers(start, end, asc)
|
|
679
|
+
);
|
|
614
680
|
}
|
|
615
681
|
async pauseQueue(queueName) {
|
|
616
|
-
await this.call("pauseQueue", this.queueFor(queueName).pause());
|
|
682
|
+
await this.call("pauseQueue", queueName, () => this.queueFor(queueName).pause());
|
|
617
683
|
}
|
|
618
684
|
async resumeQueue(queueName) {
|
|
619
|
-
await this.call("resumeQueue", this.queueFor(queueName).resume());
|
|
685
|
+
await this.call("resumeQueue", queueName, () => this.queueFor(queueName).resume());
|
|
620
686
|
}
|
|
621
687
|
async cleanQueue(queueName, gracePeriodMs, limit, status) {
|
|
622
|
-
return this.call(
|
|
688
|
+
return this.call(
|
|
689
|
+
"cleanQueue",
|
|
690
|
+
queueName,
|
|
691
|
+
() => this.queueFor(queueName).clean(gracePeriodMs, limit, status)
|
|
692
|
+
);
|
|
623
693
|
}
|
|
624
694
|
getCachedQueues() {
|
|
625
695
|
return __privateGet(this, _queues);
|
|
@@ -629,9 +699,11 @@ var QueueService = class {
|
|
|
629
699
|
await queue.close().catch(() => void 0);
|
|
630
700
|
}
|
|
631
701
|
__privateGet(this, _queues).clear();
|
|
702
|
+
__privateGet(this, _warm).clear();
|
|
632
703
|
}
|
|
633
704
|
};
|
|
634
705
|
_queues = new WeakMap();
|
|
706
|
+
_warm = new WeakMap();
|
|
635
707
|
QueueService = __decorateClass([
|
|
636
708
|
Injectable(),
|
|
637
709
|
__decorateParam(0, Inject(ConnectionResolver)),
|
|
@@ -640,13 +712,15 @@ QueueService = __decorateClass([
|
|
|
640
712
|
function errorCodeOf(err) {
|
|
641
713
|
return err.getResponse().error.code;
|
|
642
714
|
}
|
|
643
|
-
var _connection;
|
|
715
|
+
var _connection, _deadlineMs;
|
|
644
716
|
var FlowService = class {
|
|
645
|
-
constructor(connection, enabled, prefix, telemetry) {
|
|
717
|
+
constructor(connection, enabled, prefix, telemetry, enqueueTimeoutMs = DEFAULT_ENQUEUE_TIMEOUT_MS) {
|
|
646
718
|
__publicField(this, "logger", new Logger(FlowService.name));
|
|
647
719
|
__publicField(this, "producer");
|
|
648
720
|
__privateAdd(this, _connection);
|
|
721
|
+
__privateAdd(this, _deadlineMs);
|
|
649
722
|
__privateSet(this, _connection, connection);
|
|
723
|
+
__privateSet(this, _deadlineMs, enqueueTimeoutMs);
|
|
650
724
|
if (enabled) {
|
|
651
725
|
this.producer = new FlowProducer({
|
|
652
726
|
connection: connection.getClient(),
|
|
@@ -657,17 +731,24 @@ var FlowService = class {
|
|
|
657
731
|
}
|
|
658
732
|
}
|
|
659
733
|
async add(flow) {
|
|
660
|
-
return
|
|
661
|
-
"addFlow",
|
|
662
|
-
() => this.commandTimeout(),
|
|
663
|
-
this.requireProducer().add(flow)
|
|
664
|
-
);
|
|
734
|
+
return this.bounded("addFlow", this.requireProducer().add(flow));
|
|
665
735
|
}
|
|
666
736
|
async addBulk(flows) {
|
|
737
|
+
const producer = this.requireProducer();
|
|
738
|
+
if (flows.length > MAX_BULK_SIZE) {
|
|
739
|
+
throw new QueueException(
|
|
740
|
+
QUEUE_ERROR_CODES.BULK_ENQUEUE_FAILED,
|
|
741
|
+
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
742
|
+
{ reason: "batch size exceeds limit", limit: MAX_BULK_SIZE, received: flows.length }
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
return this.bounded("addFlowBulk", producer.addBulk([...flows]));
|
|
746
|
+
}
|
|
747
|
+
bounded(operation, work) {
|
|
667
748
|
return withCommandTimeoutCode(
|
|
668
|
-
|
|
749
|
+
operation,
|
|
669
750
|
() => this.commandTimeout(),
|
|
670
|
-
this
|
|
751
|
+
withDeadline(work, __privateGet(this, _deadlineMs), () => timeoutException(operation, __privateGet(this, _deadlineMs)))
|
|
671
752
|
);
|
|
672
753
|
}
|
|
673
754
|
commandTimeout() {
|
|
@@ -689,6 +770,7 @@ var FlowService = class {
|
|
|
689
770
|
}
|
|
690
771
|
};
|
|
691
772
|
_connection = new WeakMap();
|
|
773
|
+
_deadlineMs = new WeakMap();
|
|
692
774
|
FlowService = __decorateClass([
|
|
693
775
|
Injectable()
|
|
694
776
|
], FlowService);
|
|
@@ -734,6 +816,41 @@ var MetricsService = class {
|
|
|
734
816
|
MetricsService = __decorateClass([
|
|
735
817
|
Injectable()
|
|
736
818
|
], MetricsService);
|
|
819
|
+
|
|
820
|
+
// src/server/utils/worker-option-problem.ts
|
|
821
|
+
var WORKER_TIMER_MIN_MS = 1e3;
|
|
822
|
+
function countProblem(name, value, min, max) {
|
|
823
|
+
if (!Number.isInteger(value)) return { reason: `${name} must be a whole number`, actual: value };
|
|
824
|
+
if (value < min) return { reason: `${name} must be >= ${String(min)}`, actual: value };
|
|
825
|
+
if (value > max) return { reason: `${name} must be <= ${String(max)}`, actual: value };
|
|
826
|
+
return void 0;
|
|
827
|
+
}
|
|
828
|
+
function timerProblem(name, value) {
|
|
829
|
+
if (value === void 0) return void 0;
|
|
830
|
+
if (Number.isInteger(value) && value >= WORKER_TIMER_MIN_MS && value <= MAX_TIMER_DELAY_MS) {
|
|
831
|
+
return void 0;
|
|
832
|
+
}
|
|
833
|
+
return {
|
|
834
|
+
reason: `${name} must be a whole number from ${String(WORKER_TIMER_MIN_MS)} to ${String(MAX_TIMER_DELAY_MS)}`,
|
|
835
|
+
actual: value
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
function workerOptionProblem(opts) {
|
|
839
|
+
if (opts === void 0) return void 0;
|
|
840
|
+
const { concurrency, limiter, lockDuration, stalledInterval } = opts;
|
|
841
|
+
return (concurrency === void 0 ? void 0 : countProblem("concurrency", concurrency, 1, MAX_WORKER_CONCURRENCY)) ?? (limiter === void 0 ? void 0 : countProblem("limiter.max", limiter.max, 1, Number.POSITIVE_INFINITY) ?? countProblem("limiter.duration", limiter.duration, 1, Number.POSITIVE_INFINITY)) ?? timerProblem("lockDuration", lockDuration) ?? timerProblem("stalledInterval", stalledInterval);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// src/server/utils/abandoned-workers.ts
|
|
845
|
+
var abandoned = /* @__PURE__ */ new WeakSet();
|
|
846
|
+
function abandonWorker(worker) {
|
|
847
|
+
abandoned.add(worker);
|
|
848
|
+
}
|
|
849
|
+
function isAbandoned(worker) {
|
|
850
|
+
return abandoned.has(worker);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/server/services/worker-registry.service.ts
|
|
737
854
|
var _entries;
|
|
738
855
|
var WorkerRegistry = class {
|
|
739
856
|
constructor(connection, options) {
|
|
@@ -754,7 +871,7 @@ var WorkerRegistry = class {
|
|
|
754
871
|
try {
|
|
755
872
|
worker = new Worker(
|
|
756
873
|
queueName,
|
|
757
|
-
toProcessor(handler, options?.jobTimeoutMs),
|
|
874
|
+
toProcessor(handler, options?.jobTimeoutMs, () => isAbandoned(worker)),
|
|
758
875
|
bullOpts
|
|
759
876
|
);
|
|
760
877
|
} catch (cause) {
|
|
@@ -847,51 +964,15 @@ var WorkerRegistry = class {
|
|
|
847
964
|
}
|
|
848
965
|
}
|
|
849
966
|
validateOptions(opts) {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
actual: opts.concurrency
|
|
858
|
-
}
|
|
859
|
-
);
|
|
860
|
-
}
|
|
861
|
-
if (opts.concurrency > MAX_WORKER_CONCURRENCY) {
|
|
862
|
-
throw new QueueException(
|
|
863
|
-
QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
|
|
864
|
-
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
865
|
-
{
|
|
866
|
-
reason: `concurrency must be <= ${MAX_WORKER_CONCURRENCY.toString()}`,
|
|
867
|
-
actual: opts.concurrency
|
|
868
|
-
}
|
|
869
|
-
);
|
|
870
|
-
}
|
|
967
|
+
const problem = workerOptionProblem(opts);
|
|
968
|
+
if (problem !== void 0) {
|
|
969
|
+
throw new QueueException(
|
|
970
|
+
QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
|
|
971
|
+
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
972
|
+
problem
|
|
973
|
+
);
|
|
871
974
|
}
|
|
872
975
|
this.validateJobTimeout(opts);
|
|
873
|
-
if (opts?.limiter !== void 0) {
|
|
874
|
-
if (opts.limiter.max < 1) {
|
|
875
|
-
throw new QueueException(
|
|
876
|
-
QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
|
|
877
|
-
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
878
|
-
{
|
|
879
|
-
reason: "limiter.max must be >= 1",
|
|
880
|
-
actual: opts.limiter.max
|
|
881
|
-
}
|
|
882
|
-
);
|
|
883
|
-
}
|
|
884
|
-
if (opts.limiter.duration < 1) {
|
|
885
|
-
throw new QueueException(
|
|
886
|
-
QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
|
|
887
|
-
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
888
|
-
{
|
|
889
|
-
reason: "limiter.duration must be >= 1",
|
|
890
|
-
actual: opts.limiter.duration
|
|
891
|
-
}
|
|
892
|
-
);
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
976
|
}
|
|
896
977
|
validateJobTimeout(opts) {
|
|
897
978
|
if (opts?.jobTimeoutMs !== void 0 && !isTimerDelayMs(opts.jobTimeoutMs)) {
|
|
@@ -968,7 +1049,7 @@ var JobTimeoutError = class extends Error {
|
|
|
968
1049
|
this.name = "JobTimeoutError";
|
|
969
1050
|
}
|
|
970
1051
|
};
|
|
971
|
-
function
|
|
1052
|
+
function adaptHandler(handler, timeoutMs) {
|
|
972
1053
|
if (timeoutMs === void 0) return (job, _token, signal) => handler(job, signal);
|
|
973
1054
|
return (job, _token, signal) => {
|
|
974
1055
|
const own = new AbortController();
|
|
@@ -990,6 +1071,16 @@ function toProcessor(handler, timeoutMs) {
|
|
|
990
1071
|
});
|
|
991
1072
|
};
|
|
992
1073
|
}
|
|
1074
|
+
function toProcessor(handler, timeoutMs, abandoned2) {
|
|
1075
|
+
const run = adaptHandler(handler, timeoutMs);
|
|
1076
|
+
return (job, token, signal) => run(job, token, signal).then(
|
|
1077
|
+
(value) => abandoned2() ? new Promise(() => void 0) : value,
|
|
1078
|
+
(error) => {
|
|
1079
|
+
if (abandoned2()) return new Promise(() => void 0);
|
|
1080
|
+
throw error;
|
|
1081
|
+
}
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
993
1084
|
var _events, _connections;
|
|
994
1085
|
var QueueEventsRegistry = class {
|
|
995
1086
|
constructor(connection, options) {
|
|
@@ -1168,6 +1259,7 @@ ProcessorDiscoveryService = __decorateClass([
|
|
|
1168
1259
|
__decorateParam(1, Inject(WorkerRegistry)),
|
|
1169
1260
|
__decorateParam(2, Inject(QueueEventsRegistry))
|
|
1170
1261
|
], ProcessorDiscoveryService);
|
|
1262
|
+
var FORCE_CLOSE_GRACE_MS = 1e3;
|
|
1171
1263
|
var DrainTimeoutError = class extends Error {
|
|
1172
1264
|
};
|
|
1173
1265
|
var QueueLifecycle = class {
|
|
@@ -1201,25 +1293,33 @@ var QueueLifecycle = class {
|
|
|
1201
1293
|
await this.closeQueues();
|
|
1202
1294
|
await this.teardownConnection();
|
|
1203
1295
|
this.logger.log(
|
|
1204
|
-
`Queue shutdown complete in ${String(Date.now() - (this.startedAt ?? Date.now()))}ms (
|
|
1296
|
+
`Queue shutdown complete in ${String(Date.now() - (this.startedAt ?? Date.now()))}ms (${String(this.forcedWorkers)} worker(s) stopped without draining)`
|
|
1205
1297
|
);
|
|
1206
1298
|
}
|
|
1207
1299
|
async closeWorkers(deadline) {
|
|
1208
|
-
let
|
|
1300
|
+
let undrained = 0;
|
|
1209
1301
|
await Promise.all(
|
|
1210
1302
|
Array.from(this.workers.getAll(), async ([name, worker]) => {
|
|
1211
1303
|
try {
|
|
1212
|
-
await bounded(
|
|
1304
|
+
await bounded(
|
|
1305
|
+
worker.pause().then(() => worker.close()),
|
|
1306
|
+
deadline
|
|
1307
|
+
);
|
|
1213
1308
|
} catch (cause) {
|
|
1214
|
-
|
|
1309
|
+
undrained++;
|
|
1310
|
+
abandonWorker(worker);
|
|
1311
|
+
const stopped = "it is stopped without waiting for its in-flight job, which becomes stalled and is retried";
|
|
1215
1312
|
this.logger.warn(
|
|
1216
|
-
cause instanceof DrainTimeoutError ? `Worker "${name}" exceeded the ${String(this.resolved.shutdown.drainTimeoutMs)}ms drain budget
|
|
1313
|
+
cause instanceof DrainTimeoutError ? `Worker "${name}" exceeded the ${String(this.resolved.shutdown.drainTimeoutMs)}ms drain budget (${QUEUE_ERROR_CODES.SHUTDOWN_TIMEOUT_EXCEEDED}); ${stopped}` : `Worker "${name}" failed to close gracefully (${describeError(cause)}); ${stopped}`
|
|
1314
|
+
);
|
|
1315
|
+
const deadlineError = () => new DrainTimeoutError();
|
|
1316
|
+
await withDeadline(worker.close(true), FORCE_CLOSE_GRACE_MS, deadlineError).catch(
|
|
1317
|
+
() => void 0
|
|
1217
1318
|
);
|
|
1218
|
-
void worker.close(true).catch(() => void 0);
|
|
1219
1319
|
}
|
|
1220
1320
|
})
|
|
1221
1321
|
);
|
|
1222
|
-
return
|
|
1322
|
+
return undrained;
|
|
1223
1323
|
}
|
|
1224
1324
|
async closeQueueEvents(deadline) {
|
|
1225
1325
|
await Promise.all(
|
|
@@ -1294,7 +1394,13 @@ var BymaxQueueModule = class extends ConfigurableModuleClass {
|
|
|
1294
1394
|
{ provide: BYMAX_QUEUE_RESOLVED_OPTIONS, useValue: resolved },
|
|
1295
1395
|
{
|
|
1296
1396
|
provide: FlowService,
|
|
1297
|
-
useFactory: (conn) => new FlowService(
|
|
1397
|
+
useFactory: (conn) => new FlowService(
|
|
1398
|
+
conn,
|
|
1399
|
+
resolved.flows.enabled,
|
|
1400
|
+
resolved.prefix,
|
|
1401
|
+
resolved.telemetry,
|
|
1402
|
+
resolved.enqueueTimeoutMs
|
|
1403
|
+
),
|
|
1298
1404
|
inject: [ConnectionResolver]
|
|
1299
1405
|
},
|
|
1300
1406
|
{
|
|
@@ -1324,7 +1430,13 @@ var BymaxQueueModule = class extends ConfigurableModuleClass {
|
|
|
1324
1430
|
},
|
|
1325
1431
|
{
|
|
1326
1432
|
provide: FlowService,
|
|
1327
|
-
useFactory: (conn, resolved) => new FlowService(
|
|
1433
|
+
useFactory: (conn, resolved) => new FlowService(
|
|
1434
|
+
conn,
|
|
1435
|
+
resolved.flows.enabled,
|
|
1436
|
+
resolved.prefix,
|
|
1437
|
+
resolved.telemetry,
|
|
1438
|
+
resolved.enqueueTimeoutMs
|
|
1439
|
+
),
|
|
1328
1440
|
inject: [ConnectionResolver, BYMAX_QUEUE_RESOLVED_OPTIONS]
|
|
1329
1441
|
},
|
|
1330
1442
|
{
|
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.16",
|
|
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",
|