Skip to content

SocketClosedUnexpectedlyError while awaiting #initiator results in multiple connecting sockets / connections #2443

Closed
@edkimmel

Description

@edkimmel

Description

Hello, we noticed in our logs that node-redis was able to get into a broken state during a ongoing networking issue causing socket closures. The last logs from some machines were the "Socket closed unexpectedly" error message, with no follow up reconnect event. We also noticed that some machines were firing multiple connecting / reconnecting events in parallel for the same client.

I believe there is an issue in error handling when a socket is closed during the #initiator promise in #connect.

  this.#socket = await this.#createSocket();
  this.#writableNeedDrain = false;
  this.emit('connect');

  try {
    await this.#initiator(); // <------ Socket closed here results in multiple #connect chains.
  } catch (err) {

On that line, the state of socket is:

  • isOpen - true
  • #socket - set as expected

Looking at createSocket:

.once('close', hadError => {
  if (!hadError && this.#isOpen && this.#socket === socket) {
    this.#onSocketError(new SocketClosedUnexpectedlyError());
  }
})
  • hasError can be falsey
  • isOpen is true - we were in the middle of #connect
  • #socket === socket is true, we had just set the instance variable

And finally looking at #onSocketError

    #onSocketError(err: Error): void {
        this.#isReady = false;
        this.emit('error', err);

        if (!this.#isOpen || typeof this.#shouldReconnect(0, err) !== 'number') return;
        
        this.emit('reconnecting');
        this.#connect().catch(() => {
            // the error was already emitted, silently ignore it
        });
    }
  • isOpen is true, we've established that
  • shouldReconnect will always be a number if no option is provided

So we see that the socket close event will cause this.#connect to be called again, even though we are in the middle of connecting to begin with.

A potential solution would be to check #isReady in onSocketError to know whether we are already in the process of connecting or not.

Node.js Version

lts/gallium

Redis Server Version

No response

Node Redis Version

4.3.1, but source taken from 4.6.5

Platform

linux

Logs

23:45:43.678 redis client error Socket closed unexpectedly
23:45:43.678 redis client is reconnecting
23:45:43.678 redis client is connecting
23:45:58.694 redis client error Socket closed unexpectedly
23:45:58.694 redis client error Socket closed unexpectedly
23:53:40.370 redis client error read ECONNRESET
23:53:40.370 redis client is reconnecting
23:53:40.370 redis client error read ECONNRESET
23:53:40.370 redis client is reconnecting
23:53:40.370 redis client is connecting
23:53:40.370 redis client is ready
23:53:41.371 redis client error Connection timeout
23:53:41.371 redis client is reconnecting
23:53:43.373 redis client error Connection timeout
23:53:45.375 redis client error Connection timeout
23:54:11.402 redis client error Socket closed unexpectedly
23:54:11.402 redis client is reconnecting
23:54:11.402 redis client is connecting
23:54:26.416 redis client error Socket closed unexpectedly
23:54:26.416 redis client is reconnecting
23:54:26.416 redis client error Socket closed unexpectedly
23:54:26.416 redis client is reconnecting
23:54:26.416 redis client is connecting
23:54:26.416 redis client is ready
23:54:26.416 redis client is connecting
23:54:26.416 redis client is ready

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions