Skip to content

fix(client): emit termination event when reconnecting stops - #3433

Open
GiHoon1123 wants to merge 7 commits into
redis:masterfrom
GiHoon1123:fix-2948-terminated-event
Open

fix(client): emit termination event when reconnecting stops#3433
GiHoon1123 wants to merge 7 commits into
redis:masterfrom
GiHoon1123:fix-2948-terminated-event

Conversation

@GiHoon1123

@GiHoon1123 GiHoon1123 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes #2948

When reconnecting stops because reconnectStrategy returns false or an Error, the client currently emits error but provides no separate signal that it will not retry again.

This adds a terminated event to RedisSocket, forwards it through RedisClient, and documents when it is emitted. Tests cover initial connection failure, terminal reconnect failures, and the retrying case.


Note

Medium Risk
Touches core connection/reconnect lifecycle and error emission ordering; behavior change for apps that assumed only error on fatal disconnect, but additive event with documented ordering.

Overview
Adds a terminated event when reconnectStrategy stops retrying (false or an Error), including after a connection was already ready—cases that previously only surfaced as error, same as transient disconnects (#2948).

RedisSocket centralizes that path in #shouldReconnect: emit terminated (with cause, often ReconnectStrategyError) before the companion error, and refactor reconnect/error handling so retries still emit error without terminated. RedisClient re-emits terminated from the socket.

README and client-configuration.md document the event and a pattern (await events.once(client, 'terminated'), then destroy()). New socket and client tests cover ordering, post-ready loss, and no terminated while retrying.

Reviewed by Cursor Bugbot for commit f74c465. Bugbot is set up for automated code reviews on this repo. Configure here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b2705d2bb9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread docs/client-configuration.md Outdated
});
```

An `'error'` event fires on every disconnect, including ones the client is about to retry, so it can't tell you whether reconnection is still in progress. Once `reconnectStrategy` gives up (returns `false` or an `Error`), the client also emits a `'terminated'` event with the reason — that's the signal that reconnection has permanently stopped and the client needs to be recreated:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add terminated to the exhaustive event tables

The canonical event references in README.md:384-397 and packages/redis/README.md:298-310 both omit terminated and explicitly state that the client emits no events beyond those listed. Introducing this public event here makes the published documentation contradictory and hides the new terminal-reconnection signal from users consulting the event API; update both tables with its semantics and Error argument.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the event tables in both README files with the terminated event and its Error argument. I also updated the reconnect strategy example to call destroy() before replacing the client, so the existing cleanup path is used for client-level resources.

this.#maybeScheduleWrite();
})
.on('reconnecting', () => this.emit('reconnecting'))
.on('terminated', cause => this.emit('terminated', cause))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clean up client resources on terminal transition

When a previously connected client has an active streaming-credentials subscription or was registered with the initialized OpenTelemetry ClientRegistry, this callback only forwards the terminal event and leaves those client-level resources alive. Their cleanup occurs only in RedisClient.destroy() (index.ts:2236-2238), so an application that follows the new documentation by dropping this client and creating a replacement without explicitly destroying the old one leaves a registry handle that strongly retains the client and may leave its credential observer subscribed. Dispose these resources on the terminal path or explicitly require destroy() before replacement.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept cleanup explicit rather than doing it in the event forwarding callback. terminated reports that reconnection has ended, while destroy() remains the client lifecycle operation that releases credentials and metrics resources. The documentation now calls destroy() before replacing the client, and the example uses it.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6b9516352c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/client/lib/client/socket.ts Outdated
Comment on lines +225 to +226
this.emit('error', cause);
this.emit('terminated', cause);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit terminated before its guaranteed error

When callers await this new event with Node's standard events.once(client, 'terminated'), the promise always rejects on the preceding error instead of resolving with the termination cause; the added test explicitly acknowledges and works around this behavior. This makes the terminal signal unusable with the Promise-based EventEmitter API in every give-up path, even when the client has a separate error listener. Emit terminated before the companion error (in both strategy branches) so awaiting it can succeed.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87a1fc7299

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

internal: false,
clientId: this.#clientId
}));
this.emit('terminated', cause);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit terminated before the first post-ready error

When an already-ready connection dies, #onSocketError still emits error before invoking #shouldReconnect, so events.once(client, 'terminated') rejects before this event is reached whenever the strategy gives up. Fresh evidence in the final diff is the unchanged this.emit('error', err) at line 416 preceding the call to this changed method at line 425; determine the terminal decision before that first error emission so the documented ordering also holds for post-ready disconnects.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36a1037980

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread docs/client-configuration.md Outdated
```javascript
client.on('terminated', cause => {
console.error('client will not reconnect:', cause);
client.destroy();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Defer destroy until the companion error is handled

When a live connection terminates with commands pending, this synchronous destroy() runs from the terminated listener before the socket emits its companion error. RedisClient.destroy() therefore flushes every queued command with DisconnectsClientError, and the subsequent socket error handler has nothing left to reject with the actual connection failure. Users following this example lose the meaningful failure cause for all in-flight and offline-queued commands; defer the cleanup to a microtask or otherwise ensure the companion error handler flushes the queue first.

Useful? React with 👍 / 👎.

Comment on lines 439 to 440
this.emit('reconnecting');
this.#connect().catch(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recheck isOpen after emitting the reconnectable error

When a ready connection fails with a numeric retry strategy and an error listener synchronously calls client.destroy() or client.close(), that listener sets the socket's #isOpen to false, but execution then reaches these lines unconditionally. Because #connect() performs its first attempt before checking the loop condition, the destroyed client can establish a new socket and emit connect/ready after end, leaving a live connection behind; recheck #isOpen after the synchronous error emission before announcing or starting reconnection.

Useful? React with 👍 / 👎.

Comment thread packages/client/lib/client/socket.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a04d4f81d4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/client/lib/client/socket.ts Outdated
return;
}

const retryIn = this.#shouldReconnect(0, err);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the socket cause when the reconnect strategy throws

When an established connection fails and the custom reconnectStrategy throws or returns an invalid value, this call enters #createReconnectStrategy's catch block, which synchronously emits the strategy exception before the actual socket error is emitted below. The RedisClient error handler therefore flushes pending commands with the strategy exception, whereas previously the waiting commands were rejected with the real connection failure before the strategy ran. Determine the retry decision without letting that intermediate error consume the command queue, or preserve the original socket-error emission ordering for this fallback path.

Useful? React with 👍 / 👎.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit a04d4f8. Configure here.

Comment thread packages/client/lib/client/socket.ts
@GiHoon1123

GiHoon1123 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. The terminal path now emits terminated before the corresponding error, including failures during the handshake. Retrying paths keep the existing error and reconnecting behavior, and a synchronous shutdown from an error listener no longer starts another reconnect. Added regression coverage for these cases; targeted socket tests, type checks, and lint all pass.

@nkaradzhov

nkaradzhov commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR and for working through all the ordering feedback! 🙏

One small ask: could you briefly document the terminatederror ordering in the README event table / docs/client-configuration.md? E.g.:

terminated is always emitted before its companion error, so await events.once(client, 'terminated') resolves with the cause, and an error preceded by terminated means the client has permanently given up (vs. a transient retry).

Right now that reasoning only lives in the review threads, and the ordering can look confusing without it.

@GiHoon1123

Copy link
Copy Markdown
Contributor Author

Documented the ordering in both README event tables and expanded the client configuration note to explain the events.once() behavior. Thanks for pointing this out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reconnect Strategy doesn't work if connection is lost

2 participants