Skip to content

Commit c00f419

Browse files
committed
--wip-- [skip ci]
1 parent 6f961bd commit c00f419

File tree

3 files changed

+35
-4
lines changed

3 files changed

+35
-4
lines changed

docs/client-configuration.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
| socket.family | `0` | IP Stack version (one of `4 \| 6 \| 0`) |
1010
| socket.path | | Path to the UNIX Socket |
1111
| socket.connectTimeout | `5000` | Connection timeout (in milliseconds) |
12+
| socket.socketTimeout | | The maximum duration (in milliseconds) that the socket can remain idle (i.e., with no data sent or received) before being automatically closed |
1213
| socket.noDelay | `true` | Toggle [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay) |
1314
| socket.keepAlive | `true` | Toggle [`keep-alive`](https://nodejs.org/api/net.html#socketsetkeepaliveenable-initialdelay) functionality |
1415
| socket.keepAliveInitialDelay | `5000` | If set to a positive number, it sets the initial delay before the first keepalive probe is sent on an idle socket |
@@ -40,7 +41,12 @@ By default the strategy uses exponential backoff, but it can be overwritten like
4041
```javascript
4142
createClient({
4243
socket: {
43-
reconnectStrategy: retries => {
44+
reconnectStrategy: (retries, cause) => {
45+
// By default, do not reconnect on socket timeout.
46+
if (cause instanceof SocketTimeoutError) {
47+
return false;
48+
}
49+
4450
// Generate a random jitter between 0 – 200 ms:
4551
const jitter = Math.floor(Math.random() * 200);
4652
// Delay is an exponential back off, (times^2) * 50 ms, with a maximum value of 2000 ms:

packages/client/lib/client/socket.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { EventEmitter, once } from 'node:events';
22
import net from 'node:net';
33
import tls from 'node:tls';
4-
import { ConnectionTimeoutError, ClientClosedError, SocketClosedUnexpectedlyError, ReconnectStrategyError } from '../errors';
4+
import { ConnectionTimeoutError, ClientClosedError, SocketClosedUnexpectedlyError, ReconnectStrategyError, SocketTimeoutError } from '../errors';
55
import { setTimeout } from 'node:timers/promises';
66
import { RedisArgument } from '../RESP/types';
77

@@ -23,6 +23,10 @@ type RedisSocketOptionsCommon = {
2323
* 3. `(retries: number, cause: Error) => false | number | Error` -> `number` is the same as configuring a `number` directly, `Error` is the same as `false`, but with a custom error.
2424
*/
2525
reconnectStrategy?: false | number | ReconnectStrategyFunction;
26+
/**
27+
* The timeout (in milliseconds) after which the socket will be closed. `undefined` means no timeout.
28+
*/
29+
socketTimeout?: number;
2630
}
2731

2832
type RedisTcpOptions = RedisSocketOptionsCommon & NetOptions & Omit<
@@ -55,6 +59,7 @@ export default class RedisSocket extends EventEmitter {
5559
readonly #connectTimeout;
5660
readonly #reconnectStrategy;
5761
readonly #socketFactory;
62+
readonly #socketTimeout;
5863

5964
#socket?: net.Socket | tls.TLSSocket;
6065

@@ -79,6 +84,7 @@ export default class RedisSocket extends EventEmitter {
7984
this.#connectTimeout = options?.connectTimeout ?? 5000;
8085
this.#reconnectStrategy = this.#createReconnectStrategy(options);
8186
this.#socketFactory = this.#createSocketFactory(options);
87+
this.#socketTimeout = options?.socketTimeout;
8288
}
8389

8490
#createReconnectStrategy(options?: RedisSocketOptions): ReconnectStrategyFunction {
@@ -97,7 +103,7 @@ export default class RedisSocket extends EventEmitter {
97103
return retryIn;
98104
} catch (err) {
99105
this.emit('error', err);
100-
return this.defaultReconnectStrategy(retries);
106+
return this.defaultReconnectStrategy(retries, err);
101107
}
102108
};
103109
}
@@ -246,6 +252,14 @@ export default class RedisSocket extends EventEmitter {
246252
socket.removeListener('timeout', onTimeout);
247253
}
248254

255+
//TODO valiate > 0 -> #validateOptions method should be available with csc PR
256+
if (this.#socketTimeout !== undefined) {
257+
socket.setTimeout(this.#socketTimeout);
258+
socket.once('timeout', () => {
259+
socket.destroy(new SocketTimeoutError(this.#socketTimeout!));
260+
});
261+
}
262+
249263
socket
250264
.once('error', err => this.#onSocketError(err))
251265
.once('close', hadError => {
@@ -334,7 +348,12 @@ export default class RedisSocket extends EventEmitter {
334348
this.#socket?.unref();
335349
}
336350

337-
defaultReconnectStrategy(retries: number) {
351+
defaultReconnectStrategy(retries: number, cause: unknown) {
352+
// By default, do not reconnect on socket timeout.
353+
if (cause instanceof SocketTimeoutError) {
354+
return false;
355+
}
356+
338357
// Generate a random jitter between 0 – 200 ms:
339358
const jitter = Math.floor(Math.random() * 200);
340359
// Delay is an exponential back off, (times^2) * 50 ms, with a maximum value of 2000 ms:

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 SocketTimeoutError extends Error {
20+
constructor(timeout: number) {
21+
super(`Socket timeout timeout. Expecting data, but didn't receive any in ${timeout}ms.`);
22+
}
23+
}
24+
1925
export class ClientClosedError extends Error {
2026
constructor() {
2127
super('The client is closed');

0 commit comments

Comments
 (0)