@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.
@@ -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
- return client.duplicate({ maxRetriesPerRequest: null });
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
- await this.waitReady(
250
- __privateGet(this, _options).connectionReadyTimeoutMs ?? DEFAULT_CONNECTION_READY_TIMEOUT_MS
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 { ...options ?? {}, lazyConnect: false };
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
- cleanup();
324
- reject(err);
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
- cleanup();
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.once("error", onError);
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.queueFor(queueName).add(jobName, data, options);
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.queueFor(queueName).addBulk(
445
- jobs.map((job) => ({
446
- name: job.name,
447
- data: job.data,
448
- ...job.options === void 0 ? {} : { opts: job.options }
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
- const jobs = await this.queueFor(queueName).getJobs([status], start, end);
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.queueFor(queueName).getJobCounts(
472
- "waiting",
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 queue.upsertJobScheduler(schedulerId, repeat, jobTemplate);
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 this.requireProducer().add(flow);
662
+ return withCommandTimeoutCode(
663
+ "addFlow",
664
+ () => this.commandTimeout(),
665
+ this.requireProducer().add(flow)
666
+ );
542
667
  }
543
668
  async addBulk(flows) {
544
- return this.requireProducer().addBulk([...flows]);
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(queueName, handler, bullOpts);
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
- await entry.worker.close();
672
- await this.closeConnection(entry.connection);
673
- __privateGet(this, _entries).delete(queueName);
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.forcedWorkers = await this.closeWorkers();
1008
- await this.closeQueueEvents();
1009
- await this.closeDuplicatedConnections();
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
- const drainTimeoutMs = this.resolved.shutdown.drainTimeoutMs;
1023
- for (const [name, worker] of this.workers.getAll()) {
1024
- try {
1025
- await this.closeWorkerWithTimeout(worker, drainTimeoutMs);
1026
- } catch (cause) {
1027
- forced++;
1028
- this.logger.warn(
1029
- cause instanceof DrainTimeoutError ? `Worker "${name}" exceeded the ${String(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`
1030
- );
1031
- void worker.close(true).catch(() => void 0);
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 closeWorkerWithTimeout(worker, timeoutMs) {
1037
- let timer;
1038
- const closed = worker.close();
1039
- closed.catch(() => void 0);
1040
- try {
1041
- await Promise.race([
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
- for (const connection of connections) {
1064
- try {
1065
- await connection.quit();
1066
- } catch {
1067
- connection.disconnect();
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 } = (