bullmq 5.77.3 → 5.77.5
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/dist/cjs/classes/redis-connection.js +47 -12
- package/dist/cjs/commands/includes/requeueDeduplicatedJob.lua +10 -2
- package/dist/cjs/commands/includes/storeDeduplicatedNextJob.lua +3 -1
- package/dist/cjs/scripts/addDelayedJob-6.js +3 -1
- package/dist/cjs/scripts/addParentJob-6.js +3 -1
- package/dist/cjs/scripts/addPrioritizedJob-9.js +3 -1
- package/dist/cjs/scripts/addStandardJob-9.js +3 -1
- package/dist/cjs/scripts/moveToFinished-14.js +10 -2
- package/dist/cjs/tsconfig-cjs.tsbuildinfo +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/classes/redis-connection.d.ts +2 -0
- package/dist/esm/classes/redis-connection.js +47 -12
- package/dist/esm/commands/includes/requeueDeduplicatedJob.lua +10 -2
- package/dist/esm/commands/includes/storeDeduplicatedNextJob.lua +3 -1
- package/dist/esm/scripts/addDelayedJob-6.js +3 -1
- package/dist/esm/scripts/addParentJob-6.js +3 -1
- package/dist/esm/scripts/addPrioritizedJob-9.js +3 -1
- package/dist/esm/scripts/addStandardJob-9.js +3 -1
- package/dist/esm/scripts/moveToFinished-14.js +10 -2
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
|
@@ -23,6 +23,13 @@ const clusterOriginalBzpopmin = Symbol('bullmqClusterOriginalBzpopmin');
|
|
|
23
23
|
const clusterWrappedBzpopmin = Symbol('bullmqClusterWrappedBzpopmin');
|
|
24
24
|
const clusterPatchRefCount = Symbol('bullmqClusterPatchRefCount');
|
|
25
25
|
const clusterClosingRefCount = Symbol('bullmqClusterClosingRefCount');
|
|
26
|
+
// Hard cap on a single cluster reconnect attempt. Without this, a hung
|
|
27
|
+
// `client.connect()` (e.g. ioredis Cluster cannot recover and slot refresh
|
|
28
|
+
// keeps retrying internally) leaves `clusterReconnectPromise` pinned forever
|
|
29
|
+
// and every subsequent bzpopmin call awaits the same dead promise, deadlocking
|
|
30
|
+
// the worker. 30s is comfortably above the default ioredis slotsRefreshTimeout
|
|
31
|
+
// while bounded enough that wedged workers recover within one or two ticks.
|
|
32
|
+
const DEFAULT_CLUSTER_RECONNECT_TIMEOUT_MS = 30000;
|
|
26
33
|
class RedisConnection extends events_1.EventEmitter {
|
|
27
34
|
constructor(opts, extraOptions) {
|
|
28
35
|
super();
|
|
@@ -36,7 +43,7 @@ class RedisConnection extends events_1.EventEmitter {
|
|
|
36
43
|
this.packageVersion = version_1.version;
|
|
37
44
|
this.disabledBlockingClusterReconnect = false;
|
|
38
45
|
// Set extra options defaults
|
|
39
|
-
this.extraOptions = Object.assign({ shared: false, blocking: true, skipVersionCheck: false, skipWaitingForReady: false }, extraOptions);
|
|
46
|
+
this.extraOptions = Object.assign({ shared: false, blocking: true, skipVersionCheck: false, skipWaitingForReady: false, clusterReconnectTimeoutMs: DEFAULT_CLUSTER_RECONNECT_TIMEOUT_MS }, extraOptions);
|
|
40
47
|
if (!(0, utils_2.isRedisInstance)(opts)) {
|
|
41
48
|
this.checkBlockingOptions(overrideMessage, opts);
|
|
42
49
|
this.opts = Object.assign({ port: 6379, host: '127.0.0.1', retryStrategy: function (times) {
|
|
@@ -210,6 +217,7 @@ class RedisConnection extends events_1.EventEmitter {
|
|
|
210
217
|
return this._client;
|
|
211
218
|
}
|
|
212
219
|
patchBlockingClusterClient() {
|
|
220
|
+
var _a;
|
|
213
221
|
const client = this._client;
|
|
214
222
|
const blockingClient = client;
|
|
215
223
|
if (!this.extraOptions.blocking ||
|
|
@@ -217,6 +225,7 @@ class RedisConnection extends events_1.EventEmitter {
|
|
|
217
225
|
typeof blockingClient.bzpopmin !== 'function') {
|
|
218
226
|
return;
|
|
219
227
|
}
|
|
228
|
+
const reconnectTimeoutMs = (_a = this.extraOptions.clusterReconnectTimeoutMs) !== null && _a !== void 0 ? _a : DEFAULT_CLUSTER_RECONNECT_TIMEOUT_MS;
|
|
220
229
|
blockingClient[clusterPatchRefCount] =
|
|
221
230
|
(blockingClient[clusterPatchRefCount] || 0) + 1;
|
|
222
231
|
this.patchedBlockingClusterClient = blockingClient;
|
|
@@ -225,7 +234,7 @@ class RedisConnection extends events_1.EventEmitter {
|
|
|
225
234
|
}
|
|
226
235
|
const bzpopmin = blockingClient.bzpopmin;
|
|
227
236
|
const wrappedBzpopmin = async (...args) => {
|
|
228
|
-
await RedisConnection.reconnectClusterIfNeeded(blockingClient);
|
|
237
|
+
await RedisConnection.reconnectClusterIfNeeded(blockingClient, reconnectTimeoutMs);
|
|
229
238
|
try {
|
|
230
239
|
return await bzpopmin.apply(blockingClient, args);
|
|
231
240
|
}
|
|
@@ -233,7 +242,7 @@ class RedisConnection extends events_1.EventEmitter {
|
|
|
233
242
|
const commandError = error;
|
|
234
243
|
if (RedisConnection.shouldReconnectClusterAfterError(blockingClient, commandError)) {
|
|
235
244
|
try {
|
|
236
|
-
await RedisConnection.reconnectCluster(blockingClient);
|
|
245
|
+
await RedisConnection.reconnectCluster(blockingClient, reconnectTimeoutMs);
|
|
237
246
|
}
|
|
238
247
|
catch (_a) {
|
|
239
248
|
// Preserve the original command failure if best-effort recovery fails.
|
|
@@ -298,10 +307,10 @@ class RedisConnection extends events_1.EventEmitter {
|
|
|
298
307
|
client.status === 'end' ||
|
|
299
308
|
client.status === 'closing');
|
|
300
309
|
}
|
|
301
|
-
static async reconnectClusterIfNeeded(client) {
|
|
310
|
+
static async reconnectClusterIfNeeded(client, timeoutMs) {
|
|
302
311
|
if (!RedisConnection.isReconnectingDisabled(client) &&
|
|
303
312
|
RedisConnection.isClusterWithEmptyNodes(client)) {
|
|
304
|
-
await RedisConnection.reconnectCluster(client);
|
|
313
|
+
await RedisConnection.reconnectCluster(client, timeoutMs);
|
|
305
314
|
}
|
|
306
315
|
}
|
|
307
316
|
static shouldReconnectClusterAfterError(client, error) {
|
|
@@ -317,20 +326,46 @@ class RedisConnection extends events_1.EventEmitter {
|
|
|
317
326
|
return (RedisConnection.isClusterWithEmptyNodes(client) ||
|
|
318
327
|
/Command timed out|Failed to refresh slots cache/i.test(message));
|
|
319
328
|
}
|
|
320
|
-
static async reconnectCluster(client) {
|
|
329
|
+
static async reconnectCluster(client, timeoutMs) {
|
|
321
330
|
if (RedisConnection.isReconnectingDisabled(client)) {
|
|
322
331
|
return;
|
|
323
332
|
}
|
|
324
333
|
if (!client[clusterReconnectPromise]) {
|
|
325
|
-
client[clusterReconnectPromise] =
|
|
326
|
-
client.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
client[clusterReconnectPromise] = null;
|
|
330
|
-
});
|
|
334
|
+
client[clusterReconnectPromise] =
|
|
335
|
+
RedisConnection.connectClusterWithTimeout(client, timeoutMs).finally(() => {
|
|
336
|
+
client[clusterReconnectPromise] = null;
|
|
337
|
+
});
|
|
331
338
|
}
|
|
332
339
|
await client[clusterReconnectPromise];
|
|
333
340
|
}
|
|
341
|
+
// Disconnects and reconnects a cluster client, racing connect() against a
|
|
342
|
+
// hard cap so a hung reconnect cannot pin `clusterReconnectPromise`
|
|
343
|
+
// indefinitely. On timeout, the underlying connect() is left running
|
|
344
|
+
// (ioredis Cluster handles its own retry/state); we simply stop awaiting it.
|
|
345
|
+
// The next bzpopmin call's `reconnectClusterIfNeeded` check will trigger a
|
|
346
|
+
// fresh reconnect if the pool is still empty.
|
|
347
|
+
static async connectClusterWithTimeout(client, timeoutMs) {
|
|
348
|
+
client.disconnect(false);
|
|
349
|
+
let timeoutHandle;
|
|
350
|
+
try {
|
|
351
|
+
await Promise.race([
|
|
352
|
+
client.connect(),
|
|
353
|
+
new Promise((_, reject) => {
|
|
354
|
+
var _a;
|
|
355
|
+
timeoutHandle = setTimeout(() => {
|
|
356
|
+
reject(new connection_closed_error_1.ConnectionClosedError(`BullMQ: cluster reconnect timed out after ${timeoutMs}ms`));
|
|
357
|
+
}, timeoutMs);
|
|
358
|
+
// Don't keep the event loop alive solely for this timer.
|
|
359
|
+
(_a = timeoutHandle.unref) === null || _a === void 0 ? void 0 : _a.call(timeoutHandle);
|
|
360
|
+
}),
|
|
361
|
+
]);
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
if (timeoutHandle) {
|
|
365
|
+
clearTimeout(timeoutHandle);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
334
369
|
async disconnect(wait = true) {
|
|
335
370
|
const client = await this.client;
|
|
336
371
|
if (client.status !== 'end') {
|
|
@@ -17,9 +17,17 @@ local function requeueDeduplicatedJob(prefix, deduplicationId, eventStreamKey,
|
|
|
17
17
|
local deduplicationNextKey = prefix .. "dn:" .. deduplicationId
|
|
18
18
|
if rcall("EXISTS", deduplicationNextKey) == 1 then
|
|
19
19
|
local nextData = rcall("HMGET", deduplicationNextKey,
|
|
20
|
-
"name", "data", "opts", "pk", "pd", "pdk", "rjk")
|
|
20
|
+
"name", "data", "opts", "pk", "pd", "pdk", "rjk", "jid")
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
-- Always increment the counter to keep it monotonic
|
|
23
|
+
local nextId = rcall("INCR", prefix .. "id") .. ""
|
|
24
|
+
local storedJobId = nextData[8] -- index 8 = "jid" (8th field in the HMGET call above)
|
|
25
|
+
local newJobId
|
|
26
|
+
if storedJobId then
|
|
27
|
+
newJobId = storedJobId
|
|
28
|
+
else
|
|
29
|
+
newJobId = nextId
|
|
30
|
+
end
|
|
23
31
|
local newJobIdKey = prefix .. newJobId
|
|
24
32
|
local newOpts = cjson.decode(nextData[3])
|
|
25
33
|
local deduplicationKey = prefix .. "de:" .. deduplicationId
|
|
@@ -14,7 +14,8 @@ local function storeDeduplicatedNextJob(deduplicationOpts, currentDebounceJobId,
|
|
|
14
14
|
local activeItems = rcall('LRANGE', activeKey, 0, -1)
|
|
15
15
|
if checkItemInList(activeItems, currentDebounceJobId) then
|
|
16
16
|
local deduplicationNextKey = prefix .. "dn:" .. deduplicationId
|
|
17
|
-
local fields = {'name', jobName, 'data', jobData, 'opts', cjson.encode(fullOpts)
|
|
17
|
+
local fields = {'name', jobName, 'data', jobData, 'opts', cjson.encode(fullOpts),
|
|
18
|
+
'jid', jobId}
|
|
18
19
|
|
|
19
20
|
if parentKey then
|
|
20
21
|
fields[#fields+1] = 'pk'
|
|
@@ -36,6 +37,7 @@ local function storeDeduplicatedNextJob(deduplicationOpts, currentDebounceJobId,
|
|
|
36
37
|
fields[#fields+1] = repeatJobKey
|
|
37
38
|
end
|
|
38
39
|
|
|
40
|
+
rcall('DEL', deduplicationNextKey)
|
|
39
41
|
rcall('HSET', deduplicationNextKey, unpack(fields))
|
|
40
42
|
|
|
41
43
|
-- Ensure the dedup key does not expire while the job is active,
|
|
@@ -155,7 +155,8 @@ local function storeDeduplicatedNextJob(deduplicationOpts, currentDebounceJobId,
|
|
|
155
155
|
local activeItems = rcall('LRANGE', activeKey, 0, -1)
|
|
156
156
|
if checkItemInList(activeItems, currentDebounceJobId) then
|
|
157
157
|
local deduplicationNextKey = prefix .. "dn:" .. deduplicationId
|
|
158
|
-
local fields = {'name', jobName, 'data', jobData, 'opts', cjson.encode(fullOpts)
|
|
158
|
+
local fields = {'name', jobName, 'data', jobData, 'opts', cjson.encode(fullOpts),
|
|
159
|
+
'jid', jobId}
|
|
159
160
|
if parentKey then
|
|
160
161
|
fields[#fields+1] = 'pk'
|
|
161
162
|
fields[#fields+1] = parentKey
|
|
@@ -172,6 +173,7 @@ local function storeDeduplicatedNextJob(deduplicationOpts, currentDebounceJobId,
|
|
|
172
173
|
fields[#fields+1] = 'rjk'
|
|
173
174
|
fields[#fields+1] = repeatJobKey
|
|
174
175
|
end
|
|
176
|
+
rcall('DEL', deduplicationNextKey)
|
|
175
177
|
rcall('HSET', deduplicationNextKey, unpack(fields))
|
|
176
178
|
-- Ensure the dedup key does not expire while the job is active,
|
|
177
179
|
-- so subsequent adds always hit the dedup path and never bypass
|
|
@@ -86,7 +86,8 @@ local function storeDeduplicatedNextJob(deduplicationOpts, currentDebounceJobId,
|
|
|
86
86
|
local activeItems = rcall('LRANGE', activeKey, 0, -1)
|
|
87
87
|
if checkItemInList(activeItems, currentDebounceJobId) then
|
|
88
88
|
local deduplicationNextKey = prefix .. "dn:" .. deduplicationId
|
|
89
|
-
local fields = {'name', jobName, 'data', jobData, 'opts', cjson.encode(fullOpts)
|
|
89
|
+
local fields = {'name', jobName, 'data', jobData, 'opts', cjson.encode(fullOpts),
|
|
90
|
+
'jid', jobId}
|
|
90
91
|
if parentKey then
|
|
91
92
|
fields[#fields+1] = 'pk'
|
|
92
93
|
fields[#fields+1] = parentKey
|
|
@@ -103,6 +104,7 @@ local function storeDeduplicatedNextJob(deduplicationOpts, currentDebounceJobId,
|
|
|
103
104
|
fields[#fields+1] = 'rjk'
|
|
104
105
|
fields[#fields+1] = repeatJobKey
|
|
105
106
|
end
|
|
107
|
+
rcall('DEL', deduplicationNextKey)
|
|
106
108
|
rcall('HSET', deduplicationNextKey, unpack(fields))
|
|
107
109
|
-- Ensure the dedup key does not expire while the job is active,
|
|
108
110
|
-- so subsequent adds always hit the dedup path and never bypass
|
|
@@ -120,7 +120,8 @@ local function storeDeduplicatedNextJob(deduplicationOpts, currentDebounceJobId,
|
|
|
120
120
|
local activeItems = rcall('LRANGE', activeKey, 0, -1)
|
|
121
121
|
if checkItemInList(activeItems, currentDebounceJobId) then
|
|
122
122
|
local deduplicationNextKey = prefix .. "dn:" .. deduplicationId
|
|
123
|
-
local fields = {'name', jobName, 'data', jobData, 'opts', cjson.encode(fullOpts)
|
|
123
|
+
local fields = {'name', jobName, 'data', jobData, 'opts', cjson.encode(fullOpts),
|
|
124
|
+
'jid', jobId}
|
|
124
125
|
if parentKey then
|
|
125
126
|
fields[#fields+1] = 'pk'
|
|
126
127
|
fields[#fields+1] = parentKey
|
|
@@ -137,6 +138,7 @@ local function storeDeduplicatedNextJob(deduplicationOpts, currentDebounceJobId,
|
|
|
137
138
|
fields[#fields+1] = 'rjk'
|
|
138
139
|
fields[#fields+1] = repeatJobKey
|
|
139
140
|
end
|
|
141
|
+
rcall('DEL', deduplicationNextKey)
|
|
140
142
|
rcall('HSET', deduplicationNextKey, unpack(fields))
|
|
141
143
|
-- Ensure the dedup key does not expire while the job is active,
|
|
142
144
|
-- so subsequent adds always hit the dedup path and never bypass
|
|
@@ -114,7 +114,8 @@ local function storeDeduplicatedNextJob(deduplicationOpts, currentDebounceJobId,
|
|
|
114
114
|
local activeItems = rcall('LRANGE', activeKey, 0, -1)
|
|
115
115
|
if checkItemInList(activeItems, currentDebounceJobId) then
|
|
116
116
|
local deduplicationNextKey = prefix .. "dn:" .. deduplicationId
|
|
117
|
-
local fields = {'name', jobName, 'data', jobData, 'opts', cjson.encode(fullOpts)
|
|
117
|
+
local fields = {'name', jobName, 'data', jobData, 'opts', cjson.encode(fullOpts),
|
|
118
|
+
'jid', jobId}
|
|
118
119
|
if parentKey then
|
|
119
120
|
fields[#fields+1] = 'pk'
|
|
120
121
|
fields[#fields+1] = parentKey
|
|
@@ -131,6 +132,7 @@ local function storeDeduplicatedNextJob(deduplicationOpts, currentDebounceJobId,
|
|
|
131
132
|
fields[#fields+1] = 'rjk'
|
|
132
133
|
fields[#fields+1] = repeatJobKey
|
|
133
134
|
end
|
|
135
|
+
rcall('DEL', deduplicationNextKey)
|
|
134
136
|
rcall('HSET', deduplicationNextKey, unpack(fields))
|
|
135
137
|
-- Ensure the dedup key does not expire while the job is active,
|
|
136
138
|
-- so subsequent adds always hit the dedup path and never bypass
|
|
@@ -829,8 +829,16 @@ local function requeueDeduplicatedJob(prefix, deduplicationId, eventStreamKey,
|
|
|
829
829
|
local deduplicationNextKey = prefix .. "dn:" .. deduplicationId
|
|
830
830
|
if rcall("EXISTS", deduplicationNextKey) == 1 then
|
|
831
831
|
local nextData = rcall("HMGET", deduplicationNextKey,
|
|
832
|
-
"name", "data", "opts", "pk", "pd", "pdk", "rjk")
|
|
833
|
-
|
|
832
|
+
"name", "data", "opts", "pk", "pd", "pdk", "rjk", "jid")
|
|
833
|
+
-- Always increment the counter to keep it monotonic
|
|
834
|
+
local nextId = rcall("INCR", prefix .. "id") .. ""
|
|
835
|
+
local storedJobId = nextData[8] -- index 8 = "jid" (8th field in the HMGET call above)
|
|
836
|
+
local newJobId
|
|
837
|
+
if storedJobId then
|
|
838
|
+
newJobId = storedJobId
|
|
839
|
+
else
|
|
840
|
+
newJobId = nextId
|
|
841
|
+
end
|
|
834
842
|
local newJobIdKey = prefix .. newJobId
|
|
835
843
|
local newOpts = cjson.decode(nextData[3])
|
|
836
844
|
local deduplicationKey = prefix .. "de:" .. deduplicationId
|