Skip to content

Commit 6cacd17

Browse files
authored
fix: force disconnect after a timeout if socket is still half-open (#1318)
1 parent caa12f8 commit 6cacd17

File tree

5 files changed

+70
-42
lines changed

5 files changed

+70
-42
lines changed

API.md

+37-36
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
## Redis ⇐ <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code>
1515

16-
**Kind**: global class
16+
**Kind**: global class
1717
**Extends**: <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code>, [<code>Commander</code>](#Commander)
1818

1919
- [Redis](#Redis) ⇐ <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code>
@@ -55,6 +55,7 @@ Creates a Redis instance
5555
| [options.enableReadyCheck] | <code>boolean</code> | <code>true</code> | When a connection is established to the Redis server, the server might still be loading the database from disk. While loading, the server not respond to any commands. To work around this, when this option is `true`, ioredis will check the status of the Redis server, and when the Redis server is able to process commands, a `ready` event will be emitted. |
5656
| [options.enableOfflineQueue] | <code>boolean</code> | <code>true</code> | By default, if there is no active connection to the Redis server, commands are added to a queue and are executed once the connection is "ready" (when `enableReadyCheck` is `true`, "ready" means the Redis server has loaded the database from disk, otherwise means the connection to the Redis server has been established). If this option is false, when execute the command when the connection isn't ready, an error will be returned. |
5757
| [options.connectTimeout] | <code>number</code> | <code>10000</code> | The milliseconds before a timeout occurs during the initial connection to the Redis server. |
58+
| [options.disconnectTimeout] | <code>number</code> | <code>2000</code> | The milliseconds before [socket.destroy()](https://nodejs.org/dist/latest-v14.x/docs/api/net.html#net_socket_destroy_error) is called after [socket.end()](https://nodejs.org/dist/latest-v14.x/docs/api/net.html#net_socket_end_data_encoding_callback) if the connection remains half-open during disconnection. |
5859
| [options.autoResubscribe] | <code>boolean</code> | <code>true</code> | After reconnected, if the previous connection was in the subscriber mode, client will auto re-subscribe these channels. |
5960
| [options.autoResendUnfulfilledCommands] | <code>boolean</code> | <code>true</code> | If true, client will resend unfulfilled commands(e.g. block commands) in the previous connection when reconnected. |
6061
| [options.lazyConnect] | <code>boolean</code> | <code>false</code> | By default, When a new `Redis` instance is created, it will connect to Redis server automatically. If you want to keep the instance disconnected until a command is called, you can pass the `lazyConnect` option to the constructor: `javascript var redis = new Redis({ lazyConnect: true }); // No attempting to connect to the Redis server here. // Now let's connect to the Redis server redis.get('foo', function () { });` |
@@ -96,7 +97,7 @@ unless `lazyConnect: true` is passed.
9697
When calling this method manually, a Promise is returned, which will
9798
be resolved when the connection status is ready.
9899

99-
**Kind**: instance method of [<code>Redis</code>](#Redis)
100+
**Kind**: instance method of [<code>Redis</code>](#Redis)
100101
**Access**: public
101102

102103
| Param | Type |
@@ -113,8 +114,8 @@ This method closes the connection immediately,
113114
and may lose some pending replies that haven't written to client.
114115
If you want to wait for the pending replies, use Redis#quit instead.
115116

116-
**Kind**: instance method of [<code>Redis</code>](#Redis)
117-
**Access**: public
117+
**Kind**: instance method of [<code>Redis</code>](#Redis)
118+
**Access**: public
118119
<a name="Redis+end"></a>
119120

120121
### ~~redis.end()~~
@@ -123,15 +124,15 @@ If you want to wait for the pending replies, use Redis#quit instead.
123124

124125
Disconnect from Redis.
125126

126-
**Kind**: instance method of [<code>Redis</code>](#Redis)
127+
**Kind**: instance method of [<code>Redis</code>](#Redis)
127128
<a name="Redis+duplicate"></a>
128129

129130
### redis.duplicate()
130131

131132
Create a new instance with the same options as the current one.
132133

133-
**Kind**: instance method of [<code>Redis</code>](#Redis)
134-
**Access**: public
134+
**Kind**: instance method of [<code>Redis</code>](#Redis)
135+
**Access**: public
135136
**Example**
136137

137138
```js
@@ -149,7 +150,7 @@ This command will create a new connection to Redis and send a
149150
MONITOR command via the new connection in order to avoid disturbing
150151
the current connection.
151152

152-
**Kind**: instance method of [<code>Redis</code>](#Redis)
153+
**Kind**: instance method of [<code>Redis</code>](#Redis)
153154
**Access**: public
154155

155156
| Param | Type | Description |
@@ -181,17 +182,17 @@ redis.monitor().then(function (monitor) {
181182

182183
Return supported builtin commands
183184

184-
**Kind**: instance method of [<code>Redis</code>](#Redis)
185-
**Returns**: <code>Array.&lt;string&gt;</code> - command list
186-
**Access**: public
185+
**Kind**: instance method of [<code>Redis</code>](#Redis)
186+
**Returns**: <code>Array.&lt;string&gt;</code> - command list
187+
**Access**: public
187188
<a name="Commander+createBuiltinCommand"></a>
188189

189190
### redis.createBuiltinCommand(commandName) ⇒ <code>object</code>
190191

191192
Create a builtin command
192193

193-
**Kind**: instance method of [<code>Redis</code>](#Redis)
194-
**Returns**: <code>object</code> - functions
194+
**Kind**: instance method of [<code>Redis</code>](#Redis)
195+
**Returns**: <code>object</code> - functions
195196
**Access**: public
196197

197198
| Param | Type | Description |
@@ -221,12 +222,12 @@ Define a custom command using lua script
221222

222223
Create a Redis instance
223224

224-
**Kind**: static method of [<code>Redis</code>](#Redis)
225+
**Kind**: static method of [<code>Redis</code>](#Redis)
225226
<a name="Cluster"></a>
226227

227228
## Cluster ⇐ <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code>
228229

229-
**Kind**: global class
230+
**Kind**: global class
230231
**Extends**: <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code>, [<code>Commander</code>](#Commander)
231232

232233
- [Cluster](#Cluster) ⇐ <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code>
@@ -269,15 +270,15 @@ Creates a Redis Cluster instance
269270

270271
Connect to a cluster
271272

272-
**Kind**: instance method of [<code>Cluster</code>](#Cluster)
273-
**Access**: public
273+
**Kind**: instance method of [<code>Cluster</code>](#Cluster)
274+
**Access**: public
274275
<a name="Cluster+disconnect"></a>
275276

276277
### cluster.disconnect([reconnect])
277278

278279
Disconnect from every node in the cluster.
279280

280-
**Kind**: instance method of [<code>Cluster</code>](#Cluster)
281+
**Kind**: instance method of [<code>Cluster</code>](#Cluster)
281282
**Access**: public
282283

283284
| Param | Type |
@@ -290,8 +291,8 @@ Disconnect from every node in the cluster.
290291

291292
Quit the cluster gracefully.
292293

293-
**Kind**: instance method of [<code>Cluster</code>](#Cluster)
294-
**Returns**: <code>Promise</code> - return 'OK' if successfully
294+
**Kind**: instance method of [<code>Cluster</code>](#Cluster)
295+
**Returns**: <code>Promise</code> - return 'OK' if successfully
295296
**Access**: public
296297

297298
| Param | Type |
@@ -304,8 +305,8 @@ Quit the cluster gracefully.
304305

305306
Get nodes with the specified role
306307

307-
**Kind**: instance method of [<code>Cluster</code>](#Cluster)
308-
**Returns**: [<code>Array.&lt;Redis&gt;</code>](#Redis) - array of nodes
308+
**Kind**: instance method of [<code>Cluster</code>](#Cluster)
309+
**Returns**: [<code>Array.&lt;Redis&gt;</code>](#Redis) - array of nodes
309310
**Access**: public
310311

311312
| Param | Type | Default | Description |
@@ -318,17 +319,17 @@ Get nodes with the specified role
318319

319320
Return supported builtin commands
320321

321-
**Kind**: instance method of [<code>Cluster</code>](#Cluster)
322-
**Returns**: <code>Array.&lt;string&gt;</code> - command list
323-
**Access**: public
322+
**Kind**: instance method of [<code>Cluster</code>](#Cluster)
323+
**Returns**: <code>Array.&lt;string&gt;</code> - command list
324+
**Access**: public
324325
<a name="Commander+createBuiltinCommand"></a>
325326

326327
### cluster.createBuiltinCommand(commandName) ⇒ <code>object</code>
327328

328329
Create a builtin command
329330

330-
**Kind**: instance method of [<code>Cluster</code>](#Cluster)
331-
**Returns**: <code>object</code> - functions
331+
**Kind**: instance method of [<code>Cluster</code>](#Cluster)
332+
**Returns**: <code>object</code> - functions
332333
**Access**: public
333334

334335
| Param | Type | Description |
@@ -356,9 +357,9 @@ Define a custom command using lua script
356357

357358
Send a command
358359

359-
**Kind**: instance abstract method of [<code>Cluster</code>](#Cluster)
360-
**Overrides**: [<code>sendCommand</code>](#Commander+sendCommand)
361-
**Access**: public
360+
**Kind**: instance abstract method of [<code>Cluster</code>](#Cluster)
361+
**Overrides**: [<code>sendCommand</code>](#Commander+sendCommand)
362+
**Access**: public
362363
<a name="Commander"></a>
363364

364365
## Commander
@@ -390,17 +391,17 @@ This is the base class of Redis, Redis.Cluster and Pipeline
390391

391392
Return supported builtin commands
392393

393-
**Kind**: instance method of [<code>Commander</code>](#Commander)
394-
**Returns**: <code>Array.&lt;string&gt;</code> - command list
395-
**Access**: public
394+
**Kind**: instance method of [<code>Commander</code>](#Commander)
395+
**Returns**: <code>Array.&lt;string&gt;</code> - command list
396+
**Access**: public
396397
<a name="Commander+createBuiltinCommand"></a>
397398

398399
### commander.createBuiltinCommand(commandName) ⇒ <code>object</code>
399400

400401
Create a builtin command
401402

402-
**Kind**: instance method of [<code>Commander</code>](#Commander)
403-
**Returns**: <code>object</code> - functions
403+
**Kind**: instance method of [<code>Commander</code>](#Commander)
404+
**Returns**: <code>object</code> - functions
404405
**Access**: public
405406

406407
| Param | Type | Description |
@@ -428,5 +429,5 @@ Define a custom command using lua script
428429

429430
Send a command
430431

431-
**Kind**: instance abstract method of [<code>Commander</code>](#Commander)
432+
**Kind**: instance abstract method of [<code>Commander</code>](#Commander)
432433
**Access**: public

lib/connectors/AbstractConnector.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,42 @@
11
import { NetStream } from "../types";
2+
import { Debug } from "../utils";
3+
4+
const debug = Debug("AbstractConnector");
25

36
export type ErrorEmitter = (type: string, err: Error) => void;
47

58
export default abstract class AbstractConnector {
9+
private disconnectTimeout: number;
610
protected connecting = false;
711
protected stream: NetStream;
812
public firstError?: Error;
913

14+
protected constructor(disconnectTimeout: number) {
15+
this.disconnectTimeout = disconnectTimeout;
16+
}
17+
1018
public check(info: any): boolean {
1119
return true;
1220
}
1321

1422
public disconnect(): void {
1523
this.connecting = false;
24+
1625
if (this.stream) {
17-
this.stream.end();
26+
const stream = this.stream; // Make sure callbacks refer to the same instance
27+
28+
const timeout = setTimeout(() => {
29+
debug(
30+
"stream %s:%s still open, destroying it",
31+
stream.remoteAddress,
32+
stream.remotePort
33+
);
34+
35+
stream.destroy();
36+
}, this.disconnectTimeout);
37+
38+
stream.on("close", () => clearTimeout(timeout));
39+
stream.end();
1840
}
1941
}
2042

lib/connectors/SentinelConnector/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface ISentinelConnectionOptions extends ITcpConnectionOptions {
4141
sentinelRetryStrategy?: (retryAttempts: number) => number | void | null;
4242
preferredSlaves?: PreferredSlaves;
4343
connectTimeout?: number;
44+
disconnectTimeout?: number;
4445
enableTLSForSentinelMode?: boolean;
4546
sentinelTLS?: ConnectionOptions;
4647
natMap?: INatMap;
@@ -52,7 +53,7 @@ export default class SentinelConnector extends AbstractConnector {
5253
protected sentinelIterator: SentinelIterator;
5354

5455
constructor(protected options: ISentinelConnectionOptions) {
55-
super();
56+
super(options.disconnectTimeout);
5657

5758
if (!this.options.sentinels.length) {
5859
throw new Error("Requires at least one sentinel to connect to.");

lib/connectors/StandaloneConnector.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ export interface IIpcConnectionOptions extends IpcNetConnectOpts {
1818
tls?: ConnectionOptions;
1919
}
2020

21+
type IStandaloneConnectionOptions = (
22+
| ITcpConnectionOptions
23+
| IIpcConnectionOptions
24+
) & { disconnectTimeout: number };
25+
2126
export default class StandaloneConnector extends AbstractConnector {
22-
constructor(
23-
protected options: ITcpConnectionOptions | IIpcConnectionOptions
24-
) {
25-
super();
27+
constructor(protected options: IStandaloneConnectionOptions) {
28+
super(options.disconnectTimeout);
2629
}
2730

2831
public connect(_: ErrorEmitter) {

lib/redis/RedisOptions.ts

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const DEFAULT_REDIS_OPTIONS: IRedisOptions = {
3737
host: "localhost",
3838
family: 4,
3939
connectTimeout: 10000,
40+
disconnectTimeout: 2000,
4041
retryStrategy: function (times) {
4142
return Math.min(times * 50, 2000);
4243
},

0 commit comments

Comments
 (0)