Skip to content

Commit 252c219

Browse files
JonasFaureleibale
andauthored
fix(client): Avoids infinite promise-chaining when socket's creation fails (#2295)
* fix(client): timeout issues during tests * fix(client): avoiding infinite Promise chaining while socket creation fails * fix(client): Added missing semicolons * clean test Co-authored-by: leibale <[email protected]>
1 parent c413657 commit 252c219

File tree

2 files changed

+41
-41
lines changed

2 files changed

+41
-41
lines changed

packages/client/lib/client/socket.spec.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { strict as assert } from 'assert';
2-
import { SinonFakeTimers, useFakeTimers, spy } from 'sinon';
2+
import { spy } from 'sinon';
33
import RedisSocket, { RedisSocketOptions } from './socket';
44

55
describe('Socket', () => {
@@ -9,37 +9,34 @@ describe('Socket', () => {
99
options
1010
);
1111

12-
socket.on('error', (err) => {
12+
socket.on('error', () => {
1313
// ignore errors
14-
console.log(err);
1514
});
1615

1716
return socket;
1817
}
1918

2019
describe('reconnectStrategy', () => {
21-
let clock: SinonFakeTimers;
22-
beforeEach(() => clock = useFakeTimers());
23-
afterEach(() => clock.restore());
24-
2520
it('custom strategy', async () => {
21+
const numberOfRetries = 10;
22+
2623
const reconnectStrategy = spy((retries: number) => {
2724
assert.equal(retries + 1, reconnectStrategy.callCount);
2825

29-
if (retries === 50) return new Error('50');
26+
if (retries === numberOfRetries) return new Error(`${numberOfRetries}`);
3027

3128
const time = retries * 2;
32-
queueMicrotask(() => clock.tick(time));
3329
return time;
3430
});
3531

3632
const socket = createSocket({
3733
host: 'error',
34+
connectTimeout: 1,
3835
reconnectStrategy
3936
});
4037

4138
await assert.rejects(socket.connect(), {
42-
message: '50'
39+
message: `${numberOfRetries}`
4340
});
4441

4542
assert.equal(socket.isOpen, false);
@@ -48,9 +45,9 @@ describe('Socket', () => {
4845
it('should handle errors', async () => {
4946
const socket = createSocket({
5047
host: 'error',
48+
connectTimeout: 1,
5149
reconnectStrategy(retries: number) {
5250
if (retries === 1) return new Error('done');
53-
queueMicrotask(() => clock.tick(500));
5451
throw new Error();
5552
}
5653
});

packages/client/lib/client/socket.ts

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -105,46 +105,49 @@ export default class RedisSocket extends EventEmitter {
105105
throw new Error('Socket already opened');
106106
}
107107

108-
return this.#connect(0);
108+
return this.#connect();
109109
}
110110

111-
async #connect(retries: number, hadError?: boolean): Promise<void> {
112-
if (retries > 0 || hadError) {
113-
this.emit('reconnecting');
114-
}
115-
116-
try {
117-
this.#isOpen = true;
118-
this.#socket = await this.#createSocket();
119-
this.#writableNeedDrain = false;
120-
this.emit('connect');
111+
async #connect(hadError?: boolean): Promise<void> {
112+
let retries = 0;
113+
do {
114+
if (retries > 0 || hadError) {
115+
this.emit('reconnecting');
116+
}
121117

122118
try {
123-
await this.#initiator();
119+
this.#isOpen = true;
120+
this.#socket = await this.#createSocket();
121+
this.#writableNeedDrain = false;
122+
this.emit('connect');
123+
124+
try {
125+
await this.#initiator();
126+
} catch (err) {
127+
this.#socket.destroy();
128+
this.#socket = undefined;
129+
throw err;
130+
}
131+
this.#isReady = true;
132+
this.emit('ready');
124133
} catch (err) {
125-
this.#socket.destroy();
126-
this.#socket = undefined;
127-
throw err;
128-
}
129-
this.#isReady = true;
130-
this.emit('ready');
131-
} catch (err) {
132-
const retryIn = this.reconnectStrategy(retries);
133-
if (retryIn instanceof Error) {
134-
this.#isOpen = false;
134+
const retryIn = this.reconnectStrategy(retries);
135+
if (retryIn instanceof Error) {
136+
this.#isOpen = false;
137+
this.emit('error', err);
138+
throw new ReconnectStrategyError(retryIn, err);
139+
}
140+
135141
this.emit('error', err);
136-
throw new ReconnectStrategyError(retryIn, err);
142+
await promiseTimeout(retryIn);
137143
}
138-
139-
this.emit('error', err);
140-
await promiseTimeout(retryIn);
141-
return this.#connect(retries + 1);
142-
}
144+
retries++;
145+
} while (!this.#isReady);
143146
}
144147

145148
#createSocket(): Promise<net.Socket | tls.TLSSocket> {
146149
return new Promise((resolve, reject) => {
147-
const {connectEvent, socket} = RedisSocket.#isTlsSocket(this.#options) ?
150+
const { connectEvent, socket } = RedisSocket.#isTlsSocket(this.#options) ?
148151
this.#createTlsSocket() :
149152
this.#createNetSocket();
150153

@@ -200,7 +203,7 @@ export default class RedisSocket extends EventEmitter {
200203
this.#isReady = false;
201204
this.emit('error', err);
202205

203-
this.#connect(0, true).catch(() => {
206+
this.#connect(true).catch(() => {
204207
// the error was already emitted, silently ignore it
205208
});
206209
}

0 commit comments

Comments
 (0)