Skip to content

Commit 80eddda

Browse files
committed
fix reconnect strategy + docs
1 parent 6e969fa commit 80eddda

File tree

2 files changed

+36
-39
lines changed

2 files changed

+36
-39
lines changed

docs/client-configuration.md

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,28 +29,17 @@
2929

3030
## Reconnect Strategy
3131

32-
TODO: `false | number | (retries: number, cause: unknown) => number | Error`
32+
When the socket closes unexpectedly (without calling `.quit()`/`.disconnect()`) the client uses `reconnectStrategy` to decide what to do:
33+
1. `false` -> do not reconnect, close the client and flush all commands in the queue.
34+
2. `number` -> wait for `X` milliseconds before reconnecting.
35+
3. `(retries: number, cause: Error) => number | Error` -> `number` is the same as configuration a `number` directly, `Error` is the same as `false`, but with a custom error.
3336

34-
You can implement a custom reconnect strategy as a function:
35-
When a network error occurs the client will automatically try to reconnect, following a default linear strategy (the more attempts, the more waiting before trying to reconnect).
36-
37-
This strategy can be overridden by providing a `socket.reconnectStrategy` option during the client's creation.
38-
39-
The `socket.reconnectStrategy` is a function that:
40-
41-
- Receives the number of retries attempted so far and the causing error.
42-
- Returns `number | Error`:
43-
- `number`: wait time in milliseconds prior to attempting a reconnect.
44-
- `Error`: closes the client and flushes internal command queues.
45-
46-
The example below shows the default `reconnectStrategy` and how to override it.
37+
By default the strategy is `Math.min(retries * 50, 500)`, but it can be overriten:
4738

4839
```typescript
49-
import { createClient } from 'redis';
50-
51-
const client = createClient({
40+
createClient({
5241
socket: {
53-
reconnectStrategy: retries => Math.min(retries * 50, 500)
42+
reconnectStrategy: retries => Math.min(retries * 50, 1000)
5443
}
5544
});
5645
```

packages/client/lib/client/socket.ts

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface RedisSocketCommonOptions {
99
connectTimeout?: number;
1010
noDelay?: boolean;
1111
keepAlive?: number | false;
12-
reconnectStrategy?: false | number | ((retries: number, cause: unknown) => number | Error);
12+
reconnectStrategy?: false | number | ((retries: number, cause: Error) => false | Error | number);
1313
}
1414

1515
type RedisNetSocketOptions = Partial<net.SocketConnectOpts> & {
@@ -83,7 +83,7 @@ export default class RedisSocket extends EventEmitter {
8383
this.#options = RedisSocket.#initiateOptions(options);
8484
}
8585

86-
reconnectStrategy(retries: number, cause: unknown): false | number | Error {
86+
#reconnectStrategy(retries: number, cause: Error) {
8787
if (this.#options.reconnectStrategy === false) {
8888
return false;
8989
} else if (typeof this.#options.reconnectStrategy === 'number') {
@@ -92,18 +92,33 @@ export default class RedisSocket extends EventEmitter {
9292
try {
9393
const retryIn = this.#options.reconnectStrategy(retries, cause);
9494
if (typeof retryIn !== 'number' && !(retryIn instanceof Error)) {
95-
throw new TypeError(`Reconnect strategy should return \`number | Error\`, got ${retryIn} instead`);
95+
throw new TypeError(`Reconnect strategy should return \`number | Error\`, got ${retryIn} instead`)
9696
}
9797

9898
return retryIn;
9999
} catch (err) {
100-
this.emit('error', err);
100+
this.emit('error', err);
101101
}
102102
}
103103

104104
return Math.min(retries * 50, 500);
105105
}
106106

107+
#shouldReconnect(retries: number, cause: Error) {
108+
const retryIn = this.#reconnectStrategy(retries, cause);
109+
if (retryIn === false) {
110+
this.#isOpen = false;
111+
this.emit('error', cause);
112+
return cause;
113+
} else if (retryIn instanceof Error) {
114+
this.#isOpen = false;
115+
this.emit('error', cause);
116+
return new ReconnectStrategyError(retryIn, cause);
117+
}
118+
119+
return retryIn;
120+
}
121+
107122
async connect(): Promise<void> {
108123
if (this.#isOpen) {
109124
throw new Error('Socket already opened');
@@ -113,13 +128,9 @@ export default class RedisSocket extends EventEmitter {
113128
return this.#connect();
114129
}
115130

116-
async #connect(hadError?: boolean): Promise<void> {
131+
async #connect(): Promise<void> {
117132
let retries = 0;
118133
do {
119-
if (retries > 0 || hadError) {
120-
this.emit('reconnecting');
121-
}
122-
123134
try {
124135
this.#socket = await this.#createSocket();
125136
this.#writableNeedDrain = false;
@@ -135,21 +146,17 @@ export default class RedisSocket extends EventEmitter {
135146
this.#isReady = true;
136147
this.emit('ready');
137148
} catch (err) {
138-
const retryIn = this.reconnectStrategy(retries, err);
139-
if (retryIn === false) {
140-
this.#isOpen = false;
141-
this.emit('error', err);
142-
throw err;
143-
} else if (retryIn instanceof Error) {
144-
this.#isOpen = false;
145-
this.emit('error', err);
146-
throw new ReconnectStrategyError(retryIn, err);
149+
const retryIn = this.#shouldReconnect(retries, err as Error);
150+
if (typeof retryIn !== 'number') {
151+
throw retryIn;
147152
}
148153

149154
this.emit('error', err);
150155
await promiseTimeout(retryIn);
151156
}
157+
152158
retries++;
159+
this.emit('reconnecting');
153160
} while (this.#isOpen && !this.#isReady);
154161
}
155162

@@ -211,9 +218,10 @@ export default class RedisSocket extends EventEmitter {
211218
this.#isReady = false;
212219
this.emit('error', err);
213220

214-
if (!this.#isOpen) return;
215-
216-
this.#connect(true).catch(() => {
221+
if (!this.#isOpen || typeof this.#shouldReconnect(0, err) !== 'number') return;
222+
223+
this.emit('reconnecting');
224+
this.#connect().catch(() => {
217225
// the error was already emitted, silently ignore it
218226
});
219227
}

0 commit comments

Comments
 (0)