bullmq 6.3.9 → 6.3.11

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.
@@ -46,7 +46,11 @@ class Worker extends queue_base_1.QueueBase {
46
46
  this.drained = false;
47
47
  this.limitUntil = 0;
48
48
  this.processorAcceptsSignal = false;
49
- this.stalledCheckerRunning = false;
49
+ // Bumped whenever the currently running stalled-checker loop should be
50
+ // abandoned (e.g. by `pause()`). The loop compares its own captured value
51
+ // against the live counter on every iteration and exits as soon as they
52
+ // diverge, without anyone having to await it.
53
+ this.stalledCheckerGeneration = 0;
50
54
  this.waiting = null;
51
55
  this.running = false;
52
56
  this.mainLoopRunning = null;
@@ -671,6 +675,12 @@ class Worker extends queue_base_1.QueueBase {
671
675
  * Pauses the processing of this queue only for this worker.
672
676
  */
673
677
  async pause(doNotWaitActive) {
678
+ // Kept in the caller's active context (not detached): a correctly
679
+ // implemented AsyncLocalStorage-based context manager isolates concurrent
680
+ // async chains, so a background stalled-checker tick (which detaches its
681
+ // own context; see `stalledChecker()`) cannot overwrite this span's
682
+ // context. Staying attached lets applications correlate `pause` with the
683
+ // request or operation that initiated it.
674
684
  await this.trace(enums_1.SpanKind.INTERNAL, 'pause', this.name, async (span) => {
675
685
  var _a;
676
686
  span === null || span === void 0 ? void 0 : span.setAttributes({
@@ -683,6 +693,14 @@ class Worker extends queue_base_1.QueueBase {
683
693
  if (!doNotWaitActive) {
684
694
  await this.whenCurrentJobsFinished();
685
695
  }
696
+ // Invalidate the currently running stalled-checker loop and cancel
697
+ // its pending timer. This is intentionally fire-and-forget: the
698
+ // loop may be in the middle of an in-flight Redis call (which, on
699
+ // a connection error, retries with its own delay), so awaiting it
700
+ // here could block `pause()` for an unbounded amount of time. The
701
+ // loop itself checks the generation on every iteration and will
702
+ // exit on its own as soon as it next has a chance to.
703
+ this.stalledCheckerGeneration++;
686
704
  (_a = this.stalledCheckStopper) === null || _a === void 0 ? void 0 : _a.call(this);
687
705
  this.emit('paused');
688
706
  }
@@ -695,6 +713,10 @@ class Worker extends queue_base_1.QueueBase {
695
713
  async resume() {
696
714
  try {
697
715
  if (!this.running || this.paused) {
716
+ let restartStalledChecker = false;
717
+ // Kept in the caller's active context for the same reason as
718
+ // `pause()`: only the background stalled-checker loop needs to
719
+ // detach its own context, so it cannot leak into this span.
698
720
  await this.trace(enums_1.SpanKind.INTERNAL, 'resume', this.name, async (span) => {
699
721
  span === null || span === void 0 ? void 0 : span.setAttributes({
700
722
  [enums_1.TelemetryAttributes.WorkerId]: this.id,
@@ -709,10 +731,15 @@ class Worker extends queue_base_1.QueueBase {
709
731
  else {
710
732
  // Main loop is still running (pause was called with doNotWaitActive=true).
711
733
  // Restart the stalled checker since pause() stopped it.
712
- await this.startStalledCheckTimer();
734
+ restartStalledChecker = true;
713
735
  }
714
736
  this.emit('resumed');
715
737
  });
738
+ // Started outside of the trace above so that the stalled checker loop
739
+ // does not inherit the (already ended) `resume` span as its parent.
740
+ if (restartStalledChecker) {
741
+ await this.startStalledCheckTimer();
742
+ }
716
743
  }
717
744
  }
718
745
  catch (error) {
@@ -801,28 +828,88 @@ class Worker extends queue_base_1.QueueBase {
801
828
  * @see {@link https://docs.bullmq.io/patterns/manually-fetching-jobs}
802
829
  */
803
830
  async startStalledCheckTimer() {
804
- if (!this.opts.skipStalledCheck) {
805
- if (!this.closing && !this.stalledCheckerRunning) {
806
- await this.trace(enums_1.SpanKind.INTERNAL, 'startStalledCheckTimer', this.name, async (span) => {
807
- span === null || span === void 0 ? void 0 : span.setAttributes({
808
- [enums_1.TelemetryAttributes.WorkerId]: this.id,
809
- [enums_1.TelemetryAttributes.WorkerName]: this.opts.name,
810
- });
811
- this.stalledCheckerRunning = true;
812
- this.stalledChecker()
813
- .catch(err => {
814
- this.emit('error', err);
815
- })
816
- .finally(() => {
817
- this.stalledCheckerRunning = false;
818
- });
819
- });
820
- }
831
+ var _a, _b;
832
+ if (this.opts.skipStalledCheck) {
833
+ return;
834
+ }
835
+ if (this.closing) {
836
+ return;
837
+ }
838
+ // Claim a new generation for this loop synchronously (before the first
839
+ // await), so that two concurrent callers (e.g. the automatic `run()`
840
+ // startup racing a manual `startStalledCheckTimer()` call, or `resume()`
841
+ // racing a not-yet-finished old loop that `pause()` just invalidated)
842
+ // cannot both end up owning a checker loop: only the call that captures
843
+ // the latest generation value proceeds past the check below.
844
+ const generation = ++this.stalledCheckerGeneration;
845
+ // Capture whatever loop (if any) is still winding down. Bumping the
846
+ // generation above only makes it stop at its next checkpoint; it does
847
+ // not cancel an in-flight `moveStalledJobsToWait` call, so we must let it
848
+ // fully finish before starting a new loop, otherwise both loops could run
849
+ // concurrently and scan for stalled jobs at the same time. This is
850
+ // chained below rather than awaited here in the public call path: an
851
+ // in-flight Redis call can be stuck behind `checkConnectionError`'s retry
852
+ // delay (or a slow reconnect) well after the worker has been paused, and
853
+ // callers such as `resume()` must return promptly instead of blocking on
854
+ // it, exactly like the previous "reuse the still-running checker"
855
+ // behavior used to.
856
+ const previousStalledCheckerPromise = this.stalledCheckerPromise;
857
+ // Wake up the previous loop's pending interval timer (if any) so it can
858
+ // observe the new generation and exit promptly, instead of leaving it to
859
+ // wait out the remainder of its `stalledInterval` wait.
860
+ (_a = this.stalledCheckStopper) === null || _a === void 0 ? void 0 : _a.call(this);
861
+ await this.trace(enums_1.SpanKind.INTERNAL, 'startStalledCheckTimer', this.name, async (span) => {
862
+ span === null || span === void 0 ? void 0 : span.setAttributes({
863
+ [enums_1.TelemetryAttributes.WorkerId]: this.id,
864
+ [enums_1.TelemetryAttributes.WorkerName]: this.opts.name,
865
+ });
866
+ });
867
+ // The trace above is asynchronous, so the worker may have been closed, or
868
+ // another call may have claimed a newer generation, while it was in
869
+ // flight.
870
+ if (this.closing || generation !== this.stalledCheckerGeneration) {
871
+ return;
821
872
  }
873
+ // Chain the new loop to start only once the previous one has fully
874
+ // exited (its errors are already handled by its own `.catch` below, so
875
+ // swallow them here), guaranteeing the two never scan concurrently -
876
+ // without awaiting that settlement from this public method, so
877
+ // `startStalledCheckTimer()`/`resume()` return promptly regardless of how
878
+ // long the previous loop's in-flight Redis call takes to settle.
879
+ this.stalledCheckerPromise = ((_b = previousStalledCheckerPromise === null || previousStalledCheckerPromise === void 0 ? void 0 : previousStalledCheckerPromise.catch(() => undefined)) !== null && _b !== void 0 ? _b : Promise.resolve())
880
+ .then(() => {
881
+ // The wait above can take a while (e.g. a connection error retry
882
+ // delay), during which the worker may have closed or yet another
883
+ // caller may have claimed a newer generation.
884
+ if (this.closing || generation !== this.stalledCheckerGeneration) {
885
+ return;
886
+ }
887
+ return (0, utils_1.withDetachedContext)(this.opts.telemetry, () => this.stalledChecker(generation));
888
+ })
889
+ .catch(err => {
890
+ this.emit('error', err);
891
+ });
822
892
  }
823
- async stalledChecker() {
824
- while (!(this.closing || this.paused)) {
893
+ async stalledChecker(generation) {
894
+ // The entire loop (including its recurring interval timer) is started
895
+ // under a detached root context by the caller above. Context managers
896
+ // backed by `AsyncLocalStorage` associate every continuation created
897
+ // inside a callback with whatever context was active when that
898
+ // continuation was created; detaching only the individual Redis call on
899
+ // each tick would still leave the long-lived `setTimeout` chain (and
900
+ // therefore every subsequent tick) parented to the caller's ambient span
901
+ // for the worker's whole lifetime, since a later no-op detached callback
902
+ // cannot retroactively replace a store that AsyncLocalStorage already
903
+ // restored to the outer (caller) context once the initial detached call
904
+ // returned.
905
+ while (!(this.closing || this.paused) &&
906
+ generation === this.stalledCheckerGeneration) {
825
907
  await this.checkConnectionError(() => this.moveStalledJobsToWait());
908
+ if (this.closing ||
909
+ this.paused ||
910
+ generation !== this.stalledCheckerGeneration) {
911
+ break;
912
+ }
826
913
  await new Promise(resolve => {
827
914
  const timeout = setTimeout(resolve, this.opts.stalledInterval);
828
915
  this.stalledCheckStopper = () => {
@@ -852,15 +939,15 @@ class Worker extends queue_base_1.QueueBase {
852
939
  reconnect && (await this.backend.reconnectBlocking());
853
940
  }
854
941
  async retryIfFailed(fn, opts) {
855
- var _a;
942
+ var _a, _b;
856
943
  let retry = 0;
857
- const maxRetries = opts.maxRetries || Infinity;
944
+ const maxRetries = (_a = opts.maxRetries) !== null && _a !== void 0 ? _a : Infinity;
858
945
  do {
859
946
  try {
860
947
  return await fn();
861
948
  }
862
949
  catch (err) {
863
- (_a = opts.span) === null || _a === void 0 ? void 0 : _a.recordException(err.message);
950
+ (_b = opts.span) === null || _b === void 0 ? void 0 : _b.recordException(err.message);
864
951
  if ((0, utils_1.isNotConnectionError)(err)) {
865
952
  // Emit error when not paused or closing; optionally swallow (no throw) when opts.onlyEmitError is set.
866
953
  if (!this.paused && !this.closing) {
@@ -874,6 +961,12 @@ class Worker extends queue_base_1.QueueBase {
874
961
  }
875
962
  }
876
963
  else {
964
+ if (this.closing || this.closed) {
965
+ if (opts.onlyEmitError) {
966
+ return;
967
+ }
968
+ throw err;
969
+ }
877
970
  if (opts.delayInMs && !this.closing && !this.closed) {
878
971
  await this.delay(opts.delayInMs, this.abortDelayController);
879
972
  }