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