Skip to content

Commit 56f0272

Browse files
mjombleluin
andauthored
feat: added commandTimeout option (#1320)
* Added timeoutPerRequest option * Renamed timeoutPerRequest to commandTimeout * Documented commandTimeout * Added sentinelCommandTimeout option * Add a test case for commandTimeout Co-authored-by: luin <[email protected]>
1 parent 0c129c8 commit 56f0272

File tree

7 files changed

+46
-4
lines changed

7 files changed

+46
-4
lines changed

API.md

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ Creates a Redis instance
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. |
5858
| [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. |
59+
| [options.commandTimeout] | <code>number</code> | | The milliseconds before a timeout occurs when executing a single command. By default, there is no timeout and the client will wait indefinitely. The timeout is enforced only on the client side, not server side. The server may still complete the operation after a timeout error occurs on the client side. |
5960
| [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. |
6061
| [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. |
6162
| [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 () { });` |

lib/command.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ export default class Command implements ICommand {
160160
private slot?: number | null;
161161
private keys?: Array<string | Buffer>;
162162

163+
public isResolved = false;
163164
public reject: (err: Error) => void;
164165
public resolve: (result: any) => void;
165166
public promise: Promise<any>;
@@ -342,6 +343,7 @@ export default class Command implements ICommand {
342343
return (value) => {
343344
try {
344345
resolve(this.transformReply(value));
346+
this.isResolved = true;
345347
} catch (err) {
346348
this.reject(err);
347349
}
@@ -392,7 +394,7 @@ const hsetArgumentTransformer = function (args) {
392394
}
393395
}
394396
return args;
395-
}
397+
};
396398

397399
Command.setArgumentTransformer("mset", msetArgumentTransformer);
398400
Command.setArgumentTransformer("msetnx", msetArgumentTransformer);

lib/connectors/SentinelConnector/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export interface ISentinelConnectionOptions extends ITcpConnectionOptions {
4242
preferredSlaves?: PreferredSlaves;
4343
connectTimeout?: number;
4444
disconnectTimeout?: number;
45+
sentinelCommandTimeout?: number;
4546
enableTLSForSentinelMode?: boolean;
4647
sentinelTLS?: ConnectionOptions;
4748
natMap?: INatMap;
@@ -265,6 +266,7 @@ export default class SentinelConnector extends AbstractConnector {
265266
retryStrategy: null,
266267
enableReadyCheck: false,
267268
connectTimeout: this.options.connectTimeout,
269+
commandTimeout: this.options.sentinelCommandTimeout,
268270
dropBufferSupport: true,
269271
});
270272

lib/redis/RedisOptions.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface IRedisOptions
1111
Partial<IClusterOptions> {
1212
Connector?: typeof AbstractConnector;
1313
retryStrategy?: (times: number) => number | void | null;
14+
commandTimeout?: number;
1415
keepAlive?: number;
1516
noDelay?: boolean;
1617
connectionName?: string;

lib/redis/index.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ const debug = Debug("redis");
9898
* @param {NatMap} [options.natMap=null] NAT map for sentinel connector.
9999
* @param {boolean} [options.updateSentinels=true] - Update the given `sentinels` list with new IP
100100
* addresses when communicating with existing sentinels.
101-
* @param {boolean} [options.enableAutoPipelining=false] - When enabled, all commands issued during an event loop
102-
* iteration are automatically wrapped in a pipeline and sent to the server at the same time.
101+
* @param {boolean} [options.enableAutoPipelining=false] - When enabled, all commands issued during an event loop
102+
* iteration are automatically wrapped in a pipeline and sent to the server at the same time.
103103
* This can dramatically improve performance.
104104
* @param {string[]} [options.autoPipeliningIgnoredCommands=[]] - The list of commands which must not be automatically wrapped in pipelines.
105105
* @param {number} [options.maxScriptsCachingTime=60000] Default script definition caching time.
@@ -710,6 +710,14 @@ Redis.prototype.sendCommand = function (command, stream) {
710710
return command.promise;
711711
}
712712

713+
if (typeof this.options.commandTimeout === "number") {
714+
setTimeout(() => {
715+
if (!command.isResolved) {
716+
command.reject(new Error("Command timed out"));
717+
}
718+
}, this.options.commandTimeout);
719+
}
720+
713721
if (command.name === "quit") {
714722
clearInterval(this._addedScriptHashesCleanInterval);
715723
this._addedScriptHashesCleanInterval = null;

test/functional/commandTimeout.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { expect } from "chai";
2+
import * as sinon from "sinon";
3+
import Redis from "../../lib/redis";
4+
import MockServer from "../helpers/mock_server";
5+
6+
describe("commandTimeout", function () {
7+
it("rejects if command timed out", function (done) {
8+
const server = new MockServer(30001, function (argv, socket, flags) {
9+
if (argv[0] === "hget") {
10+
flags.hang = true;
11+
return;
12+
}
13+
});
14+
15+
const redis = new Redis({ port: 30001, commandTimeout: 1000 });
16+
const clock = sinon.useFakeTimers();
17+
redis.hget("foo", (err) => {
18+
expect(err.message).to.eql("Command timed out");
19+
clock.restore();
20+
redis.disconnect();
21+
server.disconnect(() => done());
22+
});
23+
clock.tick(1000);
24+
});
25+
});

test/helpers/mock_server.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export function getConnectionName(socket: Socket): string | undefined {
3434

3535
interface IFlags {
3636
disconnect?: boolean;
37+
hang?: boolean;
3738
}
3839
export type MockServerHandler = (
3940
reply: any,
@@ -93,7 +94,9 @@ export default class MockServer extends EventEmitter {
9394
}
9495
const flags: IFlags = {};
9596
const handlerResult = this.handler && this.handler(reply, c, flags);
96-
this.write(c, handlerResult);
97+
if (!flags.hang) {
98+
this.write(c, handlerResult);
99+
}
97100
if (flags.disconnect) {
98101
this.disconnect();
99102
}

0 commit comments

Comments
 (0)