ioredis 5.10.0 → 5.11.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 +61 -19
- package/built/Command.d.ts +1 -0
- package/built/Command.js +1 -0
- package/built/Pipeline.js +10 -0
- package/built/Redis.d.ts +11 -0
- package/built/Redis.js +60 -5
- package/built/autoPipelining.js +13 -3
- package/built/cluster/ClusterSubscriberGroup.d.ts +4 -1
- package/built/cluster/ClusterSubscriberGroup.js +71 -19
- package/built/cluster/ShardedSubscriber.d.ts +21 -5
- package/built/cluster/ShardedSubscriber.js +81 -23
- package/built/cluster/index.js +1 -1
- package/built/index.d.ts +1 -0
- package/built/tracing.d.ts +26 -0
- package/built/tracing.js +96 -0
- package/built/transaction.js +10 -3
- package/built/utils/RedisCommander.d.ts +779 -1
- package/built/utils/defaults.d.ts +6 -0
- package/built/utils/defaults.js +102 -0
- package/built/utils/index.d.ts +2 -2
- package/built/utils/index.js +57 -21
- package/built/utils/isArguments.d.ts +1 -0
- package/built/utils/isArguments.js +48 -0
- package/built/utils/lodash.d.ts +2 -3
- package/built/utils/lodash.js +13 -5
- package/package.json +9 -13
package/README.md
CHANGED
|
@@ -729,13 +729,16 @@ const stream = redis.zscanStream("myhash", {
|
|
|
729
729
|
match: "age:??",
|
|
730
730
|
});
|
|
731
731
|
```
|
|
732
|
+
|
|
732
733
|
The `hscanStream` also accepts the `noValues` option to specify whether Redis should return only the keys in the hash table without their corresponding values.
|
|
734
|
+
|
|
733
735
|
```javascript
|
|
734
736
|
const stream = redis.hscanStream("myhash", {
|
|
735
737
|
match: "age:??",
|
|
736
738
|
noValues: true,
|
|
737
739
|
});
|
|
738
740
|
```
|
|
741
|
+
|
|
739
742
|
You can learn more from the [Redis documentation](http://redis.io/commands/scan).
|
|
740
743
|
|
|
741
744
|
**Useful Tips**
|
|
@@ -815,6 +818,7 @@ const redis = new Redis({
|
|
|
815
818
|
```
|
|
816
819
|
|
|
817
820
|
When enabled:
|
|
821
|
+
|
|
818
822
|
- For commands with a finite timeout (e.g., `blpop("key", 5)`), ioredis sets a client-side deadline based on the command's timeout plus a small grace period (`blockingTimeoutGrace`, default 100ms). If no reply arrives before the deadline, the command resolves with `null`—the same value Redis returns when a blocking command times out normally.
|
|
819
823
|
- For commands that block forever (e.g., `timeout = 0` or `BLOCK 0`), the `blockingTimeout` value is used as a safety net.
|
|
820
824
|
|
|
@@ -842,15 +846,15 @@ On ElastiCache instances with Auto-failover enabled, `reconnectOnError` does not
|
|
|
842
846
|
|
|
843
847
|
The Redis instance will emit some events about the state of the connection to the Redis server.
|
|
844
848
|
|
|
845
|
-
| Event | Description
|
|
846
|
-
| :----------- |
|
|
847
|
-
| connect | emits when a connection is established to the Redis server.
|
|
848
|
-
| ready | If `enableReadyCheck` is `true`, client will emit `ready` when the server reports that it is ready to receive commands (e.g. finish loading data from disk).<br>Otherwise, `ready` will be emitted immediately right after the `connect` event.
|
|
849
|
-
| error | emits when an error occurs while connecting.<br>However, ioredis emits all `error` events silently (only emits when there's at least one listener) so that your application won't crash if you're not listening to the `error` event.<br>When `redis.connect()` is explicitly called the error will also be rejected from the returned promise, in addition to emitting it. If `redis.connect()` is not called explicitly and `lazyConnect` is true, ioredis will try to connect automatically on the first command and emit the `error` event silently.
|
|
850
|
-
| close | emits when an established Redis server connection has closed.
|
|
851
|
-
| reconnecting | emits after `close` when a reconnection will be made. The argument of the event is the time (in ms) before reconnecting.
|
|
852
|
-
| end | emits after `close` when no more reconnections will be made, or the connection is failed to establish.
|
|
853
|
-
| wait | emits when `lazyConnect` is set and will wait for the first command to be called before connecting.
|
|
849
|
+
| Event | Description |
|
|
850
|
+
| :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
851
|
+
| connect | emits when a connection is established to the Redis server. |
|
|
852
|
+
| ready | If `enableReadyCheck` is `true`, client will emit `ready` when the server reports that it is ready to receive commands (e.g. finish loading data from disk).<br>Otherwise, `ready` will be emitted immediately right after the `connect` event. |
|
|
853
|
+
| error | emits when an error occurs while connecting.<br>However, ioredis emits all `error` events silently (only emits when there's at least one listener) so that your application won't crash if you're not listening to the `error` event.<br>When `redis.connect()` is explicitly called the error will also be rejected from the returned promise, in addition to emitting it. If `redis.connect()` is not called explicitly and `lazyConnect` is true, ioredis will try to connect automatically on the first command and emit the `error` event silently. |
|
|
854
|
+
| close | emits when an established Redis server connection has closed. |
|
|
855
|
+
| reconnecting | emits after `close` when a reconnection will be made. The argument of the event is the time (in ms) before reconnecting. |
|
|
856
|
+
| end | emits after `close` when no more reconnections will be made, or the connection is failed to establish. |
|
|
857
|
+
| wait | emits when `lazyConnect` is set and will wait for the first command to be called before connecting. |
|
|
854
858
|
|
|
855
859
|
You can also check out the `Redis#status` property to get the current connection status.
|
|
856
860
|
|
|
@@ -860,6 +864,38 @@ Besides the above connection events, there are several other custom events:
|
|
|
860
864
|
| :----- | :------------------------------------------------------------------ |
|
|
861
865
|
| select | emits when the database changed. The argument is the new db number. |
|
|
862
866
|
|
|
867
|
+
## Diagnostics Channel
|
|
868
|
+
|
|
869
|
+
ioredis publishes telemetry through Node.js [`diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html), allowing APM tools and custom instrumentation to observe commands, connections, and batch operations without modifying application code.
|
|
870
|
+
|
|
871
|
+
These channels use `TracingChannel#tracePromise()` and emit `start`, `end`, `asyncStart`, `asyncEnd`, and `error` sub-events. Requires Node.js >= 18.19.0; on older versions the channels are silently unavailable (zero overhead). Subscribe via `tracing:<name>:<event>`:
|
|
872
|
+
|
|
873
|
+
```typescript
|
|
874
|
+
import dc from "node:diagnostics_channel";
|
|
875
|
+
|
|
876
|
+
dc.subscribe("tracing:ioredis:command:start", ({ command, args }) => {
|
|
877
|
+
console.log(`> ${command}`, args);
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
dc.subscribe("tracing:ioredis:command:asyncEnd", ({ command }) => {
|
|
881
|
+
console.log(`${command} settled`);
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
dc.subscribe("tracing:ioredis:command:error", ({ command, error }) => {
|
|
885
|
+
console.error(`${command} failed:`, error);
|
|
886
|
+
});
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
| Channel name | Payload | Description |
|
|
890
|
+
| :---------------- | :---------------------- | :------------------------------------------------------- |
|
|
891
|
+
| `ioredis:command` | `CommandTraceContext` | Individual command (standalone, pipeline, or within MULTI) |
|
|
892
|
+
| `ioredis:batch` | `BatchOperationContext` | MULTI transaction as a whole |
|
|
893
|
+
| `ioredis:connect` | `ConnectTraceContext` | Socket connection attempt |
|
|
894
|
+
|
|
895
|
+
Command arguments are sanitized before emission using rules adapted from `@opentelemetry/redis-common`. Sensitive values (e.g. values in `SET`, `AUTH` passwords) are replaced with `?`, while read-only commands like `GET` and `DEL` retain all arguments. This is safe by default: unlisted or custom commands have all arguments redacted.
|
|
896
|
+
|
|
897
|
+
Context types (`CommandTraceContext`, `BatchOperationContext`, `ConnectTraceContext`) are exported from `ioredis`.
|
|
898
|
+
|
|
863
899
|
## Offline Queue
|
|
864
900
|
|
|
865
901
|
When a command can't be processed by Redis (being sent before the `ready` event), by default, it's added to the offline queue and will be
|
|
@@ -1159,6 +1195,7 @@ const cluster = new Redis.Cluster(
|
|
|
1159
1195
|
```
|
|
1160
1196
|
|
|
1161
1197
|
Or you can specify this parameter through function:
|
|
1198
|
+
|
|
1162
1199
|
```javascript
|
|
1163
1200
|
const cluster = new Redis.Cluster(
|
|
1164
1201
|
[
|
|
@@ -1169,7 +1206,7 @@ const cluster = new Redis.Cluster(
|
|
|
1169
1206
|
],
|
|
1170
1207
|
{
|
|
1171
1208
|
natMap: (key) => {
|
|
1172
|
-
if(key.includes(
|
|
1209
|
+
if (key.includes("30001")) {
|
|
1173
1210
|
return { host: "203.0.113.73", port: 30001 };
|
|
1174
1211
|
}
|
|
1175
1212
|
|
|
@@ -1226,28 +1263,33 @@ For sharded Pub/Sub, use the `spublish` and `ssubscribe` commands instead of the
|
|
|
1226
1263
|
The following basic example shows you how to use sharded Pub/Sub:
|
|
1227
1264
|
|
|
1228
1265
|
```javascript
|
|
1229
|
-
const cluster: Cluster = new Cluster([{host: host, port: port}], {
|
|
1266
|
+
const cluster: Cluster = new Cluster([{ host: host, port: port }], {
|
|
1267
|
+
shardedSubscribers: true,
|
|
1268
|
+
});
|
|
1230
1269
|
|
|
1231
1270
|
//Register the callback
|
|
1232
1271
|
cluster.on("smessage", (channel, message) => {
|
|
1233
|
-
|
|
1272
|
+
console.log(message);
|
|
1234
1273
|
});
|
|
1235
1274
|
|
|
1236
|
-
|
|
1237
1275
|
//Subscribe to the channels on the same slot
|
|
1238
|
-
cluster
|
|
1276
|
+
cluster
|
|
1277
|
+
.ssubscribe("channel{my}:1", "channel{my}:2")
|
|
1278
|
+
.then((count: number) => {
|
|
1239
1279
|
console.log(count);
|
|
1240
|
-
})
|
|
1280
|
+
})
|
|
1281
|
+
.catch((err) => {
|
|
1241
1282
|
console.log(err);
|
|
1242
|
-
});
|
|
1283
|
+
});
|
|
1243
1284
|
|
|
1244
1285
|
//Publish a message
|
|
1245
|
-
cluster
|
|
1286
|
+
cluster
|
|
1287
|
+
.spublish("channel{my}:1", "This is a test message to my first channel.")
|
|
1288
|
+
.then((value: number) => {
|
|
1246
1289
|
console.log("Published a message to channel{my}:1");
|
|
1247
|
-
});
|
|
1290
|
+
});
|
|
1248
1291
|
```
|
|
1249
1292
|
|
|
1250
|
-
|
|
1251
1293
|
### Events
|
|
1252
1294
|
|
|
1253
1295
|
| Event | Description |
|
package/built/Command.d.ts
CHANGED
package/built/Command.js
CHANGED
package/built/Pipeline.js
CHANGED
|
@@ -5,6 +5,7 @@ const commands_1 = require("@ioredis/commands");
|
|
|
5
5
|
const standard_as_callback_1 = require("standard-as-callback");
|
|
6
6
|
const util_1 = require("util");
|
|
7
7
|
const Command_1 = require("./Command");
|
|
8
|
+
const buffer_1 = require("buffer");
|
|
8
9
|
const utils_1 = require("./utils");
|
|
9
10
|
const Commander_1 = require("./utils/Commander");
|
|
10
11
|
/*
|
|
@@ -315,6 +316,15 @@ Pipeline.prototype.exec = function (callback) {
|
|
|
315
316
|
buffers.push(writable);
|
|
316
317
|
}
|
|
317
318
|
else {
|
|
319
|
+
if (data.length + writable.length >= buffer_1.constants.MAX_STRING_LENGTH) {
|
|
320
|
+
if (!buffers) {
|
|
321
|
+
buffers = [];
|
|
322
|
+
}
|
|
323
|
+
if (data) {
|
|
324
|
+
buffers.push(Buffer.from(data, "utf8"));
|
|
325
|
+
data = "";
|
|
326
|
+
}
|
|
327
|
+
}
|
|
318
328
|
data += writable;
|
|
319
329
|
}
|
|
320
330
|
if (!--writePending) {
|
package/built/Redis.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { RedisOptions } from "./redis/RedisOptions";
|
|
|
7
7
|
import ScanStream from "./ScanStream";
|
|
8
8
|
import { Transaction } from "./transaction";
|
|
9
9
|
import { Callback, CommandItem, NetStream, ScanStreamOptions, WriteableStream } from "./types";
|
|
10
|
+
import { type BatchOperationContext } from "./tracing";
|
|
10
11
|
import Commander from "./utils/Commander";
|
|
11
12
|
import Deque = require("denque");
|
|
12
13
|
declare type RedisStatus = "wait" | "reconnecting" | "connecting" | "connect" | "ready" | "close" | "end";
|
|
@@ -85,6 +86,7 @@ declare class Redis extends Commander implements DataHandledable {
|
|
|
85
86
|
* if the connection fails, times out, or if Redis is already connecting/connected.
|
|
86
87
|
*/
|
|
87
88
|
connect(callback?: Callback<void>): Promise<void>;
|
|
89
|
+
private _connect;
|
|
88
90
|
/**
|
|
89
91
|
* Disconnect from Redis.
|
|
90
92
|
*
|
|
@@ -186,6 +188,15 @@ declare class Redis extends Commander implements DataHandledable {
|
|
|
186
188
|
* @ignore
|
|
187
189
|
*/
|
|
188
190
|
handleReconnection(err: Error, item: CommandItem): void;
|
|
191
|
+
/**
|
|
192
|
+
* @ignore
|
|
193
|
+
*/
|
|
194
|
+
_getServerAddress(): {
|
|
195
|
+
address: string;
|
|
196
|
+
port: number | undefined;
|
|
197
|
+
};
|
|
198
|
+
private _buildCommandContext;
|
|
199
|
+
_buildBatchContext(batchSize: number): BatchOperationContext;
|
|
189
200
|
/**
|
|
190
201
|
* Get description of the connection. Used for debugging.
|
|
191
202
|
*/
|
package/built/Redis.js
CHANGED
|
@@ -12,6 +12,7 @@ const RedisOptions_1 = require("./redis/RedisOptions");
|
|
|
12
12
|
const ScanStream_1 = require("./ScanStream");
|
|
13
13
|
const transaction_1 = require("./transaction");
|
|
14
14
|
const utils_1 = require("./utils");
|
|
15
|
+
const tracing_1 = require("./tracing");
|
|
15
16
|
const applyMixin_1 = require("./utils/applyMixin");
|
|
16
17
|
const Commander_1 = require("./utils/Commander");
|
|
17
18
|
const lodash_1 = require("./utils/lodash");
|
|
@@ -101,7 +102,18 @@ class Redis extends Commander_1.default {
|
|
|
101
102
|
* if the connection fails, times out, or if Redis is already connecting/connected.
|
|
102
103
|
*/
|
|
103
104
|
connect(callback) {
|
|
104
|
-
const promise =
|
|
105
|
+
const promise = (0, tracing_1.traceConnect)(() => this._connect(), () => {
|
|
106
|
+
const { address, port } = this._getServerAddress();
|
|
107
|
+
return {
|
|
108
|
+
serverAddress: address,
|
|
109
|
+
serverPort: port,
|
|
110
|
+
connectionEpoch: this.connectionEpoch,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
return (0, standard_as_callback_1.default)(promise, callback);
|
|
114
|
+
}
|
|
115
|
+
_connect() {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
105
117
|
if (this.status === "connecting" ||
|
|
106
118
|
this.status === "connect" ||
|
|
107
119
|
this.status === "ready") {
|
|
@@ -211,7 +223,6 @@ class Redis extends Commander_1.default {
|
|
|
211
223
|
_this.once("close", connectionCloseHandler);
|
|
212
224
|
});
|
|
213
225
|
});
|
|
214
|
-
return (0, standard_as_callback_1.default)(promise, callback);
|
|
215
226
|
}
|
|
216
227
|
/**
|
|
217
228
|
* Disconnect from Redis.
|
|
@@ -416,7 +427,8 @@ class Redis extends Commander_1.default {
|
|
|
416
427
|
if (Command_1.default.checkFlag("WILL_DISCONNECT", command.name)) {
|
|
417
428
|
this.manuallyClosing = true;
|
|
418
429
|
}
|
|
419
|
-
if (this.options.socketTimeout !== undefined &&
|
|
430
|
+
if (this.options.socketTimeout !== undefined &&
|
|
431
|
+
this.socketTimeoutTimer === undefined) {
|
|
420
432
|
this.setSocketTimeout();
|
|
421
433
|
}
|
|
422
434
|
}
|
|
@@ -428,7 +440,15 @@ class Redis extends Commander_1.default {
|
|
|
428
440
|
debug("switch to db [%d]", this.condition.select);
|
|
429
441
|
}
|
|
430
442
|
}
|
|
431
|
-
|
|
443
|
+
if (!writable || command.isTraced) {
|
|
444
|
+
return command.promise;
|
|
445
|
+
}
|
|
446
|
+
// Trace on the write path only, and only once per command. Commands may
|
|
447
|
+
// pass through sendCommand multiple times (offline queue flush,
|
|
448
|
+
// prevCommandQueue resend after reconnect). The isTraced flag ensures
|
|
449
|
+
// we don't emit duplicate trace events.
|
|
450
|
+
command.isTraced = true;
|
|
451
|
+
return (0, tracing_1.traceCommand)(() => command.promise, () => this._buildCommandContext(command));
|
|
432
452
|
}
|
|
433
453
|
getBlockingTimeoutInMs(command) {
|
|
434
454
|
var _a;
|
|
@@ -444,7 +464,8 @@ class Redis extends Commander_1.default {
|
|
|
444
464
|
if (typeof timeout === "number") {
|
|
445
465
|
if (timeout > 0) {
|
|
446
466
|
// Finite timeout from command args - add grace period
|
|
447
|
-
return timeout +
|
|
467
|
+
return (timeout +
|
|
468
|
+
((_a = this.options.blockingTimeoutGrace) !== null && _a !== void 0 ? _a : RedisOptions_1.DEFAULT_REDIS_OPTIONS.blockingTimeoutGrace));
|
|
448
469
|
}
|
|
449
470
|
// Command has timeout=0 (block forever), use blockingTimeout option as safety net
|
|
450
471
|
return configuredTimeout;
|
|
@@ -575,6 +596,40 @@ class Redis extends Commander_1.default {
|
|
|
575
596
|
item.command.reject(err);
|
|
576
597
|
}
|
|
577
598
|
}
|
|
599
|
+
/**
|
|
600
|
+
* @ignore
|
|
601
|
+
*/
|
|
602
|
+
_getServerAddress() {
|
|
603
|
+
if ("path" in this.options && this.options.path) {
|
|
604
|
+
return { address: this.options.path, port: undefined };
|
|
605
|
+
}
|
|
606
|
+
return {
|
|
607
|
+
address: ("host" in this.options && this.options.host) || "localhost",
|
|
608
|
+
port: ("port" in this.options && this.options.port) || 6379,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
_buildCommandContext(command) {
|
|
612
|
+
var _a, _b, _c;
|
|
613
|
+
const { address, port } = this._getServerAddress();
|
|
614
|
+
return {
|
|
615
|
+
command: command.name,
|
|
616
|
+
args: (0, tracing_1.sanitizeArgs)(command.name, command.args),
|
|
617
|
+
database: (_c = (_b = (_a = this.condition) === null || _a === void 0 ? void 0 : _a.select) !== null && _b !== void 0 ? _b : this.options.db) !== null && _c !== void 0 ? _c : 0,
|
|
618
|
+
serverAddress: address,
|
|
619
|
+
serverPort: port,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
_buildBatchContext(batchSize) {
|
|
623
|
+
var _a, _b, _c;
|
|
624
|
+
const { address, port } = this._getServerAddress();
|
|
625
|
+
return {
|
|
626
|
+
batchMode: "MULTI",
|
|
627
|
+
batchSize,
|
|
628
|
+
database: (_c = (_b = (_a = this.condition) === null || _a === void 0 ? void 0 : _a.select) !== null && _b !== void 0 ? _b : this.options.db) !== null && _c !== void 0 ? _c : 0,
|
|
629
|
+
serverAddress: address,
|
|
630
|
+
serverPort: port,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
578
633
|
/**
|
|
579
634
|
* Get description of the connection. Used for debugging.
|
|
580
635
|
*/
|
package/built/autoPipelining.js
CHANGED
|
@@ -100,6 +100,18 @@ function getFirstValueInFlattenedArray(args) {
|
|
|
100
100
|
return undefined;
|
|
101
101
|
}
|
|
102
102
|
exports.getFirstValueInFlattenedArray = getFirstValueInFlattenedArray;
|
|
103
|
+
function getFirstKeyForCommand(commandName, args) {
|
|
104
|
+
if ((0, commands_1.exists)(commandName, { caseInsensitive: true })) {
|
|
105
|
+
const flattenedArgs = args.flat();
|
|
106
|
+
const keyIndexes = (0, commands_1.getKeyIndexes)(commandName, flattenedArgs, {
|
|
107
|
+
nameCaseInsensitive: true,
|
|
108
|
+
});
|
|
109
|
+
if (keyIndexes.length) {
|
|
110
|
+
return flattenedArgs[keyIndexes[0]];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return getFirstValueInFlattenedArray(args);
|
|
114
|
+
}
|
|
103
115
|
function executeWithAutoPipelining(client, functionName, commandName, args, callback) {
|
|
104
116
|
// On cluster mode let's wait for slots to be available
|
|
105
117
|
if (client.isCluster && !client.slots.length) {
|
|
@@ -116,11 +128,9 @@ function executeWithAutoPipelining(client, functionName, commandName, args, call
|
|
|
116
128
|
}), callback);
|
|
117
129
|
}
|
|
118
130
|
// If we have slot information, we can improve routing by grouping slots served by the same subset of nodes
|
|
119
|
-
// Note that the first value in args may be a (possibly empty) array.
|
|
120
|
-
// ioredis will only flatten one level of the array, in the Command constructor.
|
|
121
131
|
const prefix = client.options.keyPrefix || "";
|
|
122
132
|
let slotKey = client.isCluster
|
|
123
|
-
? client.slots[calculateSlot(`${prefix}${
|
|
133
|
+
? client.slots[calculateSlot(`${prefix}${getFirstKeyForCommand(commandName, args)}`)].join(",")
|
|
124
134
|
: "main";
|
|
125
135
|
// When scaleReads is enabled, separate read and write commands into different pipelines
|
|
126
136
|
// so they can be routed to replicas and masters respectively
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import * as EventEmitter from "events";
|
|
3
3
|
import ShardedSubscriber from "./ShardedSubscriber";
|
|
4
|
+
import { ClusterOptions } from "./ClusterOptions";
|
|
4
5
|
/**
|
|
5
6
|
* Redis distinguishes between "normal" and sharded PubSub. When using the normal PubSub feature,
|
|
6
7
|
* exactly one subscriber exists per cluster instance because the Redis cluster bus forwards
|
|
@@ -12,6 +13,7 @@ import ShardedSubscriber from "./ShardedSubscriber";
|
|
|
12
13
|
*/
|
|
13
14
|
export default class ClusterSubscriberGroup {
|
|
14
15
|
private readonly subscriberGroupEmitter;
|
|
16
|
+
private readonly options;
|
|
15
17
|
private shardedSubscribers;
|
|
16
18
|
private clusterSlots;
|
|
17
19
|
private subscriberToSlotsIndex;
|
|
@@ -27,7 +29,7 @@ export default class ClusterSubscriberGroup {
|
|
|
27
29
|
*
|
|
28
30
|
* @param cluster
|
|
29
31
|
*/
|
|
30
|
-
constructor(subscriberGroupEmitter: EventEmitter);
|
|
32
|
+
constructor(subscriberGroupEmitter: EventEmitter, options: ClusterOptions);
|
|
31
33
|
/**
|
|
32
34
|
* Get the responsible subscriber.
|
|
33
35
|
*
|
|
@@ -102,4 +104,5 @@ export default class ClusterSubscriberGroup {
|
|
|
102
104
|
* @param nodeKey
|
|
103
105
|
*/
|
|
104
106
|
private handleSubscriberConnectSucceeded;
|
|
107
|
+
private shouldStartSubscriber;
|
|
105
108
|
}
|
|
@@ -20,8 +20,9 @@ class ClusterSubscriberGroup {
|
|
|
20
20
|
*
|
|
21
21
|
* @param cluster
|
|
22
22
|
*/
|
|
23
|
-
constructor(subscriberGroupEmitter) {
|
|
23
|
+
constructor(subscriberGroupEmitter, options) {
|
|
24
24
|
this.subscriberGroupEmitter = subscriberGroupEmitter;
|
|
25
|
+
this.options = options;
|
|
25
26
|
this.shardedSubscribers = new Map();
|
|
26
27
|
this.clusterSlots = [];
|
|
27
28
|
// Simple [min, max] slot ranges aren't enough because you can migrate single slots
|
|
@@ -68,7 +69,18 @@ class ClusterSubscriberGroup {
|
|
|
68
69
|
*/
|
|
69
70
|
getResponsibleSubscriber(slot) {
|
|
70
71
|
const nodeKey = this.clusterSlots[slot][0];
|
|
71
|
-
|
|
72
|
+
const sub = this.shardedSubscribers.get(nodeKey);
|
|
73
|
+
if (sub && sub.subscriberStatus === "idle") {
|
|
74
|
+
sub
|
|
75
|
+
.start()
|
|
76
|
+
.then(() => {
|
|
77
|
+
this.handleSubscriberConnectSucceeded(sub.getNodeKey());
|
|
78
|
+
})
|
|
79
|
+
.catch((err) => {
|
|
80
|
+
this.handleSubscriberConnectFailed(err, sub.getNodeKey());
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return sub;
|
|
72
84
|
}
|
|
73
85
|
/**
|
|
74
86
|
* Adds a channel for which this subscriber group is responsible
|
|
@@ -130,7 +142,7 @@ class ClusterSubscriberGroup {
|
|
|
130
142
|
start() {
|
|
131
143
|
const startPromises = [];
|
|
132
144
|
for (const s of this.shardedSubscribers.values()) {
|
|
133
|
-
if (
|
|
145
|
+
if (this.shouldStartSubscriber(s)) {
|
|
134
146
|
startPromises.push(s
|
|
135
147
|
.start()
|
|
136
148
|
.then(() => {
|
|
@@ -139,6 +151,7 @@ class ClusterSubscriberGroup {
|
|
|
139
151
|
.catch((err) => {
|
|
140
152
|
this.handleSubscriberConnectFailed(err, s.getNodeKey());
|
|
141
153
|
}));
|
|
154
|
+
this.subscriberGroupEmitter.emit("+subscriber");
|
|
142
155
|
}
|
|
143
156
|
}
|
|
144
157
|
return Promise.all(startPromises);
|
|
@@ -162,9 +175,9 @@ class ClusterSubscriberGroup {
|
|
|
162
175
|
// For each of the sharded subscribers
|
|
163
176
|
for (const [nodeKey, shardedSubscriber] of this.shardedSubscribers) {
|
|
164
177
|
if (
|
|
165
|
-
// If the subscriber is still responsible for a slot range and is
|
|
178
|
+
// If the subscriber is still responsible for a slot range and is healthy then keep it
|
|
166
179
|
this.subscriberToSlotsIndex.has(nodeKey) &&
|
|
167
|
-
shardedSubscriber.
|
|
180
|
+
shardedSubscriber.isHealthy()) {
|
|
168
181
|
debug("Skipping deleting subscriber for %s", nodeKey);
|
|
169
182
|
continue;
|
|
170
183
|
}
|
|
@@ -177,11 +190,31 @@ class ClusterSubscriberGroup {
|
|
|
177
190
|
const startPromises = [];
|
|
178
191
|
// For each node in slots cache
|
|
179
192
|
for (const [nodeKey, _] of this.subscriberToSlotsIndex) {
|
|
180
|
-
|
|
181
|
-
|
|
193
|
+
const existingSubscriber = this.shardedSubscribers.get(nodeKey);
|
|
194
|
+
// If we already have a subscriber for this node, only ensure it is healthy
|
|
195
|
+
// when it now owns slots with active channel subscriptions.
|
|
196
|
+
if (existingSubscriber && existingSubscriber.isHealthy()) {
|
|
182
197
|
debug("Skipping creating new subscriber for %s", nodeKey);
|
|
198
|
+
if (!existingSubscriber.isStarted() &&
|
|
199
|
+
this.shouldStartSubscriber(existingSubscriber)) {
|
|
200
|
+
startPromises.push(existingSubscriber
|
|
201
|
+
.start()
|
|
202
|
+
.then(() => {
|
|
203
|
+
this.handleSubscriberConnectSucceeded(nodeKey);
|
|
204
|
+
})
|
|
205
|
+
.catch((error) => {
|
|
206
|
+
this.handleSubscriberConnectFailed(error, nodeKey);
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
183
209
|
continue;
|
|
184
210
|
}
|
|
211
|
+
// If we have an existing subscriber but it is not healthy, stop it
|
|
212
|
+
if (existingSubscriber && !existingSubscriber.isHealthy()) {
|
|
213
|
+
debug("Replacing subscriber for %s", nodeKey);
|
|
214
|
+
existingSubscriber.stop();
|
|
215
|
+
this.shardedSubscribers.delete(nodeKey);
|
|
216
|
+
this.subscriberGroupEmitter.emit("-subscriber");
|
|
217
|
+
}
|
|
185
218
|
debug("Creating new subscriber for %s", nodeKey);
|
|
186
219
|
// Otherwise create a new subscriber
|
|
187
220
|
const redis = clusterNodes.find((node) => {
|
|
@@ -191,16 +224,18 @@ class ClusterSubscriberGroup {
|
|
|
191
224
|
debug("Failed to find node for key %s", nodeKey);
|
|
192
225
|
continue;
|
|
193
226
|
}
|
|
194
|
-
const sub = new ShardedSubscriber_1.default(this.subscriberGroupEmitter, redis.options);
|
|
227
|
+
const sub = new ShardedSubscriber_1.default(this.subscriberGroupEmitter, redis.options, this.options.redisOptions);
|
|
195
228
|
this.shardedSubscribers.set(nodeKey, sub);
|
|
196
|
-
|
|
197
|
-
.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
229
|
+
if (this.shouldStartSubscriber(sub)) {
|
|
230
|
+
startPromises.push(sub
|
|
231
|
+
.start()
|
|
232
|
+
.then(() => {
|
|
233
|
+
this.handleSubscriberConnectSucceeded(nodeKey);
|
|
234
|
+
})
|
|
235
|
+
.catch((error) => {
|
|
236
|
+
this.handleSubscriberConnectFailed(error, nodeKey);
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
204
239
|
this.subscriberGroupEmitter.emit("+subscriber");
|
|
205
240
|
}
|
|
206
241
|
// It's vital to await the start promises before resubscribing
|
|
@@ -228,7 +263,8 @@ class ClusterSubscriberGroup {
|
|
|
228
263
|
*/
|
|
229
264
|
_refreshSlots(targetSlots) {
|
|
230
265
|
//If there was an actual change, then reassign the slot ranges
|
|
231
|
-
if (
|
|
266
|
+
// Also rebuild if subscriberToSlotsIndex is empty (e.g., after stop() was called)
|
|
267
|
+
if (this._slotsAreEqual(targetSlots) && this.subscriberToSlotsIndex.size > 0) {
|
|
232
268
|
debug("Nothing to refresh because the new cluster map is equal to the previous one.");
|
|
233
269
|
return false;
|
|
234
270
|
}
|
|
@@ -262,7 +298,7 @@ class ClusterSubscriberGroup {
|
|
|
262
298
|
const redis = s.getInstance();
|
|
263
299
|
const channels = this.channels.get(ss);
|
|
264
300
|
if (channels && channels.length > 0) {
|
|
265
|
-
if (redis.status === "end") {
|
|
301
|
+
if (!redis || redis.status === "end") {
|
|
266
302
|
return;
|
|
267
303
|
}
|
|
268
304
|
if (redis.status === "ready") {
|
|
@@ -309,10 +345,26 @@ class ClusterSubscriberGroup {
|
|
|
309
345
|
* @returns true if any subscribers need to be recreated
|
|
310
346
|
*/
|
|
311
347
|
hasUnhealthySubscribers() {
|
|
312
|
-
const hasFailedSubscribers = Array.from(this.shardedSubscribers.values()).some((sub) => !sub.
|
|
348
|
+
const hasFailedSubscribers = Array.from(this.shardedSubscribers.values()).some((sub) => !sub.isHealthy());
|
|
313
349
|
const hasMissingSubscribers = Array.from(this.subscriberToSlotsIndex.keys()).some((nodeKey) => !this.shardedSubscribers.has(nodeKey));
|
|
314
350
|
return hasFailedSubscribers || hasMissingSubscribers;
|
|
315
351
|
}
|
|
352
|
+
shouldStartSubscriber(sub) {
|
|
353
|
+
if (sub.isStarted()) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
if (!sub.isLazyConnect()) {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
const subscriberSlots = this.subscriberToSlotsIndex.get(sub.getNodeKey());
|
|
360
|
+
if (!subscriberSlots) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
return subscriberSlots.some((slot) => {
|
|
364
|
+
const channels = this.channels.get(slot);
|
|
365
|
+
return Boolean(channels && channels.length > 0);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
316
368
|
}
|
|
317
369
|
exports.default = ClusterSubscriberGroup;
|
|
318
370
|
// Retry strategy
|
|
@@ -2,19 +2,35 @@
|
|
|
2
2
|
import EventEmitter = require("events");
|
|
3
3
|
import { RedisOptions } from "./util";
|
|
4
4
|
import Redis from "../Redis";
|
|
5
|
+
import { ClusterOptions } from "./ClusterOptions";
|
|
6
|
+
declare const SubscriberStatus: {
|
|
7
|
+
readonly IDLE: "idle";
|
|
8
|
+
readonly STARTING: "starting";
|
|
9
|
+
readonly CONNECTED: "connected";
|
|
10
|
+
readonly STOPPING: "stopping";
|
|
11
|
+
readonly ENDED: "ended";
|
|
12
|
+
};
|
|
13
|
+
declare type SubscriberStatus = typeof SubscriberStatus[keyof typeof SubscriberStatus];
|
|
5
14
|
export default class ShardedSubscriber {
|
|
6
15
|
private readonly emitter;
|
|
7
16
|
private readonly nodeKey;
|
|
8
|
-
private
|
|
17
|
+
private status;
|
|
9
18
|
private instance;
|
|
19
|
+
private connectPromise;
|
|
20
|
+
private lazyConnect;
|
|
10
21
|
private readonly messageListeners;
|
|
11
|
-
constructor(emitter: EventEmitter, options: RedisOptions);
|
|
12
|
-
private onEnd;
|
|
13
|
-
private onError;
|
|
14
|
-
private onMoved;
|
|
22
|
+
constructor(emitter: EventEmitter, options: RedisOptions, redisOptions?: ClusterOptions["redisOptions"]);
|
|
15
23
|
start(): Promise<void>;
|
|
16
24
|
stop(): void;
|
|
17
25
|
isStarted(): boolean;
|
|
26
|
+
get subscriberStatus(): SubscriberStatus;
|
|
27
|
+
isHealthy(): boolean;
|
|
18
28
|
getInstance(): Redis | null;
|
|
19
29
|
getNodeKey(): string;
|
|
30
|
+
isLazyConnect(): boolean;
|
|
31
|
+
private onEnd;
|
|
32
|
+
private onError;
|
|
33
|
+
private onMoved;
|
|
34
|
+
private updateStatus;
|
|
20
35
|
}
|
|
36
|
+
export {};
|