ioredis 4.28.5 → 4.29.1
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 +1 -1
- package/built/DataHandler.js +9 -0
- package/built/SubscriptionSet.js +6 -1
- package/built/autoPipelining.js +3 -1
- package/built/cluster/ClusterOptions.js +1 -0
- package/built/cluster/ClusterSubscriber.js +40 -6
- package/built/cluster/ClusterSubscriberGroup.js +224 -0
- package/built/cluster/ConnectionPool.js +34 -11
- package/built/cluster/index.js +29 -4
- package/built/command.js +8 -5
- package/built/commander.js +2 -3
- package/built/pipeline.js +2 -2
- package/built/redis/event_handler.js +5 -0
- package/built/redis/index.js +3 -3
- package/package.json +4 -4
- package/Changelog.md +0 -1535
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
A robust, performance-focused and full-featured [Redis](http://redis.io) client for [Node.js](https://nodejs.org).
|
|
14
14
|
|
|
15
|
-
Supports Redis >= 2.6.12 and (Node.js >=
|
|
15
|
+
Supports Redis >= 2.6.12 and (Node.js >= 8). Completely compatible with Redis 7.x.
|
|
16
16
|
|
|
17
17
|
# Features
|
|
18
18
|
|
package/built/DataHandler.js
CHANGED
|
@@ -89,6 +89,14 @@ class DataHandler {
|
|
|
89
89
|
this.redis.emit("pmessageBuffer", pattern, reply[2], reply[3]);
|
|
90
90
|
break;
|
|
91
91
|
}
|
|
92
|
+
case "smessage": {
|
|
93
|
+
if (this.redis.listeners("smessage").length > 0) {
|
|
94
|
+
this.redis.emit("smessage", reply[1].toString(), reply[2] ? reply[2].toString() : "");
|
|
95
|
+
}
|
|
96
|
+
this.redis.emit("smessageBuffer", reply[1], reply[2]);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case "ssubscribe":
|
|
92
100
|
case "subscribe":
|
|
93
101
|
case "psubscribe": {
|
|
94
102
|
const channel = reply[1].toString();
|
|
@@ -102,6 +110,7 @@ class DataHandler {
|
|
|
102
110
|
}
|
|
103
111
|
break;
|
|
104
112
|
}
|
|
113
|
+
case "sunsubscribe":
|
|
105
114
|
case "unsubscribe":
|
|
106
115
|
case "punsubscribe": {
|
|
107
116
|
const channel = reply[1] ? reply[1].toString() : null;
|
package/built/SubscriptionSet.js
CHANGED
|
@@ -11,6 +11,7 @@ class SubscriptionSet {
|
|
|
11
11
|
this.set = {
|
|
12
12
|
subscribe: {},
|
|
13
13
|
psubscribe: {},
|
|
14
|
+
ssubscribe: {},
|
|
14
15
|
};
|
|
15
16
|
}
|
|
16
17
|
add(set, channel) {
|
|
@@ -24,7 +25,8 @@ class SubscriptionSet {
|
|
|
24
25
|
}
|
|
25
26
|
isEmpty() {
|
|
26
27
|
return (this.channels("subscribe").length === 0 &&
|
|
27
|
-
this.channels("psubscribe").length === 0
|
|
28
|
+
this.channels("psubscribe").length === 0 &&
|
|
29
|
+
this.channels("ssubscribe").length === 0);
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
exports.default = SubscriptionSet;
|
|
@@ -35,5 +37,8 @@ function mapSet(set) {
|
|
|
35
37
|
if (set === "punsubscribe") {
|
|
36
38
|
return "psubscribe";
|
|
37
39
|
}
|
|
40
|
+
if (set === "sunsubscribe") {
|
|
41
|
+
return "ssubscribe";
|
|
42
|
+
}
|
|
38
43
|
return set;
|
|
39
44
|
}
|
package/built/autoPipelining.js
CHANGED
|
@@ -5,11 +5,13 @@ const utils_1 = require("../utils");
|
|
|
5
5
|
const redis_1 = require("../redis");
|
|
6
6
|
const debug = utils_1.Debug("cluster:subscriber");
|
|
7
7
|
class ClusterSubscriber {
|
|
8
|
-
constructor(connectionPool, emitter) {
|
|
8
|
+
constructor(connectionPool, emitter, isSharded = false) {
|
|
9
9
|
this.connectionPool = connectionPool;
|
|
10
10
|
this.emitter = emitter;
|
|
11
|
+
this.isSharded = isSharded;
|
|
11
12
|
this.started = false;
|
|
12
13
|
this.subscriber = null;
|
|
14
|
+
this.slotRange = [];
|
|
13
15
|
this.connectionPool.on("-node", (_, key) => {
|
|
14
16
|
if (!this.started || !this.subscriber) {
|
|
15
17
|
return;
|
|
@@ -57,31 +59,38 @@ class ClusterSubscriber {
|
|
|
57
59
|
* provided for the subscriber is correct, and if not, the current subscriber
|
|
58
60
|
* will be disconnected and a new subscriber will be selected.
|
|
59
61
|
*/
|
|
62
|
+
let connectionPrefix = "subscriber";
|
|
63
|
+
if (this.isSharded)
|
|
64
|
+
connectionPrefix = "ssubscriber";
|
|
60
65
|
this.subscriber = new redis_1.default({
|
|
61
66
|
port: options.port,
|
|
62
67
|
host: options.host,
|
|
63
68
|
username: options.username,
|
|
64
69
|
password: options.password,
|
|
65
70
|
enableReadyCheck: true,
|
|
66
|
-
connectionName: util_1.getConnectionName(
|
|
71
|
+
connectionName: util_1.getConnectionName(connectionPrefix, options.connectionName),
|
|
67
72
|
lazyConnect: true,
|
|
68
73
|
tls: options.tls,
|
|
69
74
|
});
|
|
70
75
|
// Ignore the errors since they're handled in the connection pool.
|
|
71
76
|
this.subscriber.on("error", utils_1.noop);
|
|
72
77
|
// Re-subscribe previous channels
|
|
73
|
-
const previousChannels = { subscribe: [], psubscribe: [] };
|
|
78
|
+
const previousChannels = { subscribe: [], psubscribe: [], ssubscribe: [] };
|
|
74
79
|
if (lastActiveSubscriber) {
|
|
75
80
|
const condition = lastActiveSubscriber.condition || lastActiveSubscriber.prevCondition;
|
|
76
81
|
if (condition && condition.subscriber) {
|
|
77
82
|
previousChannels.subscribe = condition.subscriber.channels("subscribe");
|
|
78
|
-
previousChannels.psubscribe =
|
|
83
|
+
previousChannels.psubscribe =
|
|
84
|
+
condition.subscriber.channels("psubscribe");
|
|
85
|
+
previousChannels.ssubscribe =
|
|
86
|
+
condition.subscriber.channels("ssubscribe");
|
|
79
87
|
}
|
|
80
88
|
}
|
|
81
89
|
if (previousChannels.subscribe.length ||
|
|
82
|
-
previousChannels.psubscribe.length
|
|
90
|
+
previousChannels.psubscribe.length ||
|
|
91
|
+
previousChannels.ssubscribe.length) {
|
|
83
92
|
let pending = 0;
|
|
84
|
-
for (const type of ["subscribe", "psubscribe"]) {
|
|
93
|
+
for (const type of ["subscribe", "psubscribe", "ssubscribe"]) {
|
|
85
94
|
const channels = previousChannels[type];
|
|
86
95
|
if (channels.length) {
|
|
87
96
|
pending += 1;
|
|
@@ -112,6 +121,28 @@ class ClusterSubscriber {
|
|
|
112
121
|
this.emitter.emit(event, arg1, arg2, arg3);
|
|
113
122
|
});
|
|
114
123
|
}
|
|
124
|
+
if (this.isSharded == true) {
|
|
125
|
+
for (const event of ["smessage", "smessageBuffer"]) {
|
|
126
|
+
this.subscriber.on(event, (arg1, arg2, arg3) => {
|
|
127
|
+
this.emitter.emit(event, arg1, arg2, arg3);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Associate this subscriber to a specific slot range.
|
|
134
|
+
*
|
|
135
|
+
* Returns the range or an empty array if the slot range couldn't be associated.
|
|
136
|
+
*
|
|
137
|
+
* BTW: This is more for debugging and testing purposes.
|
|
138
|
+
*
|
|
139
|
+
* @param range
|
|
140
|
+
*/
|
|
141
|
+
associateSlotRange(range) {
|
|
142
|
+
if (this.isSharded) {
|
|
143
|
+
this.slotRange = range;
|
|
144
|
+
}
|
|
145
|
+
return this.slotRange;
|
|
115
146
|
}
|
|
116
147
|
start() {
|
|
117
148
|
this.started = true;
|
|
@@ -126,5 +157,8 @@ class ClusterSubscriber {
|
|
|
126
157
|
}
|
|
127
158
|
debug("stopped");
|
|
128
159
|
}
|
|
160
|
+
isStarted() {
|
|
161
|
+
return this.started;
|
|
162
|
+
}
|
|
129
163
|
}
|
|
130
164
|
exports.default = ClusterSubscriber;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const utils_1 = require("../utils");
|
|
4
|
+
const ClusterSubscriber_1 = require("./ClusterSubscriber");
|
|
5
|
+
const ConnectionPool_1 = require("./ConnectionPool");
|
|
6
|
+
const util_1 = require("./util");
|
|
7
|
+
const calculateSlot = require("cluster-key-slot");
|
|
8
|
+
const debug = utils_1.Debug("cluster:subscriberGroup");
|
|
9
|
+
/**
|
|
10
|
+
* Redis differs between "normal" and sharded PubSub. If using the "normal" PubSub feature, exactly one
|
|
11
|
+
* ClusterSubscriber exists per cluster instance. This works because the Redis cluster bus forwards m
|
|
12
|
+
* messages between shards. However, this has scalability limitations, which is the reason why the sharded
|
|
13
|
+
* PubSub feature was added to Redis. With sharded PubSub, each shard is responsible for its own messages.
|
|
14
|
+
* Given that, we need at least one ClusterSubscriber per master endpoint/node.
|
|
15
|
+
*
|
|
16
|
+
* This class leverages the previously exising ClusterSubscriber by adding support for multiple such subscribers
|
|
17
|
+
* in alignment to the master nodes of the cluster. The ClusterSubscriber class was extended in a non-breaking way
|
|
18
|
+
* to support this feature.
|
|
19
|
+
*/
|
|
20
|
+
class ClusterSubscriberGroup {
|
|
21
|
+
/**
|
|
22
|
+
* Register callbacks
|
|
23
|
+
*
|
|
24
|
+
* @param cluster
|
|
25
|
+
*/
|
|
26
|
+
constructor(cluster) {
|
|
27
|
+
this.cluster = cluster;
|
|
28
|
+
this.shardedSubscribers = new Map();
|
|
29
|
+
this.clusterSlots = [];
|
|
30
|
+
//Simple [min, max] slot ranges aren't enough because you can migrate single slots
|
|
31
|
+
this.subscriberToSlotsIndex = new Map();
|
|
32
|
+
this.channels = new Map();
|
|
33
|
+
cluster.on("+node", (redis) => {
|
|
34
|
+
this._addSubscriber(redis);
|
|
35
|
+
});
|
|
36
|
+
cluster.on("-node", (redis) => {
|
|
37
|
+
this._removeSubscriber(redis);
|
|
38
|
+
});
|
|
39
|
+
cluster.on("refresh", () => {
|
|
40
|
+
this._refreshSlots(cluster);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get the responsible subscriber.
|
|
45
|
+
*
|
|
46
|
+
* Returns null if no subscriber was found
|
|
47
|
+
*
|
|
48
|
+
* @param slot
|
|
49
|
+
*/
|
|
50
|
+
getResponsibleSubscriber(slot) {
|
|
51
|
+
const nodeKey = this.clusterSlots[slot][0];
|
|
52
|
+
return this.shardedSubscribers.get(nodeKey);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Adds a channel for which this subscriber group is responsible
|
|
56
|
+
*
|
|
57
|
+
* @param channels
|
|
58
|
+
*/
|
|
59
|
+
addChannels(channels) {
|
|
60
|
+
const slot = calculateSlot(channels[0]);
|
|
61
|
+
//Check if the all channels belong to the same slot and otherwise reject the operation
|
|
62
|
+
channels.forEach((c) => {
|
|
63
|
+
if (calculateSlot(c) != slot)
|
|
64
|
+
return -1;
|
|
65
|
+
});
|
|
66
|
+
const currChannels = this.channels.get(slot);
|
|
67
|
+
if (!currChannels) {
|
|
68
|
+
this.channels.set(slot, channels);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
this.channels.set(slot, currChannels.concat(channels));
|
|
72
|
+
}
|
|
73
|
+
return Array.from(this.channels.values()).reduce((sum, array) => sum + array.length, 0);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Removes channels for which the subscriber group is responsible by optionally unsubscribing
|
|
77
|
+
* @param channels
|
|
78
|
+
*/
|
|
79
|
+
removeChannels(channels) {
|
|
80
|
+
const slot = calculateSlot(channels[0]);
|
|
81
|
+
//Check if the all channels belong to the same slot and otherwise reject the operation
|
|
82
|
+
channels.forEach((c) => {
|
|
83
|
+
if (calculateSlot(c) != slot)
|
|
84
|
+
return -1;
|
|
85
|
+
});
|
|
86
|
+
const slotChannels = this.channels.get(slot);
|
|
87
|
+
if (slotChannels) {
|
|
88
|
+
const updatedChannels = slotChannels.filter((c) => !channels.includes(c));
|
|
89
|
+
this.channels.set(slot, updatedChannels);
|
|
90
|
+
}
|
|
91
|
+
return Array.from(this.channels.values()).reduce((sum, array) => sum + array.length, 0);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Disconnect all subscribers
|
|
95
|
+
*/
|
|
96
|
+
stop() {
|
|
97
|
+
for (const s of this.shardedSubscribers.values()) {
|
|
98
|
+
s.stop();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Start all not yet started subscribers
|
|
103
|
+
*/
|
|
104
|
+
start() {
|
|
105
|
+
for (const s of this.shardedSubscribers.values()) {
|
|
106
|
+
if (!s.isStarted()) {
|
|
107
|
+
s.start();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Add a subscriber to the group of subscribers
|
|
113
|
+
*
|
|
114
|
+
* @param redis
|
|
115
|
+
*/
|
|
116
|
+
_addSubscriber(redis) {
|
|
117
|
+
const pool = new ConnectionPool_1.default(redis.options);
|
|
118
|
+
if (pool.addMasterNode(redis)) {
|
|
119
|
+
const sub = new ClusterSubscriber_1.default(pool, this.cluster, true);
|
|
120
|
+
const nodeKey = util_1.getNodeKey(redis.options);
|
|
121
|
+
this.shardedSubscribers.set(nodeKey, sub);
|
|
122
|
+
sub.start();
|
|
123
|
+
// We need to attempt to resubscribe them in case the new node serves their slot
|
|
124
|
+
this._resubscribe();
|
|
125
|
+
this.cluster.emit("+subscriber");
|
|
126
|
+
return sub;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Removes a subscriber from the group
|
|
132
|
+
* @param redis
|
|
133
|
+
*/
|
|
134
|
+
_removeSubscriber(redis) {
|
|
135
|
+
const nodeKey = util_1.getNodeKey(redis.options);
|
|
136
|
+
const sub = this.shardedSubscribers.get(nodeKey);
|
|
137
|
+
if (sub) {
|
|
138
|
+
sub.stop();
|
|
139
|
+
this.shardedSubscribers.delete(nodeKey);
|
|
140
|
+
// Even though the subscriber to this node is going down, we might have another subscriber
|
|
141
|
+
// handling the same slots, so we need to attempt to subscribe the orphaned channels
|
|
142
|
+
this._resubscribe();
|
|
143
|
+
this.cluster.emit("-subscriber");
|
|
144
|
+
}
|
|
145
|
+
return this.shardedSubscribers;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Refreshes the subscriber-related slot ranges
|
|
149
|
+
*
|
|
150
|
+
* Returns false if no refresh was needed
|
|
151
|
+
*
|
|
152
|
+
* @param cluster
|
|
153
|
+
*/
|
|
154
|
+
_refreshSlots(cluster) {
|
|
155
|
+
//If there was an actual change, then reassign the slot ranges
|
|
156
|
+
if (this._slotsAreEqual(cluster.slots)) {
|
|
157
|
+
debug("Nothing to refresh because the new cluster map is equal to the previous one.");
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
debug("Refreshing the slots of the subscriber group.");
|
|
161
|
+
//Rebuild the slots index
|
|
162
|
+
this.subscriberToSlotsIndex = new Map();
|
|
163
|
+
for (let slot = 0; slot < cluster.slots.length; slot++) {
|
|
164
|
+
const node = cluster.slots[slot][0];
|
|
165
|
+
if (!this.subscriberToSlotsIndex.has(node)) {
|
|
166
|
+
this.subscriberToSlotsIndex.set(node, []);
|
|
167
|
+
}
|
|
168
|
+
this.subscriberToSlotsIndex.get(node).push(Number(slot));
|
|
169
|
+
}
|
|
170
|
+
//Update the subscribers from the index
|
|
171
|
+
this._resubscribe();
|
|
172
|
+
//Update the cached slots map
|
|
173
|
+
this.clusterSlots = JSON.parse(JSON.stringify(cluster.slots));
|
|
174
|
+
this.cluster.emit("subscribersReady");
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Resubscribes to the previous channels
|
|
181
|
+
*
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
_resubscribe() {
|
|
185
|
+
if (this.shardedSubscribers) {
|
|
186
|
+
this.shardedSubscribers.forEach((s, nodeKey) => {
|
|
187
|
+
const subscriberSlots = this.subscriberToSlotsIndex.get(nodeKey);
|
|
188
|
+
if (subscriberSlots) {
|
|
189
|
+
//More for debugging purposes
|
|
190
|
+
s.associateSlotRange(subscriberSlots);
|
|
191
|
+
//Resubscribe on the underlying connection
|
|
192
|
+
subscriberSlots.forEach((ss) => {
|
|
193
|
+
//Might return null if being disconnected
|
|
194
|
+
const redis = s.getInstance();
|
|
195
|
+
const channels = this.channels.get(ss);
|
|
196
|
+
if (channels && channels.length > 0) {
|
|
197
|
+
//Try to subscribe now
|
|
198
|
+
if (redis) {
|
|
199
|
+
redis.ssubscribe(channels);
|
|
200
|
+
//If the instance isn't ready yet, then register the re-subscription for later
|
|
201
|
+
redis.on("ready", () => {
|
|
202
|
+
redis.ssubscribe(channels);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Deep equality of the cluster slots objects
|
|
213
|
+
*
|
|
214
|
+
* @param other
|
|
215
|
+
* @private
|
|
216
|
+
*/
|
|
217
|
+
_slotsAreEqual(other) {
|
|
218
|
+
if (this.clusterSlots === undefined)
|
|
219
|
+
return false;
|
|
220
|
+
else
|
|
221
|
+
return JSON.stringify(this.clusterSlots) === JSON.stringify(other);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
exports.default = ClusterSubscriberGroup;
|
|
@@ -29,6 +29,39 @@ class ConnectionPool extends events_1.EventEmitter {
|
|
|
29
29
|
const sampleKey = utils_1.sample(keys);
|
|
30
30
|
return this.nodes[role][sampleKey];
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Add a master node to the pool
|
|
34
|
+
* @param node
|
|
35
|
+
*/
|
|
36
|
+
addMasterNode(node) {
|
|
37
|
+
const key = util_1.getNodeKey(node.options);
|
|
38
|
+
const redis = this.createRedisFromOptions(node, node.options.readOnly);
|
|
39
|
+
//Master nodes aren't read-only
|
|
40
|
+
if (!node.options.readOnly) {
|
|
41
|
+
this.nodes.all[key] = redis;
|
|
42
|
+
this.nodes.master[key] = redis;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Creates a Redis connection instance from the node options
|
|
49
|
+
* @param node
|
|
50
|
+
* @param readOnly
|
|
51
|
+
*/
|
|
52
|
+
createRedisFromOptions(node, readOnly) {
|
|
53
|
+
return new redis_1.default(utils_1.defaults({
|
|
54
|
+
// Never try to reconnect when a node is lose,
|
|
55
|
+
// instead, waiting for a `MOVED` error and
|
|
56
|
+
// fetch the slots again.
|
|
57
|
+
retryStrategy: null,
|
|
58
|
+
// Offline queue should be enabled so that
|
|
59
|
+
// we don't need to wait for the `ready` event
|
|
60
|
+
// before sending commands to the node.
|
|
61
|
+
enableOfflineQueue: true,
|
|
62
|
+
readOnly: readOnly,
|
|
63
|
+
}, node, this.redisOptions, { lazyConnect: true }));
|
|
64
|
+
}
|
|
32
65
|
/**
|
|
33
66
|
* Find or create a connection to the node
|
|
34
67
|
*
|
|
@@ -65,17 +98,7 @@ class ConnectionPool extends events_1.EventEmitter {
|
|
|
65
98
|
}
|
|
66
99
|
else {
|
|
67
100
|
debug("Connecting to %s as %s", key, readOnly ? "slave" : "master");
|
|
68
|
-
redis =
|
|
69
|
-
// Never try to reconnect when a node is lose,
|
|
70
|
-
// instead, waiting for a `MOVED` error and
|
|
71
|
-
// fetch the slots again.
|
|
72
|
-
retryStrategy: null,
|
|
73
|
-
// Offline queue should be enabled so that
|
|
74
|
-
// we don't need to wait for the `ready` event
|
|
75
|
-
// before sending commands to the node.
|
|
76
|
-
enableOfflineQueue: true,
|
|
77
|
-
readOnly: readOnly,
|
|
78
|
-
}, node, this.redisOptions, { lazyConnect: true }));
|
|
101
|
+
redis = this.createRedisFromOptions(node, readOnly);
|
|
79
102
|
this.nodes.all[key] = redis;
|
|
80
103
|
this.nodes[readOnly ? "slave" : "master"][key] = redis;
|
|
81
104
|
redis.once("end", () => {
|
package/built/cluster/index.js
CHANGED
|
@@ -13,11 +13,12 @@ const standard_as_callback_1 = require("standard-as-callback");
|
|
|
13
13
|
const PromiseContainer = require("../promiseContainer");
|
|
14
14
|
const ClusterOptions_1 = require("./ClusterOptions");
|
|
15
15
|
const utils_2 = require("../utils");
|
|
16
|
-
const
|
|
16
|
+
const commands_1 = require("@ioredis/commands");
|
|
17
17
|
const command_1 = require("../command");
|
|
18
18
|
const redis_1 = require("../redis");
|
|
19
19
|
const commander_1 = require("../commander");
|
|
20
20
|
const Deque = require("denque");
|
|
21
|
+
const ClusterSubscriberGroup_1 = require("./ClusterSubscriberGroup");
|
|
21
22
|
const debug = utils_1.Debug("cluster");
|
|
22
23
|
/**
|
|
23
24
|
* Client for the official Redis Cluster
|
|
@@ -61,6 +62,8 @@ class Cluster extends events_1.EventEmitter {
|
|
|
61
62
|
commander_1.default.call(this);
|
|
62
63
|
this.startupNodes = startupNodes;
|
|
63
64
|
this.options = utils_1.defaults({}, options, ClusterOptions_1.DEFAULT_CLUSTER_OPTIONS, this.options);
|
|
65
|
+
if (this.options.shardedSubscribers == true)
|
|
66
|
+
this.shardedSubscribers = new ClusterSubscriberGroup_1.default(this);
|
|
64
67
|
// validate options
|
|
65
68
|
if (typeof this.options.scaleReads !== "function" &&
|
|
66
69
|
["all", "master", "slave"].indexOf(this.options.scaleReads) === -1) {
|
|
@@ -202,6 +205,9 @@ class Cluster extends events_1.EventEmitter {
|
|
|
202
205
|
}
|
|
203
206
|
}.bind(this));
|
|
204
207
|
this.subscriber.start();
|
|
208
|
+
if (this.options.shardedSubscribers) {
|
|
209
|
+
this.shardedSubscribers.start();
|
|
210
|
+
}
|
|
205
211
|
})
|
|
206
212
|
.catch((err) => {
|
|
207
213
|
this.setStatus("close");
|
|
@@ -262,6 +268,9 @@ class Cluster extends events_1.EventEmitter {
|
|
|
262
268
|
}
|
|
263
269
|
this.clearNodesRefreshInterval();
|
|
264
270
|
this.subscriber.stop();
|
|
271
|
+
if (this.options.shardedSubscribers) {
|
|
272
|
+
this.shardedSubscribers.stop();
|
|
273
|
+
}
|
|
265
274
|
if (status === "wait") {
|
|
266
275
|
this.setStatus("close");
|
|
267
276
|
this.handleCloseEvent();
|
|
@@ -475,8 +484,7 @@ class Cluster extends events_1.EventEmitter {
|
|
|
475
484
|
let to = this.options.scaleReads;
|
|
476
485
|
if (to !== "master") {
|
|
477
486
|
const isCommandReadOnly = command.isReadOnly ||
|
|
478
|
-
(
|
|
479
|
-
commands.hasFlag(command.name, "readonly"));
|
|
487
|
+
(commands_1.exists(command.name) && commands_1.hasFlag(command.name, "readonly"));
|
|
480
488
|
if (!isCommandReadOnly) {
|
|
481
489
|
to = "master";
|
|
482
490
|
}
|
|
@@ -538,7 +546,24 @@ class Cluster extends events_1.EventEmitter {
|
|
|
538
546
|
}
|
|
539
547
|
else if (command_1.default.checkFlag("ENTER_SUBSCRIBER_MODE", command.name) ||
|
|
540
548
|
command_1.default.checkFlag("EXIT_SUBSCRIBER_MODE", command.name)) {
|
|
541
|
-
|
|
549
|
+
if (_this.options.shardedSubscribers == true &&
|
|
550
|
+
(command.name == "ssubscribe" || command.name == "sunsubscribe")) {
|
|
551
|
+
const sub = _this.shardedSubscribers.getResponsibleSubscriber(targetSlot);
|
|
552
|
+
let status = -1;
|
|
553
|
+
if (command.name == "ssubscribe")
|
|
554
|
+
status = _this.shardedSubscribers.addChannels(command.getKeys());
|
|
555
|
+
if (command.name == "sunsubscribe")
|
|
556
|
+
status = _this.shardedSubscribers.removeChannels(command.getKeys());
|
|
557
|
+
if (status !== -1) {
|
|
558
|
+
redis = sub.getInstance();
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
command.reject(new redis_errors_1.AbortError("Can't add or remove the given channels. Are they in the same slot?"));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
redis = _this.subscriber.getInstance();
|
|
566
|
+
}
|
|
542
567
|
if (!redis) {
|
|
543
568
|
command.reject(new redis_errors_1.AbortError("No subscriber for the cluster"));
|
|
544
569
|
return;
|
package/built/command.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const
|
|
3
|
+
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");
|
|
@@ -131,8 +131,9 @@ class Command {
|
|
|
131
131
|
_iterateKeys(transform = (key) => key) {
|
|
132
132
|
if (typeof this.keys === "undefined") {
|
|
133
133
|
this.keys = [];
|
|
134
|
-
if (
|
|
135
|
-
|
|
134
|
+
if (commands_1.exists(this.name)) {
|
|
135
|
+
// @ts-ignore
|
|
136
|
+
const keyIndexes = commands_1.getKeyIndexes(this.name, this.args);
|
|
136
137
|
for (const index of keyIndexes) {
|
|
137
138
|
this.args[index] = transform(this.args[index]);
|
|
138
139
|
this.keys.push(this.args[index]);
|
|
@@ -271,12 +272,14 @@ Command.FLAGS = {
|
|
|
271
272
|
"psubscribe",
|
|
272
273
|
"unsubscribe",
|
|
273
274
|
"punsubscribe",
|
|
275
|
+
"ssubscribe",
|
|
276
|
+
"sunsubscribe",
|
|
274
277
|
"ping",
|
|
275
278
|
"quit",
|
|
276
279
|
],
|
|
277
280
|
VALID_IN_MONITOR_MODE: ["monitor", "auth"],
|
|
278
|
-
ENTER_SUBSCRIBER_MODE: ["subscribe", "psubscribe"],
|
|
279
|
-
EXIT_SUBSCRIBER_MODE: ["unsubscribe", "punsubscribe"],
|
|
281
|
+
ENTER_SUBSCRIBER_MODE: ["subscribe", "psubscribe", "ssubscribe"],
|
|
282
|
+
EXIT_SUBSCRIBER_MODE: ["unsubscribe", "punsubscribe", "sunsubscribe"],
|
|
280
283
|
WILL_DISCONNECT: ["quit"],
|
|
281
284
|
};
|
|
282
285
|
Command._transformer = {
|
package/built/commander.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const lodash_1 = require("./utils/lodash");
|
|
4
|
+
const commands_1 = require("@ioredis/commands");
|
|
4
5
|
const command_1 = require("./command");
|
|
5
6
|
const script_1 = require("./script");
|
|
6
7
|
const PromiseContainer = require("./promiseContainer");
|
|
@@ -26,9 +27,7 @@ function Commander() {
|
|
|
26
27
|
this.addedBuiltinSet = new Set();
|
|
27
28
|
}
|
|
28
29
|
exports.default = Commander;
|
|
29
|
-
const commands =
|
|
30
|
-
return command !== "monitor";
|
|
31
|
-
});
|
|
30
|
+
const commands = commands_1.list.filter((command) => command !== "monitor");
|
|
32
31
|
commands.push("sentinel");
|
|
33
32
|
/**
|
|
34
33
|
* Return supported builtin commands
|
package/built/pipeline.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const command_1 = require("./command");
|
|
4
4
|
const util_1 = require("util");
|
|
5
5
|
const standard_as_callback_1 = require("standard-as-callback");
|
|
6
|
-
const
|
|
6
|
+
const commands_1 = require("@ioredis/commands");
|
|
7
7
|
const calculateSlot = require("cluster-key-slot");
|
|
8
8
|
const pMap = require("p-map");
|
|
9
9
|
const PromiseContainer = require("./promiseContainer");
|
|
@@ -104,7 +104,7 @@ Pipeline.prototype.fillResult = function (value, position) {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
else if (!command.inTransaction) {
|
|
107
|
-
const isReadOnly =
|
|
107
|
+
const isReadOnly = commands_1.exists(command.name) && commands_1.hasFlag(command.name, "readonly");
|
|
108
108
|
if (!isReadOnly) {
|
|
109
109
|
retriable = false;
|
|
110
110
|
break;
|
|
@@ -243,6 +243,11 @@ function readyHandler(self) {
|
|
|
243
243
|
debug("psubscribe %d channels", psubscribeChannels.length);
|
|
244
244
|
self.psubscribe(psubscribeChannels);
|
|
245
245
|
}
|
|
246
|
+
const ssubscribeChannels = condition.subscriber.channels("ssubscribe");
|
|
247
|
+
if (ssubscribeChannels.length) {
|
|
248
|
+
debug("ssubscribe %d channels", ssubscribeChannels.length);
|
|
249
|
+
self.ssubscribe(ssubscribeChannels);
|
|
250
|
+
}
|
|
246
251
|
}
|
|
247
252
|
}
|
|
248
253
|
if (self.prevCommandQueue) {
|
package/built/redis/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const standard_as_callback_1 = require("standard-as-callback");
|
|
|
11
11
|
const eventHandler = require("./event_handler");
|
|
12
12
|
const connectors_1 = require("../connectors");
|
|
13
13
|
const ScanStream_1 = require("../ScanStream");
|
|
14
|
-
const
|
|
14
|
+
const commands_1 = require("@ioredis/commands");
|
|
15
15
|
const PromiseContainer = require("../promiseContainer");
|
|
16
16
|
const transaction_1 = require("../transaction");
|
|
17
17
|
const RedisOptions_1 = require("./RedisOptions");
|
|
@@ -650,8 +650,8 @@ Redis.prototype.sendCommand = function (command, stream) {
|
|
|
650
650
|
let writable = this.status === "ready" ||
|
|
651
651
|
(!stream &&
|
|
652
652
|
this.status === "connect" &&
|
|
653
|
-
|
|
654
|
-
|
|
653
|
+
commands_1.exists(command.name) &&
|
|
654
|
+
commands_1.hasFlag(command.name, "loading"));
|
|
655
655
|
if (!this.stream) {
|
|
656
656
|
writable = false;
|
|
657
657
|
}
|