ioredis 4.24.4 → 4.26.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/Changelog.md CHANGED
@@ -1,3 +1,36 @@
1
+ # [4.26.0](https://github.com/luin/ioredis/compare/v4.25.0...v4.26.0) (2021-04-08)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **cluster:** subscriber connection leaks ([81b9be0](https://github.com/luin/ioredis/commit/81b9be021d471796bba00ee7b08768df9d7e2689)), closes [#1325](https://github.com/luin/ioredis/issues/1325)
7
+
8
+
9
+ ### Features
10
+
11
+ * **cluster:** apply provided connection name to internal connections ([2e388db](https://github.com/luin/ioredis/commit/2e388dbaa528d009b97b82c4dc362377165670a4))
12
+
13
+ # [4.25.0](https://github.com/luin/ioredis/compare/v4.24.6...v4.25.0) (2021-04-02)
14
+
15
+
16
+ ### Features
17
+
18
+ * added commandTimeout option ([#1320](https://github.com/luin/ioredis/issues/1320)) ([56f0272](https://github.com/luin/ioredis/commit/56f02729958545e5b7e713436181b0dd46f8803a))
19
+
20
+ ## [4.24.6](https://github.com/luin/ioredis/compare/v4.24.5...v4.24.6) (2021-03-31)
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * force disconnect after a timeout if socket is still half-open ([#1318](https://github.com/luin/ioredis/issues/1318)) ([6cacd17](https://github.com/luin/ioredis/commit/6cacd17e6ac4d9f995728ee09777e0a7f3b739d7))
26
+
27
+ ## [4.24.5](https://github.com/luin/ioredis/compare/v4.24.4...v4.24.5) (2021-03-27)
28
+
29
+
30
+ ### Bug Fixes
31
+
32
+ * select db in cluster mode causes unhandled errors ([#1311](https://github.com/luin/ioredis/issues/1311)) ([da3ec92](https://github.com/luin/ioredis/commit/da3ec92a406ab6c2f1517810f29f55a0c12712dc)), closes [#1310](https://github.com/luin/ioredis/issues/1310)
33
+
1
34
  ## [4.24.4](https://github.com/luin/ioredis/compare/v4.24.3...v4.24.4) (2021-03-24)
2
35
 
3
36
 
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  A robust, performance-focused and full-featured [Redis](http://redis.io) client for [Node.js](https://nodejs.org).
13
13
 
14
- Supports Redis >= 2.6.12 and (Node.js >= 6).
14
+ Supports Redis >= 2.6.12 and (Node.js >= 6). Completely compatible with Redis 6.x.
15
15
 
16
16
  # Features
17
17
 
@@ -206,12 +206,12 @@ It worth noticing that a connection (aka `Redis` instance) can't play both roles
206
206
  If you want to do pub/sub in the same file/process, you should create a separate connection:
207
207
 
208
208
  ```javascript
209
- const Redis = require('ioredis');
209
+ const Redis = require("ioredis");
210
210
  const sub = new Redis();
211
211
  const pub = new Redis();
212
212
 
213
213
  sub.subscribe(/* ... */); // From now, `sub` enters the subscriber mode.
214
- sub.on("message", /* ... */);
214
+ sub.on("message" /* ... */);
215
215
 
216
216
  setInterval(() => {
217
217
  // `pub` can be used to publish messages, or send other regular commands (e.g. `hgetall`)
@@ -230,6 +230,34 @@ redis.on("pmessage", (pattern, channel, message) => {});
230
230
  redis.on("pmessageBuffer", (pattern, channel, message) => {});
231
231
  ```
232
232
 
233
+ ## Streams
234
+
235
+ Redis v5 introduces a new data type called streams. It doubles as a communication channel for building streaming architectures and as a log-like data structure for persisting data. With ioredis, the usage can be pretty straightforward. Say we have a producer publishes messages to a stream with `redis.xadd("mystream", "*", "randomValue", Math.random())` (You may find the [official documentation of Streams](https://redis.io/topics/streams-intro) as a starter to understand the parameters used), to consume the messages, we'll have a consumer with the following code:
236
+
237
+ ```javascript
238
+ const Redis = require("ioredis");
239
+ const redis = new Redis();
240
+
241
+ const processMessage = (message) => {
242
+ console.log("Id: %s. Data: %O", message[0], message[1]);
243
+ };
244
+
245
+ async function listenForMessage(lastId = "$") {
246
+ // `results` is an array, each element of which corresponds to a key.
247
+ // Because we only listen to one key (mystream) here, `results` only contains
248
+ // a single element. See more: https://redis.io/commands/xread#return-value
249
+ const results = await redis.xread("block", 0, "STREAMS", "mystream", lastId);
250
+ const [key, messages] = results[0]; // `key` equals to "mystream"
251
+
252
+ messages.forEach(processMessage);
253
+
254
+ // Pass the last id of the results to the next round.
255
+ await listenForMessage(messages[messages.length - 1][0]);
256
+ }
257
+
258
+ listenForMessage();
259
+ ```
260
+
233
261
  ## Handle Binary Data
234
262
 
235
263
  Arguments can be buffers:
@@ -741,7 +769,7 @@ The Redis instance will emit some events about the state of the connection to th
741
769
  | close | emits when an established Redis server connection has closed. |
742
770
  | reconnecting | emits after `close` when a reconnection will be made. The argument of the event is the time (in ms) before reconnecting. |
743
771
  | end | emits after `close` when no more reconnections will be made, or the connection is failed to establish. |
744
- | wait | emits when `lazyConnect` is set and will wait for the first command to be called before connecting. |
772
+ | wait | emits when `lazyConnect` is set and will wait for the first command to be called before connecting. |
745
773
 
746
774
  You can also check out the `Redis#status` property to get the current connection status.
747
775
 
@@ -4,7 +4,6 @@ const util_1 = require("./util");
4
4
  const utils_1 = require("../utils");
5
5
  const redis_1 = require("../redis");
6
6
  const debug = utils_1.Debug("cluster:subscriber");
7
- const SUBSCRIBER_CONNECTION_NAME = "ioredisClusterSubscriber";
8
7
  class ClusterSubscriber {
9
8
  constructor(connectionPool, emitter) {
10
9
  this.connectionPool = connectionPool;
@@ -38,6 +37,9 @@ class ClusterSubscriber {
38
37
  if (lastActiveSubscriber) {
39
38
  lastActiveSubscriber.disconnect();
40
39
  }
40
+ if (this.subscriber) {
41
+ this.subscriber.disconnect();
42
+ }
41
43
  const sampleNode = utils_1.sample(this.connectionPool.getNodes());
42
44
  if (!sampleNode) {
43
45
  debug("selecting subscriber failed since there is no node discovered in the cluster yet");
@@ -61,7 +63,7 @@ class ClusterSubscriber {
61
63
  username: options.username,
62
64
  password: options.password,
63
65
  enableReadyCheck: true,
64
- connectionName: SUBSCRIBER_CONNECTION_NAME,
66
+ connectionName: util_1.getConnectionName("subscriber", options.connectionName),
65
67
  lazyConnect: true,
66
68
  tls: options.tls,
67
69
  });
@@ -90,7 +92,10 @@ class ClusterSubscriber {
90
92
  this.lastActiveSubscriber = this.subscriber;
91
93
  }
92
94
  })
93
- .catch(utils_1.noop);
95
+ .catch(() => {
96
+ // TODO: should probably disconnect the subscriber and try again.
97
+ debug("failed to %s %d channels", type, channels.length);
98
+ });
94
99
  }
95
100
  }
96
101
  }
@@ -652,7 +652,7 @@ class Cluster extends events_1.EventEmitter {
652
652
  enableOfflineQueue: true,
653
653
  enableReadyCheck: false,
654
654
  retryStrategy: null,
655
- connectionName: "ioredisClusterRefresher",
655
+ connectionName: util_1.getConnectionName("refresher", this.options.redisOptions && this.options.redisOptions.connectionName),
656
656
  });
657
657
  // Ignore error events since we will handle
658
658
  // exceptions for the CLUSTER SLOTS command.
@@ -92,3 +92,8 @@ function weightSrvRecords(recordsGroup) {
92
92
  }
93
93
  }
94
94
  exports.weightSrvRecords = weightSrvRecords;
95
+ function getConnectionName(component, nodeConnectionName) {
96
+ const prefix = `ioredis-cluster(${component})`;
97
+ return nodeConnectionName ? `${prefix}:${nodeConnectionName}` : prefix;
98
+ }
99
+ exports.getConnectionName = getConnectionName;
package/built/command.js CHANGED
@@ -46,6 +46,7 @@ class Command {
46
46
  this.transformed = false;
47
47
  this.isCustomCommand = false;
48
48
  this.inTransaction = false;
49
+ this.isResolved = false;
49
50
  this.replyEncoding = options.replyEncoding;
50
51
  this.errorStack = options.errorStack;
51
52
  this.args = lodash_1.flatten(args);
@@ -220,6 +221,7 @@ class Command {
220
221
  return (value) => {
221
222
  try {
222
223
  resolve(this.transformReply(value));
224
+ this.isResolved = true;
223
225
  }
224
226
  catch (err) {
225
227
  this.reject(err);
@@ -1,8 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("../utils");
4
+ const debug = utils_1.Debug("AbstractConnector");
3
5
  class AbstractConnector {
4
- constructor() {
6
+ constructor(disconnectTimeout) {
5
7
  this.connecting = false;
8
+ this.disconnectTimeout = disconnectTimeout;
6
9
  }
7
10
  check(info) {
8
11
  return true;
@@ -10,7 +13,13 @@ class AbstractConnector {
10
13
  disconnect() {
11
14
  this.connecting = false;
12
15
  if (this.stream) {
13
- this.stream.end();
16
+ const stream = this.stream; // Make sure callbacks refer to the same instance
17
+ const timeout = setTimeout(() => {
18
+ debug("stream %s:%s still open, destroying it", stream.remoteAddress, stream.remotePort);
19
+ stream.destroy();
20
+ }, this.disconnectTimeout);
21
+ stream.on("close", () => clearTimeout(timeout));
22
+ stream.end();
14
23
  }
15
24
  }
16
25
  }
@@ -1,4 +1,13 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  Object.defineProperty(exports, "__esModule", { value: true });
3
12
  const net_1 = require("net");
4
13
  const utils_1 = require("../../utils");
@@ -11,7 +20,7 @@ const redis_1 = require("../../redis");
11
20
  const debug = utils_1.Debug("SentinelConnector");
12
21
  class SentinelConnector extends AbstractConnector_1.default {
13
22
  constructor(options) {
14
- super();
23
+ super(options.disconnectTimeout);
15
24
  this.options = options;
16
25
  if (!this.options.sentinels.length) {
17
26
  throw new Error("Requires at least one sentinel to connect to.");
@@ -38,7 +47,7 @@ class SentinelConnector extends AbstractConnector_1.default {
38
47
  this.connecting = true;
39
48
  this.retryAttempts = 0;
40
49
  let lastError;
41
- const connectToNext = () => new Promise((resolve, reject) => {
50
+ const connectToNext = () => __awaiter(this, void 0, void 0, function* () {
42
51
  const endpoint = this.sentinelIterator.next();
43
52
  if (endpoint.done) {
44
53
  this.sentinelIterator.reset(false);
@@ -54,69 +63,69 @@ class SentinelConnector extends AbstractConnector_1.default {
54
63
  debug(errorMsg);
55
64
  const error = new Error(errorMsg);
56
65
  if (typeof retryDelay === "number") {
57
- setTimeout(() => {
58
- resolve(connectToNext());
59
- }, retryDelay);
60
66
  eventEmitter("error", error);
67
+ yield new Promise((resolve) => setTimeout(resolve, retryDelay));
68
+ return connectToNext();
61
69
  }
62
70
  else {
63
- reject(error);
71
+ throw error;
64
72
  }
65
- return;
66
73
  }
67
- this.resolve(endpoint.value, (err, resolved) => {
68
- if (!this.connecting) {
69
- reject(new Error(utils_1.CONNECTION_CLOSED_ERROR_MSG));
70
- return;
71
- }
72
- if (resolved) {
73
- debug("resolved: %s:%s", resolved.host, resolved.port);
74
- if (this.options.enableTLSForSentinelMode && this.options.tls) {
75
- Object.assign(resolved, this.options.tls);
76
- this.stream = tls_1.connect(resolved);
77
- }
78
- else {
79
- this.stream = net_1.createConnection(resolved);
80
- }
81
- this.stream.once("error", (err) => {
82
- this.firstError = err;
83
- });
84
- this.sentinelIterator.reset(true);
85
- resolve(this.stream);
74
+ let resolved = null;
75
+ let err = null;
76
+ try {
77
+ resolved = yield this.resolve(endpoint.value);
78
+ }
79
+ catch (error) {
80
+ err = error;
81
+ }
82
+ if (!this.connecting) {
83
+ throw new Error(utils_1.CONNECTION_CLOSED_ERROR_MSG);
84
+ }
85
+ if (resolved) {
86
+ debug("resolved: %s:%s", resolved.host, resolved.port);
87
+ if (this.options.enableTLSForSentinelMode && this.options.tls) {
88
+ Object.assign(resolved, this.options.tls);
89
+ this.stream = tls_1.connect(resolved);
86
90
  }
87
91
  else {
88
- const endpointAddress = endpoint.value.host + ":" + endpoint.value.port;
89
- const errorMsg = err
90
- ? "failed to connect to sentinel " +
91
- endpointAddress +
92
- " because " +
93
- err.message
94
- : "connected to sentinel " +
95
- endpointAddress +
96
- " successfully, but got an invalid reply: " +
97
- resolved;
98
- debug(errorMsg);
99
- eventEmitter("sentinelError", new Error(errorMsg));
100
- if (err) {
101
- lastError = err;
102
- }
103
- resolve(connectToNext());
92
+ this.stream = net_1.createConnection(resolved);
104
93
  }
105
- });
94
+ this.stream.once("error", (err) => {
95
+ this.firstError = err;
96
+ });
97
+ this.sentinelIterator.reset(true);
98
+ return this.stream;
99
+ }
100
+ else {
101
+ const endpointAddress = endpoint.value.host + ":" + endpoint.value.port;
102
+ const errorMsg = err
103
+ ? "failed to connect to sentinel " +
104
+ endpointAddress +
105
+ " because " +
106
+ err.message
107
+ : "connected to sentinel " +
108
+ endpointAddress +
109
+ " successfully, but got an invalid reply: " +
110
+ resolved;
111
+ debug(errorMsg);
112
+ eventEmitter("sentinelError", new Error(errorMsg));
113
+ if (err) {
114
+ lastError = err;
115
+ }
116
+ return connectToNext();
117
+ }
106
118
  });
107
119
  return connectToNext();
108
120
  }
109
- updateSentinels(client, callback) {
110
- if (!this.options.updateSentinels) {
111
- return callback(null);
112
- }
113
- client.sentinel("sentinels", this.options.name, (err, result) => {
114
- if (err) {
115
- client.disconnect();
116
- return callback(err);
121
+ updateSentinels(client) {
122
+ return __awaiter(this, void 0, void 0, function* () {
123
+ if (!this.options.updateSentinels) {
124
+ return;
117
125
  }
126
+ const result = yield client.sentinel("sentinels", this.options.name);
118
127
  if (!Array.isArray(result)) {
119
- return callback(null);
128
+ return;
120
129
  }
121
130
  result
122
131
  .map(utils_1.packObject)
@@ -132,39 +141,27 @@ class SentinelConnector extends AbstractConnector_1.default {
132
141
  }
133
142
  });
134
143
  debug("Updated internal sentinels: %s", this.sentinelIterator);
135
- callback(null);
136
144
  });
137
145
  }
138
- resolveMaster(client, callback) {
139
- client.sentinel("get-master-addr-by-name", this.options.name, (err, result) => {
140
- if (err) {
141
- client.disconnect();
142
- return callback(err);
143
- }
144
- this.updateSentinels(client, (err) => {
145
- client.disconnect();
146
- if (err) {
147
- return callback(err);
148
- }
149
- callback(null, this.sentinelNatResolve(Array.isArray(result)
150
- ? { host: result[0], port: Number(result[1]) }
151
- : null));
152
- });
146
+ resolveMaster(client) {
147
+ return __awaiter(this, void 0, void 0, function* () {
148
+ const result = yield client.sentinel("get-master-addr-by-name", this.options.name);
149
+ yield this.updateSentinels(client);
150
+ return this.sentinelNatResolve(Array.isArray(result)
151
+ ? { host: result[0], port: Number(result[1]) }
152
+ : null);
153
153
  });
154
154
  }
155
- resolveSlave(client, callback) {
156
- client.sentinel("slaves", this.options.name, (err, result) => {
157
- client.disconnect();
158
- if (err) {
159
- return callback(err);
160
- }
155
+ resolveSlave(client) {
156
+ return __awaiter(this, void 0, void 0, function* () {
157
+ const result = yield client.sentinel("slaves", this.options.name);
161
158
  if (!Array.isArray(result)) {
162
- return callback(null, null);
159
+ return null;
163
160
  }
164
161
  const availableSlaves = result
165
162
  .map(utils_1.packObject)
166
163
  .filter((slave) => slave.flags && !slave.flags.match(/(disconnected|s_down|o_down)/));
167
- callback(null, this.sentinelNatResolve(selectPreferredSentinel(availableSlaves, this.options.preferredSlaves)));
164
+ return this.sentinelNatResolve(selectPreferredSentinel(availableSlaves, this.options.preferredSlaves));
168
165
  });
169
166
  }
170
167
  sentinelNatResolve(item) {
@@ -172,30 +169,38 @@ class SentinelConnector extends AbstractConnector_1.default {
172
169
  return item;
173
170
  return this.options.natMap[`${item.host}:${item.port}`] || item;
174
171
  }
175
- resolve(endpoint, callback) {
176
- const client = new redis_1.default({
177
- port: endpoint.port || 26379,
178
- host: endpoint.host,
179
- username: this.options.sentinelUsername || null,
180
- password: this.options.sentinelPassword || null,
181
- family: endpoint.family ||
182
- (StandaloneConnector_1.isIIpcConnectionOptions(this.options)
183
- ? undefined
184
- : this.options.family),
185
- tls: this.options.sentinelTLS,
186
- retryStrategy: null,
187
- enableReadyCheck: false,
188
- connectTimeout: this.options.connectTimeout,
189
- dropBufferSupport: true,
172
+ resolve(endpoint) {
173
+ return __awaiter(this, void 0, void 0, function* () {
174
+ const client = new redis_1.default({
175
+ port: endpoint.port || 26379,
176
+ host: endpoint.host,
177
+ username: this.options.sentinelUsername || null,
178
+ password: this.options.sentinelPassword || null,
179
+ family: endpoint.family ||
180
+ (StandaloneConnector_1.isIIpcConnectionOptions(this.options)
181
+ ? undefined
182
+ : this.options.family),
183
+ tls: this.options.sentinelTLS,
184
+ retryStrategy: null,
185
+ enableReadyCheck: false,
186
+ connectTimeout: this.options.connectTimeout,
187
+ commandTimeout: this.options.sentinelCommandTimeout,
188
+ dropBufferSupport: true,
189
+ });
190
+ // ignore the errors since resolve* methods will handle them
191
+ client.on("error", noop);
192
+ try {
193
+ if (this.options.role === "slave") {
194
+ return yield this.resolveSlave(client);
195
+ }
196
+ else {
197
+ return yield this.resolveMaster(client);
198
+ }
199
+ }
200
+ finally {
201
+ client.disconnect();
202
+ }
190
203
  });
191
- // ignore the errors since resolve* methods will handle them
192
- client.on("error", noop);
193
- if (this.options.role === "slave") {
194
- this.resolveSlave(client, callback);
195
- }
196
- else {
197
- this.resolveMaster(client, callback);
198
- }
199
204
  }
200
205
  }
201
206
  exports.default = SentinelConnector;
@@ -10,7 +10,7 @@ function isIIpcConnectionOptions(value) {
10
10
  exports.isIIpcConnectionOptions = isIIpcConnectionOptions;
11
11
  class StandaloneConnector extends AbstractConnector_1.default {
12
12
  constructor(options) {
13
- super();
13
+ super(options.disconnectTimeout);
14
14
  this.options = options;
15
15
  }
16
16
  connect(_) {
@@ -6,6 +6,7 @@ exports.DEFAULT_REDIS_OPTIONS = {
6
6
  host: "localhost",
7
7
  family: 4,
8
8
  connectTimeout: 10000,
9
+ disconnectTimeout: 2000,
9
10
  retryStrategy: function (times) {
10
11
  return Math.min(times * 50, 2000);
11
12
  },
@@ -36,7 +36,11 @@ function connectHandler(self) {
36
36
  });
37
37
  }
38
38
  if (self.condition.select) {
39
- self.select(self.condition.select);
39
+ self.select(self.condition.select).catch((err) => {
40
+ // If the node is in cluster mode, select is disallowed.
41
+ // In this case, reconnect won't help.
42
+ self.silentEmit("error", err);
43
+ });
40
44
  }
41
45
  if (!self.options.enableReadyCheck) {
42
46
  exports.readyHandler(self)();
@@ -623,6 +623,13 @@ Redis.prototype.sendCommand = function (command, stream) {
623
623
  command.reject(new Error("Connection in subscriber mode, only subscriber commands may be used"));
624
624
  return command.promise;
625
625
  }
626
+ if (typeof this.options.commandTimeout === "number") {
627
+ setTimeout(() => {
628
+ if (!command.isResolved) {
629
+ command.reject(new Error("Command timed out"));
630
+ }
631
+ }, this.options.commandTimeout);
632
+ }
626
633
  if (command.name === "quit") {
627
634
  clearInterval(this._addedScriptHashesCleanInterval);
628
635
  this._addedScriptHashesCleanInterval = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ioredis",
3
- "version": "4.24.4",
3
+ "version": "4.26.0",
4
4
  "description": "A robust, performance-focused and full-featured Redis client for Node.js.",
5
5
  "main": "built/index.js",
6
6
  "files": [
@@ -58,17 +58,17 @@
58
58
  "@types/redis-errors": "1.2.0",
59
59
  "@types/sinon": "^9.0.0",
60
60
  "@types/uuid": "^8.3.0",
61
- "@typescript-eslint/eslint-plugin": "^2.26.0",
61
+ "@typescript-eslint/eslint-plugin": "^1.13.0",
62
62
  "@typescript-eslint/parser": "^2.26.0",
63
63
  "bluebird": "^3.7.2",
64
64
  "chai": "^4.2.0",
65
65
  "chai-as-promised": "^7.1.1",
66
66
  "cronometro": "^0.6.0",
67
67
  "cz-conventional-changelog": "^3.1.0",
68
- "eslint": "^6.8.0",
68
+ "eslint": "^5.16.0",
69
69
  "eslint-config-prettier": "^6.10.1",
70
70
  "husky": "^4.2.3",
71
- "mocha": "^7.1.1",
71
+ "mocha": "^6.2.3",
72
72
  "prettier": "^2.0.2",
73
73
  "pretty-quick": "^2.0.1",
74
74
  "server-destroy": "^1.0.1",