Skip to content

Commit 38dac66

Browse files
add command timeout
1 parent a2fb939 commit 38dac66

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';
@@ -231,6 +231,22 @@ describe('Client', () => {
231231
}, GLOBAL.SERVERS.OPEN);
232232
});
233233

234+
testUtils.testWithClient('CommandTimeoutError', async client => {
235+
const promise = assert.rejects(client.sendCommand(['PING']), CommandTimeoutError),
236+
start = process.hrtime.bigint();
237+
238+
while (process.hrtime.bigint() - start < 50_000_000) {
239+
// block the event loop for 1ms, to make sure the connection will timeout
240+
}
241+
242+
await promise;
243+
}, {
244+
...GLOBAL.SERVERS.OPEN,
245+
clientOptions: {
246+
commandTimeout: 50,
247+
}
248+
});
249+
234250
testUtils.testWithClient('undefined and null should not break the client', async client => {
235251
await assert.rejects(
236252
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';
@@ -80,6 +80,10 @@ export interface RedisClientOptions<
8080
* TODO
8181
*/
8282
commandOptions?: CommandOptions<TYPE_MAPPING>;
83+
/**
84+
* Provides a timeout in milliseconds.
85+
*/
86+
commandTimeout?: number;
8387
}
8488

8589
type WithCommands<
@@ -730,9 +734,34 @@ export default class RedisClient<
730734
return Promise.reject(new ClientOfflineError());
731735
}
732736

737+
let controller: AbortController;
738+
if (this._self.#options?.commandTimeout) {
739+
controller = new AbortController()
740+
options = {
741+
...options,
742+
abortSignal: controller.signal
743+
}
744+
}
733745
const promise = this._self.#queue.addCommand<T>(args, options);
746+
734747
this._self.#scheduleWrite();
735-
return promise;
748+
if (!this._self.#options?.commandTimeout) {
749+
return promise;
750+
}
751+
752+
return new Promise<T>((resolve, reject) => {
753+
const timeoutId = setTimeout(() => {
754+
controller.abort();
755+
reject(new CommandTimeoutError());
756+
}, this._self.#options?.commandTimeout)
757+
promise.then(result => {
758+
clearInterval(timeoutId);
759+
resolve(result)
760+
}).catch(error => {
761+
clearInterval(timeoutId);
762+
reject(error)
763+
});
764+
})
736765
}
737766

738767
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
@@ -16,6 +16,12 @@ export class ConnectionTimeoutError extends Error {
1616
}
1717
}
1818

19+
export class CommandTimeoutError extends Error {
20+
constructor() {
21+
super('Command timeout');
22+
}
23+
}
24+
1925
export class ClientClosedError extends Error {
2026
constructor() {
2127
super('The client is closed');

0 commit comments

Comments
 (0)