Skip to content

Commit

Permalink
fix(client): Avoids infinite promise-chaining when socket's creation …
Browse files Browse the repository at this point in the history
…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 <leibale1998@gmail.com>
  • Loading branch information
JonasFaure and leibale committed Oct 26, 2022
1 parent c413657 commit 252c219
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 41 deletions.
19 changes: 8 additions & 11 deletions packages/client/lib/client/socket.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { strict as assert } from 'assert';
import { SinonFakeTimers, useFakeTimers, spy } from 'sinon';
import { spy } from 'sinon';
import RedisSocket, { RedisSocketOptions } from './socket';

describe('Socket', () => {
Expand All @@ -9,37 +9,34 @@ describe('Socket', () => {
options
);

socket.on('error', (err) => {
socket.on('error', () => {
// ignore errors
console.log(err);
});

return socket;
}

describe('reconnectStrategy', () => {
let clock: SinonFakeTimers;
beforeEach(() => clock = useFakeTimers());
afterEach(() => clock.restore());

it('custom strategy', async () => {
const numberOfRetries = 10;

const reconnectStrategy = spy((retries: number) => {
assert.equal(retries + 1, reconnectStrategy.callCount);

if (retries === 50) return new Error('50');
if (retries === numberOfRetries) return new Error(`${numberOfRetries}`);

const time = retries * 2;
queueMicrotask(() => clock.tick(time));
return time;
});

const socket = createSocket({
host: 'error',
connectTimeout: 1,
reconnectStrategy
});

await assert.rejects(socket.connect(), {
message: '50'
message: `${numberOfRetries}`
});

assert.equal(socket.isOpen, false);
Expand All @@ -48,9 +45,9 @@ describe('Socket', () => {
it('should handle errors', async () => {
const socket = createSocket({
host: 'error',
connectTimeout: 1,
reconnectStrategy(retries: number) {
if (retries === 1) return new Error('done');
queueMicrotask(() => clock.tick(500));
throw new Error();
}
});
Expand Down
63 changes: 33 additions & 30 deletions packages/client/lib/client/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,46 +105,49 @@ export default class RedisSocket extends EventEmitter {
throw new Error('Socket already opened');
}

return this.#connect(0);
return this.#connect();
}

async #connect(retries: number, hadError?: boolean): Promise<void> {
if (retries > 0 || hadError) {
this.emit('reconnecting');
}

try {
this.#isOpen = true;
this.#socket = await this.#createSocket();
this.#writableNeedDrain = false;
this.emit('connect');
async #connect(hadError?: boolean): Promise<void> {
let retries = 0;
do {
if (retries > 0 || hadError) {
this.emit('reconnecting');
}

try {
await this.#initiator();
this.#isOpen = true;
this.#socket = await this.#createSocket();
this.#writableNeedDrain = false;
this.emit('connect');

try {
await this.#initiator();
} catch (err) {
this.#socket.destroy();
this.#socket = undefined;
throw err;
}
this.#isReady = true;
this.emit('ready');
} catch (err) {
this.#socket.destroy();
this.#socket = undefined;
throw err;
}
this.#isReady = true;
this.emit('ready');
} catch (err) {
const retryIn = this.reconnectStrategy(retries);
if (retryIn instanceof Error) {
this.#isOpen = false;
const retryIn = this.reconnectStrategy(retries);
if (retryIn instanceof Error) {
this.#isOpen = false;
this.emit('error', err);
throw new ReconnectStrategyError(retryIn, err);
}

this.emit('error', err);
throw new ReconnectStrategyError(retryIn, err);
await promiseTimeout(retryIn);
}

this.emit('error', err);
await promiseTimeout(retryIn);
return this.#connect(retries + 1);
}
retries++;
} while (!this.#isReady);
}

#createSocket(): Promise<net.Socket | tls.TLSSocket> {
return new Promise((resolve, reject) => {
const {connectEvent, socket} = RedisSocket.#isTlsSocket(this.#options) ?
const { connectEvent, socket } = RedisSocket.#isTlsSocket(this.#options) ?
this.#createTlsSocket() :
this.#createNetSocket();

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

this.#connect(0, true).catch(() => {
this.#connect(true).catch(() => {
// the error was already emitted, silently ignore it
});
}
Expand Down

0 comments on commit 252c219

Please sign in to comment.