bullmq 6.3.1 → 6.3.3

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.
@@ -49,15 +49,21 @@ function createBunRedisClient(client, opts) {
49
49
  */
50
50
  class BunRedisAdapter extends events_1.EventEmitter {
51
51
  get status() {
52
+ var _a;
52
53
  if (this.statusOverride) {
53
54
  return this.statusOverride;
54
55
  }
55
56
  if (this.closed) {
56
57
  return 'end';
57
58
  }
58
- if (this.raw.connected) {
59
+ // `raw` may not exist yet on a duplicate whose raw client is created
60
+ // lazily (via `rawFactory`) on first connect.
61
+ if (this.ready) {
59
62
  return 'ready';
60
63
  }
64
+ if ((_a = this.raw) === null || _a === void 0 ? void 0 : _a.connected) {
65
+ return 'connect';
66
+ }
61
67
  return this.hasConnected ? 'end' : 'wait';
62
68
  }
63
69
  set status(val) {
@@ -90,8 +96,14 @@ class BunRedisAdapter extends events_1.EventEmitter {
90
96
  this.reconnectTimer = null;
91
97
  this.reconnectAttempts = 0;
92
98
  this.maxReconnectDelay = 20000; // cap at 20s (matches ioredis default)
99
+ this.ready = false;
93
100
  this.isCluster = false;
94
- this._setupCallbacks();
101
+ this.rawFactory = opts === null || opts === void 0 ? void 0 : opts.rawFactory;
102
+ // When a `rawFactory` is provided the raw client is created lazily on the
103
+ // first `connect()`; callbacks are wired up there once it exists.
104
+ if (this.raw) {
105
+ this._setupCallbacks();
106
+ }
95
107
  // ioredis auto-connects by default. Mimic that behavior unless
96
108
  // lazyConnect is set.
97
109
  if (!(opts === null || opts === void 0 ? void 0 : opts.lazyConnect)) {
@@ -109,23 +121,10 @@ class BunRedisAdapter extends events_1.EventEmitter {
109
121
  // event until CLIENT SETNAME completes so callers waiting for 'ready'
110
122
  // see the name already applied.
111
123
  this.raw.onconnect = () => {
112
- this.hasConnected = true;
113
- this.closed = false;
114
- this.closing = false;
115
- this.reconnecting = false;
116
- this.reconnectAttempts = 0;
117
- this.statusOverride = undefined;
118
- // The server-side SCRIPT cache is gone for this (possibly new) raw
119
- // connection. Force re-loading on next use.
120
- this.loadedScriptShas.clear();
121
- if (this.connectionName) {
122
- this.clientSetName(this.connectionName).then(() => this.emit('ready'), () => this.emit('ready'));
123
- }
124
- else {
125
- this.emit('ready');
126
- }
124
+ this._handleConnected();
127
125
  };
128
126
  this.raw.onclose = (error) => {
127
+ this.ready = false;
129
128
  if (this.closing) {
130
129
  // User-initiated close – no reconnect
131
130
  this.closed = true;
@@ -142,6 +141,31 @@ class BunRedisAdapter extends events_1.EventEmitter {
142
141
  this._scheduleReconnect();
143
142
  };
144
143
  }
144
+ _handleConnected() {
145
+ this.hasConnected = true;
146
+ this.ready = false;
147
+ this.closed = false;
148
+ this.closing = false;
149
+ this.reconnecting = false;
150
+ this.reconnectAttempts = 0;
151
+ this.statusOverride = undefined;
152
+ // The server-side SCRIPT cache is gone for this (possibly new) raw
153
+ // connection. Force re-loading on next use.
154
+ this.loadedScriptShas.clear();
155
+ const markReady = () => {
156
+ this.ready = true;
157
+ this.emit('ready');
158
+ };
159
+ const readying = this.connectionName
160
+ ? this.clientSetName(this.connectionName).then(markReady, markReady)
161
+ : (markReady(), Promise.resolve());
162
+ this.readying = readying.finally(() => {
163
+ if (this.readying === readying) {
164
+ this.readying = undefined;
165
+ }
166
+ });
167
+ return this.readying;
168
+ }
145
169
  /**
146
170
  * Schedule a reconnection attempt with exponential backoff.
147
171
  */
@@ -160,10 +184,16 @@ class BunRedisAdapter extends events_1.EventEmitter {
160
184
  return;
161
185
  }
162
186
  try {
163
- // Create a fresh raw client with the same URL
164
- const BunRedisClient = this.raw
165
- .constructor;
166
- const newRaw = new BunRedisClient(this.raw.url);
187
+ // Create a fresh raw client aimed at the *same* server. Bun's native
188
+ // `duplicate()` preserves the connection target and options; the old
189
+ // `new BunRedisClient(this.raw.url)` produced a client pointed at Bun's
190
+ // default target because `url` is always undefined (#4582). If the raw
191
+ // client was never created (a duplicate reconnecting before its first
192
+ // connect), fall back to its lazy factory.
193
+ const newRaw = this.raw
194
+ ? await this._duplicateRaw(this.raw)
195
+ : await this.rawFactory();
196
+ this.rawFactory = undefined;
167
197
  // Swap the raw client reference
168
198
  this.raw = newRaw;
169
199
  this.closed = false;
@@ -186,6 +216,14 @@ class BunRedisAdapter extends events_1.EventEmitter {
186
216
  // Connection lifecycle
187
217
  // ---------------------------------------------------------------
188
218
  async connect() {
219
+ var _a, _b;
220
+ // A duplicate created with a `rawFactory` builds its raw client lazily on
221
+ // the first connect (Bun's native `duplicate()` is async).
222
+ if (!this.raw && this.rawFactory) {
223
+ this.raw = await this.rawFactory();
224
+ this.rawFactory = undefined;
225
+ this._setupCallbacks();
226
+ }
189
227
  const replaceRaw = this.hasConnected && (this.closed || !this.raw.connected);
190
228
  if (this.reconnectTimer) {
191
229
  clearTimeout(this.reconnectTimer);
@@ -197,6 +235,9 @@ class BunRedisAdapter extends events_1.EventEmitter {
197
235
  this.closed = false;
198
236
  this.closing = false;
199
237
  this.statusOverride = undefined;
238
+ if (!this.ready) {
239
+ await ((_a = this.readying) !== null && _a !== void 0 ? _a : this._handleConnected());
240
+ }
200
241
  return;
201
242
  }
202
243
  if (!this.connecting) {
@@ -204,11 +245,10 @@ class BunRedisAdapter extends events_1.EventEmitter {
204
245
  this.closing = false;
205
246
  this.statusOverride = undefined;
206
247
  // If the raw client was previously closed, Bun doesn't support
207
- // reconnecting on the same instance. Create a fresh raw client.
248
+ // reconnecting on the same instance. Create a fresh raw client aimed at
249
+ // the same server via Bun's native `duplicate()` (see #4582).
208
250
  if (replaceRaw) {
209
- const BunRedisClient = this.raw
210
- .constructor;
211
- this.raw = new BunRedisClient(this.raw.url);
251
+ this.raw = await this._duplicateRaw(this.raw);
212
252
  this._setupCallbacks();
213
253
  }
214
254
  this.connecting = this.raw
@@ -224,6 +264,31 @@ class BunRedisAdapter extends events_1.EventEmitter {
224
264
  });
225
265
  }
226
266
  await this.connecting;
267
+ await this.readying;
268
+ // Bun may report the socket as connected before this adapter transitions
269
+ // to ready (for example while applying CLIENT SETNAME on duplicates).
270
+ // Keep connect() aligned with ioredis semantics by waiting until the
271
+ // adapter is either ready or closed.
272
+ if (!this.ready && !this.closed && !this.closing && ((_b = this.raw) === null || _b === void 0 ? void 0 : _b.connected)) {
273
+ await new Promise(resolve => {
274
+ var _a;
275
+ const cleanup = () => {
276
+ this.off('ready', onDone);
277
+ this.off('close', onDone);
278
+ this.off('end', onDone);
279
+ };
280
+ const onDone = () => {
281
+ cleanup();
282
+ resolve();
283
+ };
284
+ this.on('ready', onDone);
285
+ this.on('close', onDone);
286
+ this.on('end', onDone);
287
+ if (this.ready || this.closed || this.closing || !((_a = this.raw) === null || _a === void 0 ? void 0 : _a.connected)) {
288
+ onDone();
289
+ }
290
+ });
291
+ }
227
292
  }
228
293
  _closeRaw() {
229
294
  // Cancel any pending reconnect
@@ -232,7 +297,12 @@ class BunRedisAdapter extends events_1.EventEmitter {
232
297
  this.reconnectTimer = null;
233
298
  }
234
299
  this.reconnecting = false;
300
+ // A duplicate closed before it ever connected has no raw client yet.
301
+ this.rawFactory = undefined;
235
302
  const raw = this.raw;
303
+ if (!raw) {
304
+ return;
305
+ }
236
306
  raw.onconnect = () => { };
237
307
  raw.onclose = () => { };
238
308
  raw.onerror = () => { };
@@ -262,18 +332,20 @@ class BunRedisAdapter extends events_1.EventEmitter {
262
332
  this.closed = true;
263
333
  this.statusOverride = undefined;
264
334
  const raw = this.raw;
265
- raw.onclose = () => { };
266
- if (raw.connected) {
267
- setImmediate(() => {
268
- try {
269
- if (raw.connected) {
270
- raw.close();
335
+ if (raw) {
336
+ raw.onclose = () => { };
337
+ if (raw.connected) {
338
+ setImmediate(() => {
339
+ try {
340
+ if (raw.connected) {
341
+ raw.close();
342
+ }
271
343
  }
272
- }
273
- catch (_err) {
274
- // swallow
275
- }
276
- });
344
+ catch (_err) {
345
+ // swallow
346
+ }
347
+ });
348
+ }
277
349
  }
278
350
  this.emit('close');
279
351
  this._scheduleReconnect();
@@ -306,15 +378,45 @@ class BunRedisAdapter extends events_1.EventEmitter {
306
378
  });
307
379
  return 'OK';
308
380
  }
381
+ /**
382
+ * Create a fresh raw client aimed at the *same* Redis server as `src`.
383
+ *
384
+ * Bun's `RedisClient` exposes no public connection info (no `url`, host,
385
+ * port or options), so the target cannot be reconstructed from the instance.
386
+ * Its native `duplicate()` is the only reliable way to clone the target and
387
+ * options; we fall back to URL-based reconstruction for exotic raw clients
388
+ * that don't implement it. See #4582.
389
+ */
390
+ async _duplicateRaw(src) {
391
+ if (typeof src.duplicate === 'function') {
392
+ return await src.duplicate();
393
+ }
394
+ const Ctor = src.constructor;
395
+ return new Ctor(src.url);
396
+ }
397
+ /**
398
+ * Return the raw client, materializing it first when this adapter is a
399
+ * lazily-initialized duplicate (created via `duplicate()` with a
400
+ * `rawFactory`). Command paths that touch `this.raw` directly use this so a
401
+ * duplicate can be used immediately without an explicit `connect()`.
402
+ */
403
+ async _ensureRaw() {
404
+ if (!this.raw) {
405
+ await this.connect();
406
+ }
407
+ return this.raw;
408
+ }
309
409
  duplicate(...args) {
310
- // Bun's duplicate() is async, but IRedisClient.duplicate() is sync.
311
- // We create a new RedisClient with the same URL/options instead.
312
- // The raw client constructor in Bun doesn't connect until connect() or
313
- // first command, so this is safe.
314
- const BunRedisClient = this.raw
315
- .constructor;
316
- const dup = new BunRedisClient(this.raw.url);
317
- const adapter = new BunRedisAdapter(dup);
410
+ // Bun's duplicate() is async, but IRedisClient.duplicate() is sync. The
411
+ // duplicate adapter is therefore created immediately with a `rawFactory`
412
+ // that clones the connection target lazily (via Bun's native duplicate)
413
+ // on first connect. Rebuilding from `this.raw.url` is not possible because
414
+ // Bun never exposes the URL, which previously sent duplicates to the
415
+ // wrong (default) server (#4582).
416
+ const parentRaw = this.raw;
417
+ const adapter = new BunRedisAdapter(undefined, {
418
+ rawFactory: () => this._duplicateRaw(parentRaw),
419
+ });
318
420
  // Copy registered scripts to the duplicate
319
421
  for (const [name, script] of this.scripts) {
320
422
  adapter.scripts.set(name, script);
@@ -411,6 +513,14 @@ class BunRedisAdapter extends events_1.EventEmitter {
411
513
  if (this.closing || this.closed) {
412
514
  return Promise.reject(new connection_closed_error_1.ConnectionClosedError('Connection is closed'));
413
515
  }
516
+ // A duplicate created via `duplicate()` builds its raw client lazily on the
517
+ // first `connect()` (Bun's native `duplicate()` is async). Materialize it
518
+ // here so commands issued before an explicit `connect()` don't throw on an
519
+ // undefined `raw` — matching other adapters where duplicates connect
520
+ // implicitly on first use.
521
+ if (!this.raw) {
522
+ return this.connect().then(() => this.sendCommand(command, args));
523
+ }
414
524
  // Send directly to the underlying Bun client. Redis protocol guarantees
415
525
  // responses arrive in the same order as requests on a single connection,
416
526
  // so concurrent send() calls are safe and enable implicit pipelining
@@ -440,10 +550,10 @@ class BunRedisAdapter extends events_1.EventEmitter {
440
550
  // Pipeline / Transaction
441
551
  // ---------------------------------------------------------------
442
552
  multi() {
443
- return new BunRedisTransaction(this.raw, this.scripts, true, this);
553
+ return new BunRedisTransaction(this.scripts, true, this);
444
554
  }
445
555
  pipeline() {
446
- return new BunRedisTransaction(this.raw, this.scripts, false, this);
556
+ return new BunRedisTransaction(this.scripts, false, this);
447
557
  }
448
558
  // ---------------------------------------------------------------
449
559
  // Hash commands
@@ -606,7 +716,7 @@ class BunRedisAdapter extends events_1.EventEmitter {
606
716
  for (const [k, v] of Object.entries(fields)) {
607
717
  args.push(k, String(v));
608
718
  }
609
- return await this.raw.send('XADD', args);
719
+ return await (await this._ensureRaw()).send('XADD', args);
610
720
  }
611
721
  async xread(streams, options) {
612
722
  const args = [];
@@ -833,8 +943,7 @@ class BunRedisAdapter extends events_1.EventEmitter {
833
943
  // them within a MULTI/EXEC block using send().
834
944
  // ---------------------------------------------------------------------------
835
945
  class BunRedisTransaction {
836
- constructor(raw, scripts, transactional, adapter) {
837
- this.raw = raw;
946
+ constructor(scripts, transactional, adapter) {
838
947
  this.scripts = scripts;
839
948
  this.transactional = transactional;
840
949
  this.adapter = adapter;
@@ -996,15 +1105,18 @@ class BunRedisTransaction {
996
1105
  const swallow = (_) => {
997
1106
  /* error surfaces via EXEC or the outer try/catch */
998
1107
  };
1108
+ // Materialize the raw client (a lazily-initialized duplicate may not have
1109
+ // one yet) so the MULTI…EXEC frames can be written as a contiguous burst.
1110
+ const raw = await this.adapter._ensureRaw();
999
1111
  try {
1000
1112
  // Fire MULTI without awaiting — no round-trip needed before commands.
1001
- this.raw.send('MULTI', []).catch(swallow);
1113
+ raw.send('MULTI', []).catch(swallow);
1002
1114
  // Fire all queued commands synchronously (no awaits).
1003
1115
  for (const { cmd, args } of this.commands) {
1004
- this.raw.send(cmd, args).catch(swallow);
1116
+ raw.send(cmd, args).catch(swallow);
1005
1117
  }
1006
1118
  // EXEC is the only await — it returns the array of results.
1007
- const results = await this.raw.send('EXEC', []);
1119
+ const results = await raw.send('EXEC', []);
1008
1120
  if (!results) {
1009
1121
  return null;
1010
1122
  }
@@ -1021,7 +1133,7 @@ class BunRedisTransaction {
1021
1133
  catch (err) {
1022
1134
  // Try to discard the MULTI state on error
1023
1135
  try {
1024
- await this.raw.send('DISCARD', []);
1136
+ await raw.send('DISCARD', []);
1025
1137
  }
1026
1138
  catch (_a) {
1027
1139
  // ignore
@@ -23,6 +23,44 @@ const queue_keys_1 = require("./queue-keys");
23
23
  * scan the whole state unboundedly.
24
24
  */
25
25
  const GET_JOBS_MAX_BACKFILL_ITERATIONS = 5;
26
+ /**
27
+ * Whether the client currently has a live connection. IORedis Cluster reports a
28
+ * usable connection as `connect` rather than `ready` (see
29
+ * `RedisConnection.waitUntilReady`).
30
+ */
31
+ function isClientLive(client) {
32
+ return (client.status === 'ready' ||
33
+ (client.status === 'connect' && (0, utils_1.isRedisCluster)(client)));
34
+ }
35
+ /**
36
+ * Resets a connection whose blocking command was abandoned by a watchdog.
37
+ *
38
+ * Under `maxRetriesPerRequest: null` IORedis silently re-queues and re-sends
39
+ * an interrupted blocking command instead of rejecting it, so the abandoned
40
+ * command outlives a reconnect and would be served ahead of the next blocking
41
+ * read. Getting rid of it requires tearing the socket down while it is live.
42
+ *
43
+ * If the client is currently socketless (`reconnecting`) we must NOT
44
+ * disconnect it: that clears IORedis' own retry timer without emitting a
45
+ * `close` event and parks it in `reconnecting` forever (#4585). Instead we
46
+ * wait (via `reconnect`) for it to reach a live state and only then tear the
47
+ * socket down, waiting for it to actually close (`disconnect(true)` awaits
48
+ * the `end` event) before reconnecting again: IORedis closes the socket
49
+ * asynchronously, so a bare `disconnect(false)` leaves `status` transiently
50
+ * `ready`, `reconnect()` would observe the stale status, return a no-op, and
51
+ * the pending close would kill the connection for good (#4585 ready-path
52
+ * race).
53
+ */
54
+ async function resetBlockedConnection(connection, client, reconnect) {
55
+ if (!isClientLive(client)) {
56
+ // Let IORedis' own retry timer bring the connection back first.
57
+ await reconnect();
58
+ }
59
+ if (isClientLive(client)) {
60
+ await connection.disconnect(true);
61
+ await reconnect();
62
+ }
63
+ }
26
64
  class RedisQueueBackend extends events_1.EventEmitter {
27
65
  constructor(connection, name, keys, toKey, opts, blockingConnection, ownsConnection = true) {
28
66
  var _a;
@@ -242,6 +280,14 @@ class RedisQueueBackend extends events_1.EventEmitter {
242
280
  ? 0.001
243
281
  : 0.002;
244
282
  }
283
+ /**
284
+ * Largest meaningful block timeout (seconds). Capped at 10s because a
285
+ * `BZPOPMIN` blocked longer than this risks issues on reconnection
286
+ * (see #1658).
287
+ */
288
+ get maximumBlockTimeout() {
289
+ return 10;
290
+ }
245
291
  /**
246
292
  * Interrupts the in-flight blocking wait by disconnecting the dedicated
247
293
  * blocking connection. No-op if there is none.
@@ -1821,7 +1867,7 @@ class RedisQueueBackend extends events_1.EventEmitter {
1821
1867
  // Worker blocking primitive (previously bzpopmin in Worker)
1822
1868
  // ============================================================
1823
1869
  async waitForJob(blockTimeout) {
1824
- var _a;
1870
+ var _a, _b;
1825
1871
  const conn = (_a = this.blockingConnection) !== null && _a !== void 0 ? _a : this.connection;
1826
1872
  const bclient = (await this.queue.blockingClient);
1827
1873
  const roundedTimeout = conn.capabilities.canDoubleTimeout
@@ -1850,7 +1896,9 @@ class RedisQueueBackend extends events_1.EventEmitter {
1850
1896
  const timeout = new Promise(resolve => {
1851
1897
  watchdog = setTimeout(() => {
1852
1898
  timedOut = true;
1853
- bclient.disconnect(false);
1899
+ // Resolve the wait as a timeout so the worker loop always advances.
1900
+ // The (possibly stuck) command is abandoned and the connection is
1901
+ // re-established in the `finally` block — see the note there.
1854
1902
  resolve(null);
1855
1903
  }, roundedTimeout * 1000 + 1000);
1856
1904
  });
@@ -1866,16 +1914,16 @@ class RedisQueueBackend extends events_1.EventEmitter {
1866
1914
  }
1867
1915
  finally {
1868
1916
  clearTimeout(watchdog);
1869
- // The watchdog disconnected the blocking connection without letting
1870
- // IORedis auto-resend the abandoned command. Since we resolved the wait
1871
- // as a timeout (rather than surfacing a rejection to the worker's own
1872
- // reconnect path), re-establish the dedicated blocking connection here so
1873
- // the next `waitForJob` starts from a healthy, unblocked connection.
1917
+ // The watchdog resolved the wait as a timeout because the awaited command
1918
+ // never settled. Reset the dedicated blocking connection so the next
1919
+ // `waitForJob` starts from a healthy, unblocked connection and the
1920
+ // abandoned command is actually dropped see
1921
+ // `resetBlockedConnection()`.
1874
1922
  if (timedOut && !this.closing) {
1875
1923
  try {
1876
- await this.reconnectBlocking();
1924
+ await resetBlockedConnection((_b = this.blockingConnection) !== null && _b !== void 0 ? _b : this.connection, bclient, () => this.reconnectBlocking());
1877
1925
  }
1878
- catch (_b) {
1926
+ catch (_c) {
1879
1927
  // Ignored: the next waitForJob call will retry the reconnect.
1880
1928
  }
1881
1929
  }
@@ -1915,7 +1963,9 @@ class RedisQueueBackend extends events_1.EventEmitter {
1915
1963
  const timeout = new Promise(resolve => {
1916
1964
  watchdog = setTimeout(() => {
1917
1965
  timedOut = true;
1918
- client.disconnect(false);
1966
+ // Resolve as a timeout so the consumer loop always advances. The
1967
+ // (possibly stuck) command is abandoned and the connection
1968
+ // re-established in the `finally` block — see the note there.
1919
1969
  resolve(null);
1920
1970
  }, blockTimeout + 1000);
1921
1971
  });
@@ -1924,12 +1974,12 @@ class RedisQueueBackend extends events_1.EventEmitter {
1924
1974
  }
1925
1975
  finally {
1926
1976
  clearTimeout(watchdog);
1927
- // The watchdog disconnected the connection without letting IORedis
1928
- // auto-resend the abandoned command. Re-establish it here so the next
1929
- // read starts from a healthy, unblocked connection.
1977
+ // Reset the connection so the next read starts healthy and the abandoned
1978
+ // command is actually dropped. Mirrors the `waitForJob` reset see
1979
+ // `resetBlockedConnection()`.
1930
1980
  if (timedOut && !this.closing) {
1931
1981
  try {
1932
- await this.connection.reconnect();
1982
+ await resetBlockedConnection(this.connection, client, () => this.connection.reconnect());
1933
1983
  }
1934
1984
  catch (_a) {
1935
1985
  // Ignored: the next readEvents call will retry the reconnect.
@@ -14,8 +14,9 @@ const errors_1 = require("./errors");
14
14
  const enums_1 = require("../enums");
15
15
  const job_scheduler_1 = require("./job-scheduler");
16
16
  const lock_manager_1 = require("./lock-manager");
17
- // 10 seconds is the maximum time a BZPOPMIN can block.
18
- const maximumBlockTimeout = 10;
17
+ // 10 seconds is the maximum time a BZPOPMIN can block, so it is the default
18
+ // ceiling used when a backend does not delegate its own `maximumBlockTimeout`.
19
+ const defaultMaximumBlockTimeout = 10;
19
20
  /**
20
21
  *
21
22
  * This class represents a worker that is able to process jobs from the queue.
@@ -396,6 +397,10 @@ class Worker extends queue_base_1.QueueBase {
396
397
  get minimumBlockTimeout() {
397
398
  return this.backend.minimumBlockTimeout;
398
399
  }
400
+ get maximumBlockTimeout() {
401
+ var _a;
402
+ return (_a = this.backend.maximumBlockTimeout) !== null && _a !== void 0 ? _a : defaultMaximumBlockTimeout;
403
+ }
399
404
  isRateLimited() {
400
405
  return this.limitUntil > Date.now();
401
406
  }
@@ -466,10 +471,13 @@ class Worker extends queue_base_1.QueueBase {
466
471
  return this.minimumBlockTimeout;
467
472
  }
468
473
  else {
469
- // We restrict the maximum block timeout to 10 second to avoid
470
- // blocking the connection for too long in the case of reconnections
471
- // reference: https://github.com/taskforcesh/bullmq/issues/1658
472
- return Math.min(blockDelay / 1000, maximumBlockTimeout);
474
+ // We restrict the maximum block timeout to avoid blocking the
475
+ // connection for too long in the case of reconnections. The ceiling is
476
+ // backend-specific: Redis caps it at 10s (a `BZPOPMIN` blocked longer
477
+ // risks issues on reconnection, see #1658), whereas a backend that
478
+ // keeps the connection open and re-arms to the next due job can allow a
479
+ // much larger value so an idle worker stops re-polling.
480
+ return Math.min(blockDelay / 1000, this.maximumBlockTimeout);
473
481
  }
474
482
  }
475
483
  else {