ioredis 5.8.1 β 5.9.0
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/README.md +16 -2
- package/built/Command.d.ts +44 -0
- package/built/Command.js +114 -14
- package/built/Pipeline.js +2 -1
- package/built/Redis.d.ts +2 -0
- package/built/Redis.js +47 -5
- package/built/autoPipelining.js +1 -0
- package/built/cluster/ClusterSubscriberGroup.d.ts +45 -26
- package/built/cluster/ClusterSubscriberGroup.js +188 -99
- package/built/cluster/ShardedSubscriber.d.ts +20 -0
- package/built/cluster/ShardedSubscriber.js +89 -0
- package/built/cluster/index.d.ts +2 -0
- package/built/cluster/index.js +71 -7
- package/built/redis/RedisOptions.d.ts +10 -0
- package/built/redis/RedisOptions.js +2 -1
- package/built/redis/event_handler.js +44 -43
- package/built/utils/argumentParsers.d.ts +14 -0
- package/built/utils/argumentParsers.js +74 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ ioredis is a stable project and maintenance is done on a best-effort basis for r
|
|
|
21
21
|
ioredis is a robust, full-featured Redis client that is
|
|
22
22
|
used in the world's biggest online commerce company [Alibaba](http://www.alibaba.com/) and many other awesome companies.
|
|
23
23
|
|
|
24
|
-
0. Full-featured. It supports [Cluster](http://redis.io/topics/cluster-tutorial), [Sentinel](https://redis.io/docs/
|
|
24
|
+
0. Full-featured. It supports [Cluster](http://redis.io/topics/cluster-tutorial), [Sentinel](https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/), [Streams](https://redis.io/topics/streams-intro), [Pipelining](http://redis.io/topics/pipelining), and of course [Lua scripting](http://redis.io/commands/eval), [Redis Functions](https://redis.io/topics/functions-intro), [Pub/Sub](http://redis.io/topics/pubsub) (with the support of binary messages).
|
|
25
25
|
1. High performance π.
|
|
26
26
|
2. Delightful API π. It works with Node callbacks and Native promises.
|
|
27
27
|
3. Transformation of command arguments and replies.
|
|
@@ -798,6 +798,20 @@ const redis = new Redis({
|
|
|
798
798
|
|
|
799
799
|
Set maxRetriesPerRequest to `null` to disable this behavior, and every command will wait forever until the connection is alive again (which is the default behavior before ioredis v4).
|
|
800
800
|
|
|
801
|
+
### Blocking Command Timeout
|
|
802
|
+
|
|
803
|
+
ioredis can apply a client-side timeout to blocking commands (such as `blpop`, `brpop`, `bzpopmin`, `bzmpop`, `blmpop`, `xread`, `xreadgroup`, etc.). This protects against scenarios where the TCP connection becomes a zombie (e.g., due to a silent network failure like a Docker network disconnect) and Redis never replies.
|
|
804
|
+
|
|
805
|
+
For commands with a finite timeout (e.g., `blpop("key", 5)`), ioredis automatically sets a client-side deadline based on the command's timeout plus a small grace period. If no reply arrives before the deadline, the command resolves with `null`βthe same value Redis returns when a blocking command times out normally.
|
|
806
|
+
|
|
807
|
+
For commands that intentionally block forever (e.g., `timeout = 0` or `BLOCK 0`), you can provide a safety net via the optional `blockingTimeout` option (milliseconds):
|
|
808
|
+
|
|
809
|
+
```javascript
|
|
810
|
+
const redis = new Redis({
|
|
811
|
+
blockingTimeout: 30000, // Resolve with null after 30 seconds when timeout=0/BLOCK 0
|
|
812
|
+
});
|
|
813
|
+
```
|
|
814
|
+
|
|
801
815
|
### Reconnect on Error
|
|
802
816
|
|
|
803
817
|
Besides auto-reconnect when the connection is closed, ioredis supports reconnecting on certain Redis errors using the `reconnectOnError` option. Here's an example that will reconnect when receiving `READONLY` error:
|
|
@@ -1213,7 +1227,7 @@ cluster.on("smessage", (channel, message) => {
|
|
|
1213
1227
|
console.log(message);
|
|
1214
1228
|
});
|
|
1215
1229
|
|
|
1216
|
-
|
|
1230
|
+
|
|
1217
1231
|
//Subscribe to the channels on the same slot
|
|
1218
1232
|
cluster.ssubscribe("channel{my}:1", "channel{my}:2").then( ( count: number ) => {
|
|
1219
1233
|
console.log(count);
|
package/built/Command.d.ts
CHANGED
|
@@ -30,6 +30,30 @@ export interface CommandNameFlags {
|
|
|
30
30
|
ENTER_SUBSCRIBER_MODE: ["subscribe", "psubscribe", "ssubscribe"];
|
|
31
31
|
EXIT_SUBSCRIBER_MODE: ["unsubscribe", "punsubscribe", "sunsubscribe"];
|
|
32
32
|
WILL_DISCONNECT: ["quit"];
|
|
33
|
+
HANDSHAKE_COMMANDS: ["auth", "select", "client", "readonly", "info"];
|
|
34
|
+
IGNORE_RECONNECT_ON_ERROR: ["client"];
|
|
35
|
+
BLOCKING_COMMANDS: [
|
|
36
|
+
"blpop",
|
|
37
|
+
"brpop",
|
|
38
|
+
"brpoplpush",
|
|
39
|
+
"blmove",
|
|
40
|
+
"bzpopmin",
|
|
41
|
+
"bzpopmax",
|
|
42
|
+
"bzmpop",
|
|
43
|
+
"blmpop",
|
|
44
|
+
"xread",
|
|
45
|
+
"xreadgroup"
|
|
46
|
+
];
|
|
47
|
+
LAST_ARG_TIMEOUT_COMMANDS: [
|
|
48
|
+
"blpop",
|
|
49
|
+
"brpop",
|
|
50
|
+
"brpoplpush",
|
|
51
|
+
"blmove",
|
|
52
|
+
"bzpopmin",
|
|
53
|
+
"bzpopmax"
|
|
54
|
+
];
|
|
55
|
+
FIRST_ARG_TIMEOUT_COMMANDS: ["bzmpop", "blmpop"];
|
|
56
|
+
BLOCK_OPTION_COMMANDS: ["xread", "xreadgroup"];
|
|
33
57
|
}
|
|
34
58
|
/**
|
|
35
59
|
* Command instance
|
|
@@ -80,6 +104,8 @@ export default class Command implements Respondable {
|
|
|
80
104
|
private callback;
|
|
81
105
|
private transformed;
|
|
82
106
|
private _commandTimeoutTimer?;
|
|
107
|
+
private _blockingTimeoutTimer?;
|
|
108
|
+
private _blockingDeadline?;
|
|
83
109
|
private slot?;
|
|
84
110
|
private keys?;
|
|
85
111
|
/**
|
|
@@ -108,6 +134,24 @@ export default class Command implements Respondable {
|
|
|
108
134
|
* and generating an error.
|
|
109
135
|
*/
|
|
110
136
|
setTimeout(ms: number): void;
|
|
137
|
+
/**
|
|
138
|
+
* Set a timeout for blocking commands.
|
|
139
|
+
* When the timeout expires, the command resolves with null (matching Redis behavior).
|
|
140
|
+
* This handles the case of undetectable network failures (e.g., docker network disconnect)
|
|
141
|
+
* where the TCP connection becomes a zombie and no close event fires.
|
|
142
|
+
*/
|
|
143
|
+
setBlockingTimeout(ms: number): void;
|
|
144
|
+
/**
|
|
145
|
+
* Extract the blocking timeout from the command arguments.
|
|
146
|
+
*
|
|
147
|
+
* @returns The timeout in seconds, null for indefinite blocking (timeout of 0),
|
|
148
|
+
* or undefined if this is not a blocking command
|
|
149
|
+
*/
|
|
150
|
+
extractBlockingTimeout(): number | null | undefined;
|
|
151
|
+
/**
|
|
152
|
+
* Clear the command and blocking timers
|
|
153
|
+
*/
|
|
154
|
+
private _clearTimers;
|
|
111
155
|
private initPromise;
|
|
112
156
|
/**
|
|
113
157
|
* Iterate through the command arguments that are considered keys.
|
package/built/Command.js
CHANGED
|
@@ -4,6 +4,7 @@ const commands_1 = require("@ioredis/commands");
|
|
|
4
4
|
const calculateSlot = require("cluster-key-slot");
|
|
5
5
|
const standard_as_callback_1 = require("standard-as-callback");
|
|
6
6
|
const utils_1 = require("./utils");
|
|
7
|
+
const argumentParsers_1 = require("./utils/argumentParsers");
|
|
7
8
|
/**
|
|
8
9
|
* Command instance
|
|
9
10
|
*
|
|
@@ -72,6 +73,7 @@ class Command {
|
|
|
72
73
|
* Check whether the command has the flag
|
|
73
74
|
*/
|
|
74
75
|
static checkFlag(flagName, commandName) {
|
|
76
|
+
commandName = commandName.toLowerCase();
|
|
75
77
|
return !!this.getFlagMap()[flagName][commandName];
|
|
76
78
|
}
|
|
77
79
|
static setArgumentTransformer(name, func) {
|
|
@@ -194,6 +196,81 @@ class Command {
|
|
|
194
196
|
}, ms);
|
|
195
197
|
}
|
|
196
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Set a timeout for blocking commands.
|
|
201
|
+
* When the timeout expires, the command resolves with null (matching Redis behavior).
|
|
202
|
+
* This handles the case of undetectable network failures (e.g., docker network disconnect)
|
|
203
|
+
* where the TCP connection becomes a zombie and no close event fires.
|
|
204
|
+
*/
|
|
205
|
+
setBlockingTimeout(ms) {
|
|
206
|
+
if (ms <= 0) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// Clear existing timer if any (can happen when command moves from offline to command queue)
|
|
210
|
+
if (this._blockingTimeoutTimer) {
|
|
211
|
+
clearTimeout(this._blockingTimeoutTimer);
|
|
212
|
+
this._blockingTimeoutTimer = undefined;
|
|
213
|
+
}
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
// First call: establish absolute deadline
|
|
216
|
+
if (this._blockingDeadline === undefined) {
|
|
217
|
+
this._blockingDeadline = now + ms;
|
|
218
|
+
}
|
|
219
|
+
// Check if we've already exceeded the deadline
|
|
220
|
+
const remaining = this._blockingDeadline - now;
|
|
221
|
+
if (remaining <= 0) {
|
|
222
|
+
// Resolve with null to indicate timeout (same as Redis behavior)
|
|
223
|
+
this.resolve(null);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
this._blockingTimeoutTimer = setTimeout(() => {
|
|
227
|
+
if (this.isResolved) {
|
|
228
|
+
this._blockingTimeoutTimer = undefined;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this._blockingTimeoutTimer = undefined;
|
|
232
|
+
// Timeout expired - resolve with null (same as Redis behavior when blocking command times out)
|
|
233
|
+
this.resolve(null);
|
|
234
|
+
}, remaining);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Extract the blocking timeout from the command arguments.
|
|
238
|
+
*
|
|
239
|
+
* @returns The timeout in seconds, null for indefinite blocking (timeout of 0),
|
|
240
|
+
* or undefined if this is not a blocking command
|
|
241
|
+
*/
|
|
242
|
+
extractBlockingTimeout() {
|
|
243
|
+
const args = this.args;
|
|
244
|
+
if (!args || args.length === 0) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
const name = this.name.toLowerCase();
|
|
248
|
+
if (Command.checkFlag("LAST_ARG_TIMEOUT_COMMANDS", name)) {
|
|
249
|
+
return (0, argumentParsers_1.parseSecondsArgument)(args[args.length - 1]);
|
|
250
|
+
}
|
|
251
|
+
if (Command.checkFlag("FIRST_ARG_TIMEOUT_COMMANDS", name)) {
|
|
252
|
+
return (0, argumentParsers_1.parseSecondsArgument)(args[0]);
|
|
253
|
+
}
|
|
254
|
+
if (Command.checkFlag("BLOCK_OPTION_COMMANDS", name)) {
|
|
255
|
+
return (0, argumentParsers_1.parseBlockOption)(args);
|
|
256
|
+
}
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Clear the command and blocking timers
|
|
261
|
+
*/
|
|
262
|
+
_clearTimers() {
|
|
263
|
+
const existingTimer = this._commandTimeoutTimer;
|
|
264
|
+
if (existingTimer) {
|
|
265
|
+
clearTimeout(existingTimer);
|
|
266
|
+
delete this._commandTimeoutTimer;
|
|
267
|
+
}
|
|
268
|
+
const blockingTimer = this._blockingTimeoutTimer;
|
|
269
|
+
if (blockingTimer) {
|
|
270
|
+
clearTimeout(blockingTimer);
|
|
271
|
+
delete this._blockingTimeoutTimer;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
197
274
|
initPromise() {
|
|
198
275
|
const promise = new Promise((resolve, reject) => {
|
|
199
276
|
if (!this.transformed) {
|
|
@@ -205,14 +282,15 @@ class Command {
|
|
|
205
282
|
this.stringifyArguments();
|
|
206
283
|
}
|
|
207
284
|
this.resolve = this._convertValue(resolve);
|
|
208
|
-
|
|
209
|
-
this.
|
|
285
|
+
this.reject = (err) => {
|
|
286
|
+
this._clearTimers();
|
|
287
|
+
if (this.errorStack) {
|
|
210
288
|
reject((0, utils_1.optimizeErrorStack)(err, this.errorStack.stack, __dirname));
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
reject(err);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
216
294
|
});
|
|
217
295
|
this.promise = (0, standard_as_callback_1.default)(promise, this.callback);
|
|
218
296
|
}
|
|
@@ -222,9 +300,11 @@ class Command {
|
|
|
222
300
|
_iterateKeys(transform = (key) => key) {
|
|
223
301
|
if (typeof this.keys === "undefined") {
|
|
224
302
|
this.keys = [];
|
|
225
|
-
if ((0, commands_1.exists)(this.name)) {
|
|
303
|
+
if ((0, commands_1.exists)(this.name, { caseInsensitive: true })) {
|
|
226
304
|
// @ts-expect-error
|
|
227
|
-
const keyIndexes = (0, commands_1.getKeyIndexes)(this.name, this.args
|
|
305
|
+
const keyIndexes = (0, commands_1.getKeyIndexes)(this.name, this.args, {
|
|
306
|
+
nameCaseInsensitive: true,
|
|
307
|
+
});
|
|
228
308
|
for (const index of keyIndexes) {
|
|
229
309
|
this.args[index] = transform(this.args[index]);
|
|
230
310
|
this.keys.push(this.args[index]);
|
|
@@ -239,11 +319,7 @@ class Command {
|
|
|
239
319
|
_convertValue(resolve) {
|
|
240
320
|
return (value) => {
|
|
241
321
|
try {
|
|
242
|
-
|
|
243
|
-
if (existingTimer) {
|
|
244
|
-
clearTimeout(existingTimer);
|
|
245
|
-
delete this._commandTimeoutTimer;
|
|
246
|
-
}
|
|
322
|
+
this._clearTimers();
|
|
247
323
|
resolve(this.transformReply(value));
|
|
248
324
|
this.isResolved = true;
|
|
249
325
|
}
|
|
@@ -270,6 +346,30 @@ Command.FLAGS = {
|
|
|
270
346
|
ENTER_SUBSCRIBER_MODE: ["subscribe", "psubscribe", "ssubscribe"],
|
|
271
347
|
EXIT_SUBSCRIBER_MODE: ["unsubscribe", "punsubscribe", "sunsubscribe"],
|
|
272
348
|
WILL_DISCONNECT: ["quit"],
|
|
349
|
+
HANDSHAKE_COMMANDS: ["auth", "select", "client", "readonly", "info"],
|
|
350
|
+
IGNORE_RECONNECT_ON_ERROR: ["client"],
|
|
351
|
+
BLOCKING_COMMANDS: [
|
|
352
|
+
"blpop",
|
|
353
|
+
"brpop",
|
|
354
|
+
"brpoplpush",
|
|
355
|
+
"blmove",
|
|
356
|
+
"bzpopmin",
|
|
357
|
+
"bzpopmax",
|
|
358
|
+
"bzmpop",
|
|
359
|
+
"blmpop",
|
|
360
|
+
"xread",
|
|
361
|
+
"xreadgroup",
|
|
362
|
+
],
|
|
363
|
+
LAST_ARG_TIMEOUT_COMMANDS: [
|
|
364
|
+
"blpop",
|
|
365
|
+
"brpop",
|
|
366
|
+
"brpoplpush",
|
|
367
|
+
"blmove",
|
|
368
|
+
"bzpopmin",
|
|
369
|
+
"bzpopmax",
|
|
370
|
+
],
|
|
371
|
+
FIRST_ARG_TIMEOUT_COMMANDS: ["bzmpop", "blmpop"],
|
|
372
|
+
BLOCK_OPTION_COMMANDS: ["xread", "xreadgroup"],
|
|
273
373
|
};
|
|
274
374
|
Command._transformer = {
|
|
275
375
|
argument: {},
|
package/built/Pipeline.js
CHANGED
|
@@ -101,7 +101,8 @@ class Pipeline extends Commander_1.default {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
else if (!command.inTransaction) {
|
|
104
|
-
const isReadOnly = (0, commands_1.exists)(command.name
|
|
104
|
+
const isReadOnly = (0, commands_1.exists)(command.name, { caseInsensitive: true }) &&
|
|
105
|
+
(0, commands_1.hasFlag)(command.name, "readonly", { nameCaseInsensitive: true });
|
|
105
106
|
if (!isReadOnly) {
|
|
106
107
|
retriable = false;
|
|
107
108
|
break;
|
package/built/Redis.d.ts
CHANGED
|
@@ -161,6 +161,8 @@ declare class Redis extends Commander implements DataHandledable {
|
|
|
161
161
|
* @ignore
|
|
162
162
|
*/
|
|
163
163
|
sendCommand(command: Command, stream?: WriteableStream): unknown;
|
|
164
|
+
private getBlockingTimeoutInMs;
|
|
165
|
+
private getConfiguredBlockingTimeout;
|
|
164
166
|
private setSocketTimeout;
|
|
165
167
|
scanStream(options?: ScanStreamOptions): ScanStream;
|
|
166
168
|
scanBufferStream(options?: ScanStreamOptions): ScanStream;
|
package/built/Redis.js
CHANGED
|
@@ -325,7 +325,7 @@ class Redis extends Commander_1.default {
|
|
|
325
325
|
* @ignore
|
|
326
326
|
*/
|
|
327
327
|
sendCommand(command, stream) {
|
|
328
|
-
var _a, _b;
|
|
328
|
+
var _a, _b, _c;
|
|
329
329
|
if (this.status === "wait") {
|
|
330
330
|
this.connect().catch(lodash_1.noop);
|
|
331
331
|
}
|
|
@@ -341,11 +341,13 @@ class Redis extends Commander_1.default {
|
|
|
341
341
|
if (typeof this.options.commandTimeout === "number") {
|
|
342
342
|
command.setTimeout(this.options.commandTimeout);
|
|
343
343
|
}
|
|
344
|
+
const blockingTimeout = this.getBlockingTimeoutInMs(command);
|
|
344
345
|
let writable = this.status === "ready" ||
|
|
345
346
|
(!stream &&
|
|
346
347
|
this.status === "connect" &&
|
|
347
|
-
(0, commands_1.exists)(command.name) &&
|
|
348
|
-
(0, commands_1.hasFlag)(command.name, "loading")
|
|
348
|
+
(0, commands_1.exists)(command.name, { caseInsensitive: true }) &&
|
|
349
|
+
((0, commands_1.hasFlag)(command.name, "loading", { nameCaseInsensitive: true }) ||
|
|
350
|
+
Command_1.default.checkFlag("HANDSHAKE_COMMANDS", command.name)));
|
|
349
351
|
if (!this.stream) {
|
|
350
352
|
writable = false;
|
|
351
353
|
}
|
|
@@ -377,11 +379,20 @@ class Redis extends Commander_1.default {
|
|
|
377
379
|
stream: stream,
|
|
378
380
|
select: this.condition.select,
|
|
379
381
|
});
|
|
382
|
+
// For blocking commands, set a timeout while queued to ensure they don't wait forever
|
|
383
|
+
// if connection never becomes ready (e.g., docker network disconnect scenario)
|
|
384
|
+
// Use blockingTimeout if configured, otherwise fall back to the command's own timeout
|
|
385
|
+
if (Command_1.default.checkFlag("BLOCKING_COMMANDS", command.name)) {
|
|
386
|
+
const offlineTimeout = (_b = this.getConfiguredBlockingTimeout()) !== null && _b !== void 0 ? _b : blockingTimeout;
|
|
387
|
+
if (offlineTimeout !== undefined) {
|
|
388
|
+
command.setBlockingTimeout(offlineTimeout);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
380
391
|
}
|
|
381
392
|
else {
|
|
382
393
|
// @ts-expect-error
|
|
383
394
|
if (debug.enabled) {
|
|
384
|
-
debug("write command[%s]: %d -> %s(%o)", this._getDescription(), (
|
|
395
|
+
debug("write command[%s]: %d -> %s(%o)", this._getDescription(), (_c = this.condition) === null || _c === void 0 ? void 0 : _c.select, command.name, command.args);
|
|
385
396
|
}
|
|
386
397
|
if (stream) {
|
|
387
398
|
if ("isPipeline" in stream && stream.isPipeline) {
|
|
@@ -399,6 +410,9 @@ class Redis extends Commander_1.default {
|
|
|
399
410
|
stream: stream,
|
|
400
411
|
select: this.condition.select,
|
|
401
412
|
});
|
|
413
|
+
if (blockingTimeout !== undefined) {
|
|
414
|
+
command.setBlockingTimeout(blockingTimeout);
|
|
415
|
+
}
|
|
402
416
|
if (Command_1.default.checkFlag("WILL_DISCONNECT", command.name)) {
|
|
403
417
|
this.manuallyClosing = true;
|
|
404
418
|
}
|
|
@@ -416,6 +430,33 @@ class Redis extends Commander_1.default {
|
|
|
416
430
|
}
|
|
417
431
|
return command.promise;
|
|
418
432
|
}
|
|
433
|
+
getBlockingTimeoutInMs(command) {
|
|
434
|
+
var _a;
|
|
435
|
+
if (!Command_1.default.checkFlag("BLOCKING_COMMANDS", command.name)) {
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
const timeout = command.extractBlockingTimeout();
|
|
439
|
+
if (typeof timeout === "number") {
|
|
440
|
+
if (timeout > 0) {
|
|
441
|
+
// Finite timeout from command args - add grace period
|
|
442
|
+
return timeout + ((_a = this.options.blockingTimeoutGrace) !== null && _a !== void 0 ? _a : RedisOptions_1.DEFAULT_REDIS_OPTIONS.blockingTimeoutGrace);
|
|
443
|
+
}
|
|
444
|
+
// Command has timeout=0 (block forever), use blockingTimeout option as safety net
|
|
445
|
+
return this.getConfiguredBlockingTimeout();
|
|
446
|
+
}
|
|
447
|
+
if (timeout === null) {
|
|
448
|
+
// No BLOCK option found (e.g., XREAD without BLOCK), use blockingTimeout as safety net
|
|
449
|
+
return this.getConfiguredBlockingTimeout();
|
|
450
|
+
}
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
getConfiguredBlockingTimeout() {
|
|
454
|
+
if (typeof this.options.blockingTimeout === "number" &&
|
|
455
|
+
this.options.blockingTimeout > 0) {
|
|
456
|
+
return this.options.blockingTimeout;
|
|
457
|
+
}
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
419
460
|
setSocketTimeout() {
|
|
420
461
|
this.socketTimeoutTimer = setTimeout(() => {
|
|
421
462
|
this.stream.destroy(new Error(`Socket timeout. Expecting data, but didn't receive any in ${this.options.socketTimeout}ms.`));
|
|
@@ -501,7 +542,8 @@ class Redis extends Commander_1.default {
|
|
|
501
542
|
handleReconnection(err, item) {
|
|
502
543
|
var _a;
|
|
503
544
|
let needReconnect = false;
|
|
504
|
-
if (this.options.reconnectOnError
|
|
545
|
+
if (this.options.reconnectOnError &&
|
|
546
|
+
!Command_1.default.checkFlag("IGNORE_RECONNECT_ON_ERROR", item.command.name)) {
|
|
505
547
|
needReconnect = this.options.reconnectOnError(err);
|
|
506
548
|
}
|
|
507
549
|
switch (needReconnect) {
|
package/built/autoPipelining.js
CHANGED
|
@@ -1,37 +1,39 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import * as EventEmitter from "events";
|
|
3
|
+
import ShardedSubscriber from "./ShardedSubscriber";
|
|
4
4
|
/**
|
|
5
|
-
* Redis
|
|
6
|
-
*
|
|
7
|
-
* messages between shards.
|
|
8
|
-
*
|
|
9
|
-
* Given that, we need at least one ClusterSubscriber per master endpoint/node.
|
|
5
|
+
* Redis distinguishes between "normal" and sharded PubSub. When using the normal PubSub feature,
|
|
6
|
+
* exactly one subscriber exists per cluster instance because the Redis cluster bus forwards
|
|
7
|
+
* messages between shards. Sharded PubSub removes this limitation by making each shard
|
|
8
|
+
* responsible for its own messages.
|
|
10
9
|
*
|
|
11
|
-
* This class
|
|
12
|
-
*
|
|
13
|
-
* to support this feature.
|
|
10
|
+
* This class coordinates one ShardedSubscriber per master node in the cluster, providing
|
|
11
|
+
* sharded PubSub support while keeping the public API backward compatible.
|
|
14
12
|
*/
|
|
15
13
|
export default class ClusterSubscriberGroup {
|
|
16
|
-
private
|
|
14
|
+
private readonly subscriberGroupEmitter;
|
|
17
15
|
private shardedSubscribers;
|
|
18
16
|
private clusterSlots;
|
|
19
17
|
private subscriberToSlotsIndex;
|
|
20
18
|
private channels;
|
|
19
|
+
private failedAttemptsByNode;
|
|
20
|
+
private isResetting;
|
|
21
|
+
private pendingReset;
|
|
22
|
+
private static readonly MAX_RETRY_ATTEMPTS;
|
|
23
|
+
private static readonly MAX_BACKOFF_MS;
|
|
24
|
+
private static readonly BASE_BACKOFF_MS;
|
|
21
25
|
/**
|
|
22
26
|
* Register callbacks
|
|
23
27
|
*
|
|
24
28
|
* @param cluster
|
|
25
29
|
*/
|
|
26
|
-
constructor(
|
|
30
|
+
constructor(subscriberGroupEmitter: EventEmitter);
|
|
27
31
|
/**
|
|
28
32
|
* Get the responsible subscriber.
|
|
29
33
|
*
|
|
30
|
-
* Returns null if no subscriber was found
|
|
31
|
-
*
|
|
32
34
|
* @param slot
|
|
33
35
|
*/
|
|
34
|
-
getResponsibleSubscriber(slot: number):
|
|
36
|
+
getResponsibleSubscriber(slot: number): ShardedSubscriber | undefined;
|
|
35
37
|
/**
|
|
36
38
|
* Adds a channel for which this subscriber group is responsible
|
|
37
39
|
*
|
|
@@ -50,24 +52,17 @@ export default class ClusterSubscriberGroup {
|
|
|
50
52
|
/**
|
|
51
53
|
* Start all not yet started subscribers
|
|
52
54
|
*/
|
|
53
|
-
start():
|
|
54
|
-
/**
|
|
55
|
-
* Add a subscriber to the group of subscribers
|
|
56
|
-
*
|
|
57
|
-
* @param redis
|
|
58
|
-
*/
|
|
59
|
-
private _addSubscriber;
|
|
55
|
+
start(): Promise<any[]>;
|
|
60
56
|
/**
|
|
61
|
-
*
|
|
62
|
-
* @param redis
|
|
57
|
+
* Resets the subscriber group by disconnecting all subscribers that are no longer needed and connecting new ones.
|
|
63
58
|
*/
|
|
64
|
-
|
|
59
|
+
reset(clusterSlots: string[][], clusterNodes: any[]): Promise<void>;
|
|
65
60
|
/**
|
|
66
61
|
* Refreshes the subscriber-related slot ranges
|
|
67
62
|
*
|
|
68
63
|
* Returns false if no refresh was needed
|
|
69
64
|
*
|
|
70
|
-
* @param
|
|
65
|
+
* @param targetSlots
|
|
71
66
|
*/
|
|
72
67
|
private _refreshSlots;
|
|
73
68
|
/**
|
|
@@ -83,4 +78,28 @@ export default class ClusterSubscriberGroup {
|
|
|
83
78
|
* @private
|
|
84
79
|
*/
|
|
85
80
|
private _slotsAreEqual;
|
|
81
|
+
/**
|
|
82
|
+
* Checks if any subscribers are in an unhealthy state.
|
|
83
|
+
*
|
|
84
|
+
* A subscriber is considered unhealthy if:
|
|
85
|
+
* - It exists but is not started (failed/disconnected)
|
|
86
|
+
* - It's missing entirely for a node that should have one
|
|
87
|
+
*
|
|
88
|
+
* @returns true if any subscribers need to be recreated
|
|
89
|
+
*/
|
|
90
|
+
private hasUnhealthySubscribers;
|
|
91
|
+
/**
|
|
92
|
+
* Handles failed subscriber connections by emitting an event to refresh the slots cache
|
|
93
|
+
* after a backoff period.
|
|
94
|
+
*
|
|
95
|
+
* @param error
|
|
96
|
+
* @param nodeKey
|
|
97
|
+
*/
|
|
98
|
+
private handleSubscriberConnectFailed;
|
|
99
|
+
/**
|
|
100
|
+
* Handles successful subscriber connections by resetting the failed attempts counter.
|
|
101
|
+
*
|
|
102
|
+
* @param nodeKey
|
|
103
|
+
*/
|
|
104
|
+
private handleSubscriberConnectSucceeded;
|
|
86
105
|
}
|