Skip to content

fix(client): keep the selected database when a pipelined command fails - #3484

Merged
nkaradzhov merged 3 commits into
redis:masterfrom
maxymlyskov:fix/selected-db-after-pipeline-error
Oct 1, 2026
Merged

nkaradzhov merged 3 commits into
redis:masterfrom
maxymlyskov:fix/selected-db-after-pipeline-error

Conversation

@maxymlyskov

@maxymlyskov maxymlyskov commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Description

A database selected with multi().select(db) is lost on the next reconnect when the batch runs with execAsPipeline() and one of its commands fails. _executePipeline sets #selectedDB only after await Promise.all(...), so a rejected command skips the assignment, though Redis already ran the SELECT and the connection sits on the new database. The handshake re-sends SELECT only while #selectedDB !== 0, so after a reconnect the client is back on database 0 with no error. The same batch run with exec() keeps its database. #3469 made the pool forward selectedDB to this path, so pooled clients lose it the same way.

#selectedDB is now set from each SELECT's own reply, with the database taken from that command's arguments, so the last SELECT Redis ran is the one recorded. A SELECT that fails is not recorded: after select(1).select(99) the client reconnects to database 1, where master goes back to 0. A raw SELECT added with addCommand() in a pipeline is now recorded too, with its name in any case and as a string or a Buffer. The selectedDB parameter is no longer read, and it stays in the signature because cluster passes slotNumber after it.

Redis 8.2.2, multi().select(2).set('s', 'text').incr('s').execAsPipeline() and multi().select(1).select(99).set('d', '1').execAsPipeline(), each followed by CLIENT KILL from a second client:

# master
PROBE pipeline: rejected with SimpleError ERR value is not an integer or out of range
PROBE server-side db after the batch: 2
PROBE reconnected, ready = true , server-side db now: 0
PROBE double select(1).select(99): server-side db after the batch: 1
PROBE double select(1).select(99): after CLIENT KILL ready = true, server-side db now: 0, errors: Socket closed unexpectedly
# this branch
PROBE reconnected, ready = true , server-side db now: 2
PROBE double select(1).select(99): after CLIENT KILL ready = true, server-side db now: 1, errors: Socket closed unexpectedly

The first new case, next to multi should remember selected db, pipelines select(2), a failing INCR, select(1) and an out-of-range SELECT and fails on master with 0 !== 1. A second case pipelines addCommand([Buffer.from('select'), '2']) and fails on master with 0 !== 2. Both kill the connection with CLIENT KILL ID from a duplicate, as the PubSub and MONITOR cases do, because the QUIT-based killClient fails on a clean master on my Windows machine. The whole packages/client suite, run with CI's test command including the cluster and sentinel specs, has no failure on this branch that master does not have: on my Windows machine 9 cases fail on both (the three killClient reconnect cases, four HIMPORT reconnect cases and two socketTimeout cases). After the Buffer change, lib/client/index.spec.ts on Redis 8.2.2 fails the same four cases on this branch and on master.


Checklist

  • Does npm test pass with this change (including linting)?
  • Is the new or changed code fully tested?
  • Is a documentation update included (if this change modifies existing APIs, or introduces new ones)?

_executePipeline recorded the database from multi().select() only after
every command in the batch had resolved. When a later command failed,
Promise.all rejected and the record was skipped, although the server had
already run the SELECT. The connection stayed on the new database, the
client still held the old one, and the next reconnect's handshake went
back to database 0 without an error.

The database is now recorded when the SELECT's own reply arrives. A
SELECT that fails, such as an index out of range, is still not recorded.
The previous commit recorded the database from multi().select() when the
reply to the last SELECT in the batch arrived. When that last SELECT
failed, as in select(1).select(99), nothing was recorded, although the
server had run select(1) and stayed on database 1, so the next reconnect
still went back to 0.

Each SELECT now records the database from its own argument when its
reply arrives, so the last SELECT the server ran is the one kept, and a
SELECT that fails is never recorded. A pipelined SELECT added with
addCommand() is recorded the same way. _executePipeline keeps its
selectedDB parameter, no longer read, because cluster passes slotNumber
after it.
@maxymlyskov
maxymlyskov marked this pull request as ready for review September 29, 2026 21:31

@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 bbf3472. Configure here.

Comment thread packages/client/lib/client/index.ts
nkaradzhov

This comment was marked as outdated.

@nkaradzhov nkaradzhov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the fix. A failed command can leave #selectedDB out of sync after an earlier SELECT succeeds. Recording the database from each successful SELECT reply addresses that bug.

Before we merge this, please update the command-name check to handle lowercase and Buffer names in raw multi().addCommand() calls. Redis accepts those forms, but the current check misses them and can still restore the wrong database after reconnect. Please add a reconnect test for one of those forms.

The pipeline recorded a SELECT only when its command name was the
string 'SELECT'. Redis also runs select and a Buffer name sent through
multi().addCommand(), so the client kept its old database and the
next reconnect went back to database 0. The name is now compared as
String(args[0]).toUpperCase(), as the HIMPORT check in the same file
already does.
@maxymlyskov

Copy link
Copy Markdown
Contributor Author

Done in ffafe20: the pipeline now compares String(args[0]).toUpperCase() to 'SELECT', like the HIMPORT check in the same file, and the new reconnect test pipelines addCommand([Buffer.from('select'), '2']), which fails on the previous head with 0 !== 2.

@nkaradzhov nkaradzhov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@nkaradzhov
nkaradzhov merged commit 3c82aea into redis:master Oct 1, 2026
25 of 26 checks passed
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.

2 participants