Skip to content

Commit c4dae0c

Browse files
florian-schunkrummatee
authored andcommitted
add command timeout
1 parent a006ccf commit c4dae0c

File tree

3 files changed

+54
-3
lines changed

3 files changed

+54
-3
lines changed

packages/client/lib/client/index.spec.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { strict as assert } from 'node:assert';
22
import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
33
import RedisClient, { RedisClientOptions, RedisClientType } from '.';
4-
import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, DisconnectsClientError, ErrorReply, MultiErrorReply, SocketClosedUnexpectedlyError, WatchError } from '../errors';
4+
import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, CommandTimeoutError, DisconnectsClientError, ErrorReply, MultiErrorReply, SocketClosedUnexpectedlyError, WatchError } from '../errors';
55
import { defineScript } from '../lua-script';
66
import { spy } from 'sinon';
77
import { once } from 'node:events';
@@ -265,6 +265,22 @@ describe('Client', () => {
265265
}, GLOBAL.SERVERS.OPEN);
266266
});
267267

268+
testUtils.testWithClient('CommandTimeoutError', async client => {
269+
const promise = assert.rejects(client.sendCommand(['PING']), CommandTimeoutError),
270+
start = process.hrtime.bigint();
271+
272+
while (process.hrtime.bigint() - start < 50_000_000) {
273+
// block the event loop for 1ms, to make sure the connection will timeout
274+
}
275+
276+
await promise;
277+
}, {
278+
...GLOBAL.SERVERS.OPEN,
279+
clientOptions: {
280+
commandTimeout: 50,
281+
}
282+
});
283+
268284
testUtils.testWithClient('undefined and null should not break the client', async client => {
269285
await assert.rejects(
270286
client.sendCommand([null as any, undefined as any]),

packages/client/lib/client/index.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { BasicAuth, CredentialsError, CredentialsProvider, StreamingCredentialsP
44
import RedisCommandsQueue, { CommandOptions } from './commands-queue';
55
import { EventEmitter } from 'node:events';
66
import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander';
7-
import { ClientClosedError, ClientOfflineError, DisconnectsClientError, WatchError } from '../errors';
7+
import { ClientClosedError, ClientOfflineError, CommandTimeoutError, DisconnectsClientError, WatchError } from '../errors';
88
import { URL } from 'node:url';
99
import { TcpSocketConnectOpts } from 'node:net';
1010
import { PUBSUB_TYPE, PubSubType, PubSubListener, PubSubTypeListeners, ChannelListeners } from './pub-sub';
@@ -144,6 +144,10 @@ export interface RedisClientOptions<
144144
* Tag to append to library name that is sent to the Redis server
145145
*/
146146
clientInfoTag?: string;
147+
/**
148+
* Provides a timeout in milliseconds.
149+
*/
150+
commandTimeout?: number;
147151
}
148152

149153
type WithCommands<
@@ -889,9 +893,34 @@ export default class RedisClient<
889893
return Promise.reject(new ClientOfflineError());
890894
}
891895

896+
let controller: AbortController;
897+
if (this._self.#options?.commandTimeout) {
898+
controller = new AbortController()
899+
options = {
900+
...options,
901+
abortSignal: controller.signal
902+
}
903+
}
892904
const promise = this._self.#queue.addCommand<T>(args, options);
905+
893906
this._self.#scheduleWrite();
894-
return promise;
907+
if (!this._self.#options?.commandTimeout) {
908+
return promise;
909+
}
910+
911+
return new Promise<T>((resolve, reject) => {
912+
const timeoutId = setTimeout(() => {
913+
controller.abort();
914+
reject(new CommandTimeoutError());
915+
}, this._self.#options?.commandTimeout)
916+
promise.then(result => {
917+
clearInterval(timeoutId);
918+
resolve(result)
919+
}).catch(error => {
920+
clearInterval(timeoutId);
921+
reject(error)
922+
});
923+
})
895924
}
896925

897926
async SELECT(db: number): Promise<void> {

packages/client/lib/errors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ export class SocketTimeoutError extends Error {
2222
}
2323
}
2424

25+
export class CommandTimeoutError extends Error {
26+
constructor() {
27+
super('Command timeout');
28+
}
29+
}
30+
2531
export class ClientClosedError extends Error {
2632
constructor() {
2733
super('The client is closed');

0 commit comments

Comments
 (0)