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