@bymax-one/nest-queue 1.2.13 → 1.2.15
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 +117 -1
- package/README.md +106 -53
- package/dist/server/index.cjs +369 -100
- package/dist/server/index.d.cts +117 -32
- package/dist/server/index.d.ts +117 -32
- package/dist/server/index.mjs +369 -100
- 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,17 +241,23 @@ 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
|
|
231
|
-
var _logger, _client, _options;
|
|
253
|
+
var _logger, _client, _options, _pendingReady;
|
|
232
254
|
exports.ConnectionResolver = class ConnectionResolver {
|
|
233
255
|
constructor(options) {
|
|
234
256
|
__privateAdd(this, _logger, new common.Logger(exports.ConnectionResolver.name));
|
|
235
257
|
__privateAdd(this, _client);
|
|
236
258
|
__privateAdd(this, _options);
|
|
237
259
|
__publicField(this, "mode");
|
|
260
|
+
__privateAdd(this, _pendingReady);
|
|
238
261
|
__privateSet(this, _options, options);
|
|
239
262
|
}
|
|
240
263
|
async init() {
|
|
@@ -246,12 +269,22 @@ exports.ConnectionResolver = class ConnectionResolver {
|
|
|
246
269
|
this.mode = "mode-b-owned";
|
|
247
270
|
__privateSet(this, _client, "url" in cfg ? new ioredis.Redis(cfg.url, this.ownedOptions(cfg.options)) : new ioredis.Redis(this.ownedOptions(cfg.options)));
|
|
248
271
|
attachClientErrorListener(__privateGet(this, _client), __privateGet(this, _logger), "owned");
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
272
|
+
try {
|
|
273
|
+
await this.waitReady(
|
|
274
|
+
__privateGet(this, _options).connectionReadyTimeoutMs ?? DEFAULT_CONNECTION_READY_TIMEOUT_MS
|
|
275
|
+
);
|
|
276
|
+
} catch (cause) {
|
|
277
|
+
__privateGet(this, _client).disconnect();
|
|
278
|
+
throw cause;
|
|
279
|
+
}
|
|
252
280
|
}
|
|
253
281
|
ownedOptions(options) {
|
|
254
|
-
return {
|
|
282
|
+
return {
|
|
283
|
+
commandTimeout: __privateGet(this, _options).commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS,
|
|
284
|
+
enableOfflineQueue: false,
|
|
285
|
+
...options ?? {},
|
|
286
|
+
lazyConnect: false
|
|
287
|
+
};
|
|
255
288
|
}
|
|
256
289
|
getClient() {
|
|
257
290
|
if (!__privateGet(this, _client)) {
|
|
@@ -280,6 +313,17 @@ exports.ConnectionResolver = class ConnectionResolver {
|
|
|
280
313
|
isOwned() {
|
|
281
314
|
return this.mode === "mode-b-owned";
|
|
282
315
|
}
|
|
316
|
+
awaitReady() {
|
|
317
|
+
const client = this.getClient();
|
|
318
|
+
if (!WAITING_STATUSES.has(client.status)) return Promise.resolve();
|
|
319
|
+
__privateGet(this, _pendingReady) ?? __privateSet(this, _pendingReady, new Promise((resolve) => {
|
|
320
|
+
client.once("ready", () => {
|
|
321
|
+
__privateSet(this, _pendingReady, void 0);
|
|
322
|
+
resolve();
|
|
323
|
+
});
|
|
324
|
+
}));
|
|
325
|
+
return __privateGet(this, _pendingReady);
|
|
326
|
+
}
|
|
283
327
|
async teardown() {
|
|
284
328
|
if (this.isOwned() && __privateGet(this, _client)) {
|
|
285
329
|
const client = __privateGet(this, _client);
|
|
@@ -298,6 +342,11 @@ exports.ConnectionResolver = class ConnectionResolver {
|
|
|
298
342
|
{ status: client.status }
|
|
299
343
|
);
|
|
300
344
|
}
|
|
345
|
+
if (client.options.maxRetriesPerRequest === null) {
|
|
346
|
+
__privateGet(this, _logger).warn(
|
|
347
|
+
"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."
|
|
348
|
+
);
|
|
349
|
+
}
|
|
301
350
|
const probe = duplicateConnection(client);
|
|
302
351
|
attachClientErrorListener(probe, __privateGet(this, _logger), "probe");
|
|
303
352
|
try {
|
|
@@ -310,41 +359,62 @@ exports.ConnectionResolver = class ConnectionResolver {
|
|
|
310
359
|
const client = this.getClient();
|
|
311
360
|
if (client.status === "ready") return;
|
|
312
361
|
await new Promise((resolve, reject) => {
|
|
362
|
+
let lastErrorCode;
|
|
363
|
+
const details = (extra) => lastErrorCode === void 0 ? extra : { ...extra, lastErrorCode };
|
|
313
364
|
const cleanup = () => {
|
|
314
365
|
clearTimeout(timer);
|
|
315
366
|
client.off("ready", onReady);
|
|
316
367
|
client.off("error", onError);
|
|
368
|
+
client.off("end", onEnd);
|
|
369
|
+
};
|
|
370
|
+
const fail = (code, extra) => {
|
|
371
|
+
cleanup();
|
|
372
|
+
reject(new QueueException(code, common.HttpStatus.INTERNAL_SERVER_ERROR, details(extra)));
|
|
317
373
|
};
|
|
318
374
|
const onReady = () => {
|
|
319
375
|
cleanup();
|
|
320
376
|
resolve();
|
|
321
377
|
};
|
|
322
378
|
const onError = (err) => {
|
|
323
|
-
|
|
324
|
-
|
|
379
|
+
lastErrorCode = startupErrorCode(err);
|
|
380
|
+
if (isReplyError(err)) fail(QUEUE_ERROR_CODES.CONNECTION_INVALID, {});
|
|
381
|
+
};
|
|
382
|
+
const onEnd = () => {
|
|
383
|
+
fail(QUEUE_ERROR_CODES.CONNECTION_INVALID, {});
|
|
325
384
|
};
|
|
326
385
|
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
|
-
);
|
|
386
|
+
fail(QUEUE_ERROR_CODES.CONNECTION_TIMEOUT, { timeoutMs });
|
|
335
387
|
}, timeoutMs);
|
|
336
388
|
client.once("ready", onReady);
|
|
337
|
-
client.
|
|
389
|
+
client.on("error", onError);
|
|
390
|
+
client.once("end", onEnd);
|
|
338
391
|
});
|
|
339
392
|
}
|
|
340
393
|
};
|
|
341
394
|
_logger = new WeakMap();
|
|
342
395
|
_client = new WeakMap();
|
|
343
396
|
_options = new WeakMap();
|
|
397
|
+
_pendingReady = new WeakMap();
|
|
344
398
|
exports.ConnectionResolver = __decorateClass([
|
|
345
399
|
common.Injectable(),
|
|
346
400
|
__decorateParam(0, common.Inject(BYMAX_QUEUE_OPTIONS))
|
|
347
401
|
], exports.ConnectionResolver);
|
|
402
|
+
var WAITING_STATUSES = /* @__PURE__ */ new Set([
|
|
403
|
+
"connecting",
|
|
404
|
+
"connect",
|
|
405
|
+
"reconnecting",
|
|
406
|
+
"close"
|
|
407
|
+
]);
|
|
408
|
+
function isReplyError(err) {
|
|
409
|
+
return typeof err === "object" && err !== null && err.name === "ReplyError";
|
|
410
|
+
}
|
|
411
|
+
function startupErrorCode(err) {
|
|
412
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
413
|
+
const { code, message } = err;
|
|
414
|
+
if (typeof code === "string") return code;
|
|
415
|
+
if (isReplyError(err) && typeof message === "string") return /^[A-Z][A-Z0-9_]*/.exec(message)?.[0];
|
|
416
|
+
return void 0;
|
|
417
|
+
}
|
|
348
418
|
function invalid2(reason) {
|
|
349
419
|
throw new QueueException(QUEUE_ERROR_CODES.INVALID_REPEAT_OPTIONS, common.HttpStatus.BAD_REQUEST, {
|
|
350
420
|
reason
|
|
@@ -384,6 +454,51 @@ function attachDefaultErrorListener(emitter, logger, kind, queueName) {
|
|
|
384
454
|
});
|
|
385
455
|
}
|
|
386
456
|
|
|
457
|
+
// src/server/utils/with-deadline.ts
|
|
458
|
+
function withDeadline(work, timeoutMs, onTimeout) {
|
|
459
|
+
let timer;
|
|
460
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
461
|
+
timer = setTimeout(() => {
|
|
462
|
+
reject(onTimeout());
|
|
463
|
+
}, timeoutMs);
|
|
464
|
+
});
|
|
465
|
+
return Promise.race([work, deadline]).finally(() => {
|
|
466
|
+
clearTimeout(timer);
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
function timeoutException(operation, timeoutMs, cause) {
|
|
470
|
+
const exception = new QueueException(
|
|
471
|
+
QUEUE_ERROR_CODES.CONNECTION_TIMEOUT,
|
|
472
|
+
common.HttpStatus.INTERNAL_SERVER_ERROR,
|
|
473
|
+
{ operation, timeoutMs }
|
|
474
|
+
);
|
|
475
|
+
if (cause !== void 0) exception.cause = cause;
|
|
476
|
+
return exception;
|
|
477
|
+
}
|
|
478
|
+
function unavailableException(operation, cause) {
|
|
479
|
+
const exception = new QueueException(
|
|
480
|
+
QUEUE_ERROR_CODES.CONNECTION_TIMEOUT,
|
|
481
|
+
common.HttpStatus.INTERNAL_SERVER_ERROR,
|
|
482
|
+
{ operation, reason: "offline" }
|
|
483
|
+
);
|
|
484
|
+
exception.cause = cause;
|
|
485
|
+
return exception;
|
|
486
|
+
}
|
|
487
|
+
function isOfflineRejection(err) {
|
|
488
|
+
return err instanceof Error && err.message === "Stream isn't writeable and enableOfflineQueue options is false";
|
|
489
|
+
}
|
|
490
|
+
function isCommandTimeout(err) {
|
|
491
|
+
return err instanceof Error && err.message === "Command timed out";
|
|
492
|
+
}
|
|
493
|
+
async function withCommandTimeoutCode(operation, commandTimeoutMs, work) {
|
|
494
|
+
try {
|
|
495
|
+
return await work;
|
|
496
|
+
} catch (err) {
|
|
497
|
+
if (isCommandTimeout(err)) throw timeoutException(operation, commandTimeoutMs(), err);
|
|
498
|
+
throw isOfflineRejection(err) ? unavailableException(operation, err) : err;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
387
502
|
// src/server/services/queue.service.ts
|
|
388
503
|
var MAX_BULK_SIZE = 1e3;
|
|
389
504
|
function assertValidCron(repeat, queueOptions) {
|
|
@@ -396,13 +511,14 @@ function assertValidCron(repeat, queueOptions) {
|
|
|
396
511
|
});
|
|
397
512
|
}
|
|
398
513
|
}
|
|
399
|
-
var _queues;
|
|
514
|
+
var _queues, _warm;
|
|
400
515
|
exports.QueueService = class QueueService {
|
|
401
516
|
constructor(connection, options) {
|
|
402
517
|
this.connection = connection;
|
|
403
518
|
this.options = options;
|
|
404
519
|
__publicField(this, "logger", new common.Logger(exports.QueueService.name));
|
|
405
520
|
__privateAdd(this, _queues, /* @__PURE__ */ new Map());
|
|
521
|
+
__privateAdd(this, _warm, /* @__PURE__ */ new Set());
|
|
406
522
|
}
|
|
407
523
|
connection;
|
|
408
524
|
options;
|
|
@@ -421,11 +537,39 @@ exports.QueueService = class QueueService {
|
|
|
421
537
|
__privateGet(this, _queues).set(queueName, queue);
|
|
422
538
|
return queue;
|
|
423
539
|
}
|
|
540
|
+
call(operation, queueName, run) {
|
|
541
|
+
let expired = false;
|
|
542
|
+
const guarded = withDeadline(
|
|
543
|
+
this.afterReady(queueName).then(() => {
|
|
544
|
+
if (expired) throw new Error("the call had already timed out");
|
|
545
|
+
return run();
|
|
546
|
+
}),
|
|
547
|
+
this.options.enqueueTimeoutMs,
|
|
548
|
+
() => {
|
|
549
|
+
expired = true;
|
|
550
|
+
return timeoutException(operation, this.options.enqueueTimeoutMs);
|
|
551
|
+
}
|
|
552
|
+
);
|
|
553
|
+
return withCommandTimeoutCode(
|
|
554
|
+
operation,
|
|
555
|
+
() => this.connection.getClient().options.commandTimeout ?? this.options.commandTimeoutMs,
|
|
556
|
+
guarded
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
async afterReady(queueName) {
|
|
560
|
+
if (__privateGet(this, _warm).has(queueName)) return;
|
|
561
|
+
await this.connection.awaitReady();
|
|
562
|
+
__privateGet(this, _warm).add(queueName);
|
|
563
|
+
}
|
|
424
564
|
queueFor(queueName) {
|
|
425
565
|
return this.getOrCreateQueue(queueName);
|
|
426
566
|
}
|
|
427
567
|
async enqueue(queueName, jobName, data, options) {
|
|
428
|
-
const job = await this.
|
|
568
|
+
const job = await this.call(
|
|
569
|
+
"enqueue",
|
|
570
|
+
queueName,
|
|
571
|
+
() => this.queueFor(queueName).add(jobName, data, options)
|
|
572
|
+
);
|
|
429
573
|
return job;
|
|
430
574
|
}
|
|
431
575
|
async enqueueBulk(queueName, jobs) {
|
|
@@ -441,15 +585,22 @@ exports.QueueService = class QueueService {
|
|
|
441
585
|
);
|
|
442
586
|
}
|
|
443
587
|
try {
|
|
444
|
-
const created = await this.
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
588
|
+
const created = await this.call(
|
|
589
|
+
"enqueueBulk",
|
|
590
|
+
queueName,
|
|
591
|
+
() => this.queueFor(queueName).addBulk(
|
|
592
|
+
jobs.map((job) => ({
|
|
593
|
+
name: job.name,
|
|
594
|
+
data: job.data,
|
|
595
|
+
...job.options === void 0 ? {} : { opts: job.options }
|
|
596
|
+
}))
|
|
597
|
+
)
|
|
450
598
|
);
|
|
451
599
|
return created;
|
|
452
600
|
} catch (err) {
|
|
601
|
+
if (err instanceof QueueException && errorCodeOf(err) === QUEUE_ERROR_CODES.CONNECTION_TIMEOUT) {
|
|
602
|
+
throw err;
|
|
603
|
+
}
|
|
453
604
|
throw new QueueException(
|
|
454
605
|
QUEUE_ERROR_CODES.BULK_ENQUEUE_FAILED,
|
|
455
606
|
common.HttpStatus.INTERNAL_SERVER_ERROR,
|
|
@@ -460,20 +611,27 @@ exports.QueueService = class QueueService {
|
|
|
460
611
|
}
|
|
461
612
|
}
|
|
462
613
|
async getJob(queueName, jobId) {
|
|
463
|
-
const job = await this.queueFor(queueName).getJob(jobId);
|
|
614
|
+
const job = await this.call("getJob", queueName, () => this.queueFor(queueName).getJob(jobId));
|
|
464
615
|
return job ?? null;
|
|
465
616
|
}
|
|
466
617
|
async getJobs(queueName, status, start = 0, end = 50) {
|
|
467
|
-
|
|
618
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end - start >= MAX_JOBS_PAGE_SIZE) {
|
|
619
|
+
throw new QueueException(QUEUE_ERROR_CODES.INVALID_OPTIONS, common.HttpStatus.BAD_REQUEST, {
|
|
620
|
+
reason: `getJobs needs integers with 0 <= start <= end and at most ${String(MAX_JOBS_PAGE_SIZE)} jobs per call`
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
const jobs = await this.call(
|
|
624
|
+
"getJobs",
|
|
625
|
+
queueName,
|
|
626
|
+
() => this.queueFor(queueName).getJobs([status], start, end)
|
|
627
|
+
);
|
|
468
628
|
return jobs;
|
|
469
629
|
}
|
|
470
630
|
async getMetrics(queueName) {
|
|
471
|
-
const counts = await this.
|
|
472
|
-
"
|
|
473
|
-
|
|
474
|
-
"completed",
|
|
475
|
-
"failed",
|
|
476
|
-
"delayed"
|
|
631
|
+
const counts = await this.call(
|
|
632
|
+
"getMetrics",
|
|
633
|
+
queueName,
|
|
634
|
+
() => this.queueFor(queueName).getJobCounts("waiting", "active", "completed", "failed", "delayed")
|
|
477
635
|
);
|
|
478
636
|
return {
|
|
479
637
|
queue: queueName,
|
|
@@ -490,23 +648,39 @@ exports.QueueService = class QueueService {
|
|
|
490
648
|
...template?.opts === void 0 ? {} : { opts: template.opts }
|
|
491
649
|
};
|
|
492
650
|
if ("pattern" in repeat) assertValidCron(repeat, this.options.queueOptions);
|
|
493
|
-
const job = await
|
|
651
|
+
const job = await this.call(
|
|
652
|
+
"upsertJobScheduler",
|
|
653
|
+
queueName,
|
|
654
|
+
() => queue.upsertJobScheduler(schedulerId, repeat, jobTemplate)
|
|
655
|
+
);
|
|
494
656
|
return job;
|
|
495
657
|
}
|
|
496
658
|
async removeJobScheduler(queueName, schedulerId) {
|
|
497
|
-
return this.
|
|
659
|
+
return this.call(
|
|
660
|
+
"removeJobScheduler",
|
|
661
|
+
queueName,
|
|
662
|
+
() => this.queueFor(queueName).removeJobScheduler(schedulerId)
|
|
663
|
+
);
|
|
498
664
|
}
|
|
499
665
|
async getJobSchedulers(queueName, start = 0, end = 50, asc = true) {
|
|
500
|
-
return this.
|
|
666
|
+
return this.call(
|
|
667
|
+
"getJobSchedulers",
|
|
668
|
+
queueName,
|
|
669
|
+
() => this.queueFor(queueName).getJobSchedulers(start, end, asc)
|
|
670
|
+
);
|
|
501
671
|
}
|
|
502
672
|
async pauseQueue(queueName) {
|
|
503
|
-
await this.queueFor(queueName).pause();
|
|
673
|
+
await this.call("pauseQueue", queueName, () => this.queueFor(queueName).pause());
|
|
504
674
|
}
|
|
505
675
|
async resumeQueue(queueName) {
|
|
506
|
-
await this.queueFor(queueName).resume();
|
|
676
|
+
await this.call("resumeQueue", queueName, () => this.queueFor(queueName).resume());
|
|
507
677
|
}
|
|
508
678
|
async cleanQueue(queueName, gracePeriodMs, limit, status) {
|
|
509
|
-
return this.
|
|
679
|
+
return this.call(
|
|
680
|
+
"cleanQueue",
|
|
681
|
+
queueName,
|
|
682
|
+
() => this.queueFor(queueName).clean(gracePeriodMs, limit, status)
|
|
683
|
+
);
|
|
510
684
|
}
|
|
511
685
|
getCachedQueues() {
|
|
512
686
|
return __privateGet(this, _queues);
|
|
@@ -516,18 +690,26 @@ exports.QueueService = class QueueService {
|
|
|
516
690
|
await queue.close().catch(() => void 0);
|
|
517
691
|
}
|
|
518
692
|
__privateGet(this, _queues).clear();
|
|
693
|
+
__privateGet(this, _warm).clear();
|
|
519
694
|
}
|
|
520
695
|
};
|
|
521
696
|
_queues = new WeakMap();
|
|
697
|
+
_warm = new WeakMap();
|
|
522
698
|
exports.QueueService = __decorateClass([
|
|
523
699
|
common.Injectable(),
|
|
524
700
|
__decorateParam(0, common.Inject(exports.ConnectionResolver)),
|
|
525
701
|
__decorateParam(1, common.Inject(BYMAX_QUEUE_RESOLVED_OPTIONS))
|
|
526
702
|
], exports.QueueService);
|
|
703
|
+
function errorCodeOf(err) {
|
|
704
|
+
return err.getResponse().error.code;
|
|
705
|
+
}
|
|
706
|
+
var _connection;
|
|
527
707
|
exports.FlowService = class FlowService {
|
|
528
|
-
logger = new common.Logger(exports.FlowService.name);
|
|
529
|
-
producer;
|
|
530
708
|
constructor(connection, enabled, prefix, telemetry) {
|
|
709
|
+
__publicField(this, "logger", new common.Logger(exports.FlowService.name));
|
|
710
|
+
__publicField(this, "producer");
|
|
711
|
+
__privateAdd(this, _connection);
|
|
712
|
+
__privateSet(this, _connection, connection);
|
|
531
713
|
if (enabled) {
|
|
532
714
|
this.producer = new bullmq.FlowProducer({
|
|
533
715
|
connection: connection.getClient(),
|
|
@@ -538,10 +720,21 @@ exports.FlowService = class FlowService {
|
|
|
538
720
|
}
|
|
539
721
|
}
|
|
540
722
|
async add(flow) {
|
|
541
|
-
return
|
|
723
|
+
return withCommandTimeoutCode(
|
|
724
|
+
"addFlow",
|
|
725
|
+
() => this.commandTimeout(),
|
|
726
|
+
this.requireProducer().add(flow)
|
|
727
|
+
);
|
|
542
728
|
}
|
|
543
729
|
async addBulk(flows) {
|
|
544
|
-
return
|
|
730
|
+
return withCommandTimeoutCode(
|
|
731
|
+
"addFlowBulk",
|
|
732
|
+
() => this.commandTimeout(),
|
|
733
|
+
this.requireProducer().addBulk([...flows])
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
commandTimeout() {
|
|
737
|
+
return __privateGet(this, _connection).getClient().options.commandTimeout ?? 0;
|
|
545
738
|
}
|
|
546
739
|
getProducer() {
|
|
547
740
|
return this.requireProducer();
|
|
@@ -558,6 +751,7 @@ exports.FlowService = class FlowService {
|
|
|
558
751
|
return this.producer;
|
|
559
752
|
}
|
|
560
753
|
};
|
|
754
|
+
_connection = new WeakMap();
|
|
561
755
|
exports.FlowService = __decorateClass([
|
|
562
756
|
common.Injectable()
|
|
563
757
|
], exports.FlowService);
|
|
@@ -603,6 +797,17 @@ exports.MetricsService = class MetricsService {
|
|
|
603
797
|
exports.MetricsService = __decorateClass([
|
|
604
798
|
common.Injectable()
|
|
605
799
|
], exports.MetricsService);
|
|
800
|
+
|
|
801
|
+
// src/server/utils/abandoned-workers.ts
|
|
802
|
+
var abandoned = /* @__PURE__ */ new WeakSet();
|
|
803
|
+
function abandonWorker(worker) {
|
|
804
|
+
abandoned.add(worker);
|
|
805
|
+
}
|
|
806
|
+
function isAbandoned(worker) {
|
|
807
|
+
return abandoned.has(worker);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/server/services/worker-registry.service.ts
|
|
606
811
|
var _entries;
|
|
607
812
|
exports.WorkerRegistry = class WorkerRegistry {
|
|
608
813
|
constructor(connection, options) {
|
|
@@ -621,7 +826,11 @@ exports.WorkerRegistry = class WorkerRegistry {
|
|
|
621
826
|
const bullOpts = this.buildBullOptions(options, conn);
|
|
622
827
|
let worker;
|
|
623
828
|
try {
|
|
624
|
-
worker = new bullmq.Worker(
|
|
829
|
+
worker = new bullmq.Worker(
|
|
830
|
+
queueName,
|
|
831
|
+
toProcessor(handler, options?.jobTimeoutMs, () => isAbandoned(worker)),
|
|
832
|
+
bullOpts
|
|
833
|
+
);
|
|
625
834
|
} catch (cause) {
|
|
626
835
|
conn.disconnect();
|
|
627
836
|
throw new QueueException(
|
|
@@ -639,6 +848,13 @@ exports.WorkerRegistry = class WorkerRegistry {
|
|
|
639
848
|
}
|
|
640
849
|
registerSandboxed(config) {
|
|
641
850
|
const { queueName, processorFile, options } = config;
|
|
851
|
+
if (options?.jobTimeoutMs !== void 0) {
|
|
852
|
+
throw new QueueException(
|
|
853
|
+
QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
|
|
854
|
+
common.HttpStatus.INTERNAL_SERVER_ERROR,
|
|
855
|
+
{ reason: "jobTimeoutMs is not available for sandboxed workers" }
|
|
856
|
+
);
|
|
857
|
+
}
|
|
642
858
|
this.validateProcessorFile(processorFile);
|
|
643
859
|
this.guardDuplicate(queueName);
|
|
644
860
|
this.validateOptions(options);
|
|
@@ -668,9 +884,12 @@ exports.WorkerRegistry = class WorkerRegistry {
|
|
|
668
884
|
async unregister(queueName) {
|
|
669
885
|
const entry = __privateGet(this, _entries).get(queueName);
|
|
670
886
|
if (!entry) return;
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
887
|
+
try {
|
|
888
|
+
await entry.worker.close();
|
|
889
|
+
} finally {
|
|
890
|
+
await this.closeConnection(entry.connection);
|
|
891
|
+
__privateGet(this, _entries).delete(queueName);
|
|
892
|
+
}
|
|
674
893
|
}
|
|
675
894
|
list() {
|
|
676
895
|
return Array.from(__privateGet(this, _entries).keys());
|
|
@@ -724,6 +943,7 @@ exports.WorkerRegistry = class WorkerRegistry {
|
|
|
724
943
|
);
|
|
725
944
|
}
|
|
726
945
|
}
|
|
946
|
+
this.validateJobTimeout(opts);
|
|
727
947
|
if (opts?.limiter !== void 0) {
|
|
728
948
|
if (opts.limiter.max < 1) {
|
|
729
949
|
throw new QueueException(
|
|
@@ -747,6 +967,18 @@ exports.WorkerRegistry = class WorkerRegistry {
|
|
|
747
967
|
}
|
|
748
968
|
}
|
|
749
969
|
}
|
|
970
|
+
validateJobTimeout(opts) {
|
|
971
|
+
if (opts?.jobTimeoutMs !== void 0 && !isTimerDelayMs(opts.jobTimeoutMs)) {
|
|
972
|
+
throw new QueueException(
|
|
973
|
+
QUEUE_ERROR_CODES.WORKER_REGISTRATION_FAILED,
|
|
974
|
+
common.HttpStatus.INTERNAL_SERVER_ERROR,
|
|
975
|
+
{
|
|
976
|
+
reason: "jobTimeoutMs must be a whole number from 1 to 2147483647",
|
|
977
|
+
actual: opts.jobTimeoutMs
|
|
978
|
+
}
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
750
982
|
validateProcessorFile(processorFile) {
|
|
751
983
|
if (processorFile instanceof URL) {
|
|
752
984
|
if (processorFile.protocol !== "file:") {
|
|
@@ -804,6 +1036,44 @@ exports.WorkerRegistry = __decorateClass([
|
|
|
804
1036
|
__decorateParam(0, common.Inject(exports.ConnectionResolver)),
|
|
805
1037
|
__decorateParam(1, common.Inject(BYMAX_QUEUE_RESOLVED_OPTIONS))
|
|
806
1038
|
], exports.WorkerRegistry);
|
|
1039
|
+
var JobTimeoutError = class extends Error {
|
|
1040
|
+
constructor(timeoutMs) {
|
|
1041
|
+
super(`queue.job_timeout: the handler did not settle within ${String(timeoutMs)}ms`);
|
|
1042
|
+
this.name = "JobTimeoutError";
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
function adaptHandler(handler, timeoutMs) {
|
|
1046
|
+
if (timeoutMs === void 0) return (job, _token, signal) => handler(job, signal);
|
|
1047
|
+
return (job, _token, signal) => {
|
|
1048
|
+
const own = new AbortController();
|
|
1049
|
+
const relay = () => {
|
|
1050
|
+
own.abort(signal?.reason);
|
|
1051
|
+
};
|
|
1052
|
+
if (signal?.aborted === true) relay();
|
|
1053
|
+
signal?.addEventListener("abort", relay);
|
|
1054
|
+
return withDeadline(
|
|
1055
|
+
Promise.resolve().then(() => handler(job, own.signal)),
|
|
1056
|
+
timeoutMs,
|
|
1057
|
+
() => {
|
|
1058
|
+
const error = new JobTimeoutError(timeoutMs);
|
|
1059
|
+
own.abort(error);
|
|
1060
|
+
return error;
|
|
1061
|
+
}
|
|
1062
|
+
).finally(() => {
|
|
1063
|
+
signal?.removeEventListener("abort", relay);
|
|
1064
|
+
});
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
function toProcessor(handler, timeoutMs, abandoned2) {
|
|
1068
|
+
const run = adaptHandler(handler, timeoutMs);
|
|
1069
|
+
return (job, token, signal) => run(job, token, signal).then(
|
|
1070
|
+
(value) => abandoned2() ? new Promise(() => void 0) : value,
|
|
1071
|
+
(error) => {
|
|
1072
|
+
if (abandoned2()) return new Promise(() => void 0);
|
|
1073
|
+
throw error;
|
|
1074
|
+
}
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
807
1077
|
var _events, _connections;
|
|
808
1078
|
exports.QueueEventsRegistry = class QueueEventsRegistry {
|
|
809
1079
|
constructor(connection, options) {
|
|
@@ -893,10 +1163,10 @@ var ProcessorDiscoveryService = class {
|
|
|
893
1163
|
catchAll = fn;
|
|
894
1164
|
}
|
|
895
1165
|
}
|
|
896
|
-
return (job) => {
|
|
1166
|
+
return (job, signal) => {
|
|
897
1167
|
const specific = namedHandlers.get(job.name);
|
|
898
|
-
if (specific) return specific(job);
|
|
899
|
-
if (catchAll) return catchAll(job);
|
|
1168
|
+
if (specific) return specific(job, signal);
|
|
1169
|
+
if (catchAll) return catchAll(job, signal);
|
|
900
1170
|
return Promise.reject(
|
|
901
1171
|
new Error(
|
|
902
1172
|
`No handler registered for job name "${job.name}" on queue \u2014 add a matching @Process("name") or a catch-all @Process() method.`
|
|
@@ -982,6 +1252,7 @@ ProcessorDiscoveryService = __decorateClass([
|
|
|
982
1252
|
__decorateParam(1, common.Inject(exports.WorkerRegistry)),
|
|
983
1253
|
__decorateParam(2, common.Inject(exports.QueueEventsRegistry))
|
|
984
1254
|
], ProcessorDiscoveryService);
|
|
1255
|
+
var FORCE_CLOSE_GRACE_MS = 1e3;
|
|
985
1256
|
var DrainTimeoutError = class extends Error {
|
|
986
1257
|
};
|
|
987
1258
|
var QueueLifecycle = class {
|
|
@@ -1004,9 +1275,10 @@ var QueueLifecycle = class {
|
|
|
1004
1275
|
forcedWorkers = 0;
|
|
1005
1276
|
async onModuleDestroy() {
|
|
1006
1277
|
this.startedAt = Date.now();
|
|
1007
|
-
this.
|
|
1008
|
-
await this.
|
|
1009
|
-
await this.
|
|
1278
|
+
const deadline = this.startedAt + this.resolved.shutdown.drainTimeoutMs;
|
|
1279
|
+
this.forcedWorkers = await this.closeWorkers(deadline);
|
|
1280
|
+
await this.closeQueueEvents(deadline);
|
|
1281
|
+
await this.closeDuplicatedConnections(deadline);
|
|
1010
1282
|
}
|
|
1011
1283
|
async onApplicationShutdown() {
|
|
1012
1284
|
await this.drainQueuesIfRequested();
|
|
@@ -1014,59 +1286,53 @@ var QueueLifecycle = class {
|
|
|
1014
1286
|
await this.closeQueues();
|
|
1015
1287
|
await this.teardownConnection();
|
|
1016
1288
|
this.logger.log(
|
|
1017
|
-
`Queue shutdown complete in ${String(Date.now() - (this.startedAt ?? Date.now()))}ms (
|
|
1289
|
+
`Queue shutdown complete in ${String(Date.now() - (this.startedAt ?? Date.now()))}ms (${String(this.forcedWorkers)} worker(s) stopped without draining)`
|
|
1018
1290
|
);
|
|
1019
1291
|
}
|
|
1020
|
-
async closeWorkers() {
|
|
1021
|
-
let
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
timer = setTimeout(() => {
|
|
1045
|
-
reject(new DrainTimeoutError());
|
|
1046
|
-
}, timeoutMs);
|
|
1047
|
-
})
|
|
1048
|
-
]);
|
|
1049
|
-
} finally {
|
|
1050
|
-
clearTimeout(timer);
|
|
1051
|
-
}
|
|
1292
|
+
async closeWorkers(deadline) {
|
|
1293
|
+
let undrained = 0;
|
|
1294
|
+
await Promise.all(
|
|
1295
|
+
Array.from(this.workers.getAll(), async ([name, worker]) => {
|
|
1296
|
+
try {
|
|
1297
|
+
await bounded(
|
|
1298
|
+
worker.pause().then(() => worker.close()),
|
|
1299
|
+
deadline
|
|
1300
|
+
);
|
|
1301
|
+
} catch (cause) {
|
|
1302
|
+
undrained++;
|
|
1303
|
+
abandonWorker(worker);
|
|
1304
|
+
const stopped = "it is stopped without waiting for its in-flight job, which becomes stalled and is retried";
|
|
1305
|
+
this.logger.warn(
|
|
1306
|
+
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}`
|
|
1307
|
+
);
|
|
1308
|
+
const deadlineError = () => new DrainTimeoutError();
|
|
1309
|
+
await withDeadline(worker.close(true), FORCE_CLOSE_GRACE_MS, deadlineError).catch(
|
|
1310
|
+
() => void 0
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
})
|
|
1314
|
+
);
|
|
1315
|
+
return undrained;
|
|
1052
1316
|
}
|
|
1053
|
-
async closeQueueEvents() {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1317
|
+
async closeQueueEvents(deadline) {
|
|
1318
|
+
await Promise.all(
|
|
1319
|
+
Array.from(this.events.getAll().values(), async (queueEvents) => {
|
|
1320
|
+
await bounded(queueEvents.close(), deadline).catch(() => void 0);
|
|
1321
|
+
})
|
|
1322
|
+
);
|
|
1057
1323
|
}
|
|
1058
|
-
async closeDuplicatedConnections() {
|
|
1324
|
+
async closeDuplicatedConnections(deadline) {
|
|
1059
1325
|
const connections = [
|
|
1060
1326
|
...this.workers.getConnections().values(),
|
|
1061
1327
|
...this.events.getConnections().values()
|
|
1062
1328
|
];
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
await connection.quit()
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1329
|
+
await Promise.all(
|
|
1330
|
+
connections.map(async (connection) => {
|
|
1331
|
+
await bounded(connection.quit(), deadline).catch(() => {
|
|
1332
|
+
connection.disconnect();
|
|
1333
|
+
});
|
|
1334
|
+
})
|
|
1335
|
+
);
|
|
1070
1336
|
}
|
|
1071
1337
|
async drainQueuesIfRequested() {
|
|
1072
1338
|
if (!this.resolved.shutdown.drainOnShutdown) return;
|
|
@@ -1097,6 +1363,9 @@ QueueLifecycle = __decorateClass([
|
|
|
1097
1363
|
__decorateParam(4, common.Inject(exports.ConnectionResolver)),
|
|
1098
1364
|
__decorateParam(5, common.Inject(BYMAX_QUEUE_RESOLVED_OPTIONS))
|
|
1099
1365
|
], QueueLifecycle);
|
|
1366
|
+
function bounded(work, deadline) {
|
|
1367
|
+
return withDeadline(work, Math.max(0, deadline - Date.now()), () => new DrainTimeoutError());
|
|
1368
|
+
}
|
|
1100
1369
|
|
|
1101
1370
|
// src/server/bymax-queue.module.ts
|
|
1102
1371
|
var { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = (
|