From 6cacd17e6ac4d9f995728ee09777e0a7f3b739d7 Mon Sep 17 00:00:00 2001 From: Andres Kalle Date: Wed, 31 Mar 2021 18:47:17 +0300 Subject: [PATCH] fix: force disconnect after a timeout if socket is still half-open (#1318) --- API.md | 73 ++++++++++++----------- lib/connectors/AbstractConnector.ts | 24 +++++++- lib/connectors/SentinelConnector/index.ts | 3 +- lib/connectors/StandaloneConnector.ts | 11 ++-- lib/redis/RedisOptions.ts | 1 + 5 files changed, 70 insertions(+), 42 deletions(-) diff --git a/API.md b/API.md index 5abaafee..39f84c7c 100644 --- a/API.md +++ b/API.md @@ -13,7 +13,7 @@ ## Redis ⇐ [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) -**Kind**: global class +**Kind**: global class **Extends**: [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter), [Commander](#Commander) - [Redis](#Redis) ⇐ [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) @@ -55,6 +55,7 @@ Creates a Redis instance | [options.enableReadyCheck] | boolean | true | 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. | | [options.enableOfflineQueue] | boolean | true | 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. | | [options.connectTimeout] | number | 10000 | The milliseconds before a timeout occurs during the initial connection to the Redis server. | +| [options.disconnectTimeout] | number | 2000 | 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. | | [options.autoResubscribe] | boolean | true | After reconnected, if the previous connection was in the subscriber mode, client will auto re-subscribe these channels. | | [options.autoResendUnfulfilledCommands] | boolean | true | If true, client will resend unfulfilled commands(e.g. block commands) in the previous connection when reconnected. | | [options.lazyConnect] | boolean | false | 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. When calling this method manually, a Promise is returned, which will be resolved when the connection status is ready. -**Kind**: instance method of [Redis](#Redis) +**Kind**: instance method of [Redis](#Redis) **Access**: public | Param | Type | @@ -113,8 +114,8 @@ This method closes the connection immediately, and may lose some pending replies that haven't written to client. If you want to wait for the pending replies, use Redis#quit instead. -**Kind**: instance method of [Redis](#Redis) -**Access**: public +**Kind**: instance method of [Redis](#Redis) +**Access**: public ### ~~redis.end()~~ @@ -123,15 +124,15 @@ If you want to wait for the pending replies, use Redis#quit instead. Disconnect from Redis. -**Kind**: instance method of [Redis](#Redis) +**Kind**: instance method of [Redis](#Redis) ### redis.duplicate() Create a new instance with the same options as the current one. -**Kind**: instance method of [Redis](#Redis) -**Access**: public +**Kind**: instance method of [Redis](#Redis) +**Access**: public **Example** ```js @@ -149,7 +150,7 @@ This command will create a new connection to Redis and send a MONITOR command via the new connection in order to avoid disturbing the current connection. -**Kind**: instance method of [Redis](#Redis) +**Kind**: instance method of [Redis](#Redis) **Access**: public | Param | Type | Description | @@ -181,17 +182,17 @@ redis.monitor().then(function (monitor) { Return supported builtin commands -**Kind**: instance method of [Redis](#Redis) -**Returns**: Array.<string> - command list -**Access**: public +**Kind**: instance method of [Redis](#Redis) +**Returns**: Array.<string> - command list +**Access**: public ### redis.createBuiltinCommand(commandName) ⇒ object Create a builtin command -**Kind**: instance method of [Redis](#Redis) -**Returns**: object - functions +**Kind**: instance method of [Redis](#Redis) +**Returns**: object - functions **Access**: public | Param | Type | Description | @@ -221,12 +222,12 @@ Define a custom command using lua script Create a Redis instance -**Kind**: static method of [Redis](#Redis) +**Kind**: static method of [Redis](#Redis) ## Cluster ⇐ [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) -**Kind**: global class +**Kind**: global class **Extends**: [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter), [Commander](#Commander) - [Cluster](#Cluster) ⇐ [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) @@ -269,15 +270,15 @@ Creates a Redis Cluster instance Connect to a cluster -**Kind**: instance method of [Cluster](#Cluster) -**Access**: public +**Kind**: instance method of [Cluster](#Cluster) +**Access**: public ### cluster.disconnect([reconnect]) Disconnect from every node in the cluster. -**Kind**: instance method of [Cluster](#Cluster) +**Kind**: instance method of [Cluster](#Cluster) **Access**: public | Param | Type | @@ -290,8 +291,8 @@ Disconnect from every node in the cluster. Quit the cluster gracefully. -**Kind**: instance method of [Cluster](#Cluster) -**Returns**: Promise - return 'OK' if successfully +**Kind**: instance method of [Cluster](#Cluster) +**Returns**: Promise - return 'OK' if successfully **Access**: public | Param | Type | @@ -304,8 +305,8 @@ Quit the cluster gracefully. Get nodes with the specified role -**Kind**: instance method of [Cluster](#Cluster) -**Returns**: [Array.<Redis>](#Redis) - array of nodes +**Kind**: instance method of [Cluster](#Cluster) +**Returns**: [Array.<Redis>](#Redis) - array of nodes **Access**: public | Param | Type | Default | Description | @@ -318,17 +319,17 @@ Get nodes with the specified role Return supported builtin commands -**Kind**: instance method of [Cluster](#Cluster) -**Returns**: Array.<string> - command list -**Access**: public +**Kind**: instance method of [Cluster](#Cluster) +**Returns**: Array.<string> - command list +**Access**: public ### cluster.createBuiltinCommand(commandName) ⇒ object Create a builtin command -**Kind**: instance method of [Cluster](#Cluster) -**Returns**: object - functions +**Kind**: instance method of [Cluster](#Cluster) +**Returns**: object - functions **Access**: public | Param | Type | Description | @@ -356,9 +357,9 @@ Define a custom command using lua script Send a command -**Kind**: instance abstract method of [Cluster](#Cluster) -**Overrides**: [sendCommand](#Commander+sendCommand) -**Access**: public +**Kind**: instance abstract method of [Cluster](#Cluster) +**Overrides**: [sendCommand](#Commander+sendCommand) +**Access**: public ## Commander @@ -390,17 +391,17 @@ This is the base class of Redis, Redis.Cluster and Pipeline Return supported builtin commands -**Kind**: instance method of [Commander](#Commander) -**Returns**: Array.<string> - command list -**Access**: public +**Kind**: instance method of [Commander](#Commander) +**Returns**: Array.<string> - command list +**Access**: public ### commander.createBuiltinCommand(commandName) ⇒ object Create a builtin command -**Kind**: instance method of [Commander](#Commander) -**Returns**: object - functions +**Kind**: instance method of [Commander](#Commander) +**Returns**: object - functions **Access**: public | Param | Type | Description | @@ -428,5 +429,5 @@ Define a custom command using lua script Send a command -**Kind**: instance abstract method of [Commander](#Commander) +**Kind**: instance abstract method of [Commander](#Commander) **Access**: public diff --git a/lib/connectors/AbstractConnector.ts b/lib/connectors/AbstractConnector.ts index 276c3313..a69cd473 100644 --- a/lib/connectors/AbstractConnector.ts +++ b/lib/connectors/AbstractConnector.ts @@ -1,20 +1,42 @@ import { NetStream } from "../types"; +import { Debug } from "../utils"; + +const debug = Debug("AbstractConnector"); export type ErrorEmitter = (type: string, err: Error) => void; export default abstract class AbstractConnector { + private disconnectTimeout: number; protected connecting = false; protected stream: NetStream; public firstError?: Error; + protected constructor(disconnectTimeout: number) { + this.disconnectTimeout = disconnectTimeout; + } + public check(info: any): boolean { return true; } public disconnect(): void { this.connecting = false; + if (this.stream) { - this.stream.end(); + const stream = this.stream; // Make sure callbacks refer to the same instance + + const timeout = setTimeout(() => { + debug( + "stream %s:%s still open, destroying it", + stream.remoteAddress, + stream.remotePort + ); + + stream.destroy(); + }, this.disconnectTimeout); + + stream.on("close", () => clearTimeout(timeout)); + stream.end(); } } diff --git a/lib/connectors/SentinelConnector/index.ts b/lib/connectors/SentinelConnector/index.ts index 090fc06d..e3b4d882 100644 --- a/lib/connectors/SentinelConnector/index.ts +++ b/lib/connectors/SentinelConnector/index.ts @@ -41,6 +41,7 @@ export interface ISentinelConnectionOptions extends ITcpConnectionOptions { sentinelRetryStrategy?: (retryAttempts: number) => number | void | null; preferredSlaves?: PreferredSlaves; connectTimeout?: number; + disconnectTimeout?: number; enableTLSForSentinelMode?: boolean; sentinelTLS?: ConnectionOptions; natMap?: INatMap; @@ -52,7 +53,7 @@ export default class SentinelConnector extends AbstractConnector { protected sentinelIterator: SentinelIterator; constructor(protected options: ISentinelConnectionOptions) { - super(); + super(options.disconnectTimeout); if (!this.options.sentinels.length) { throw new Error("Requires at least one sentinel to connect to."); diff --git a/lib/connectors/StandaloneConnector.ts b/lib/connectors/StandaloneConnector.ts index 30e761c0..ac38a846 100644 --- a/lib/connectors/StandaloneConnector.ts +++ b/lib/connectors/StandaloneConnector.ts @@ -18,11 +18,14 @@ export interface IIpcConnectionOptions extends IpcNetConnectOpts { tls?: ConnectionOptions; } +type IStandaloneConnectionOptions = ( + | ITcpConnectionOptions + | IIpcConnectionOptions +) & { disconnectTimeout: number }; + export default class StandaloneConnector extends AbstractConnector { - constructor( - protected options: ITcpConnectionOptions | IIpcConnectionOptions - ) { - super(); + constructor(protected options: IStandaloneConnectionOptions) { + super(options.disconnectTimeout); } public connect(_: ErrorEmitter) { diff --git a/lib/redis/RedisOptions.ts b/lib/redis/RedisOptions.ts index 1032499f..bb4535e2 100644 --- a/lib/redis/RedisOptions.ts +++ b/lib/redis/RedisOptions.ts @@ -37,6 +37,7 @@ export const DEFAULT_REDIS_OPTIONS: IRedisOptions = { host: "localhost", family: 4, connectTimeout: 10000, + disconnectTimeout: 2000, retryStrategy: function (times) { return Math.min(times * 50, 2000); },