Skip to content

feat: CLIENT UNBLOCK cancelled blocking commands to reuse the connection - #1030

Open
KartikYZ wants to merge 4 commits into
redis:mainfrom
KartikYZ:unblock-on-blocking-cancel
Open

KartikYZ wants to merge 4 commits into
redis:mainfrom
KartikYZ:unblock-on-blocking-cancel

Conversation

@KartikYZ

@KartikYZ KartikYZ commented Oct 2, 2026 •

Copy link
Copy Markdown

Implements the suggestion in #897 to reuse connections for cancelled blocking calls using toPipe() when auto-pipelining is disabled.

Commands can opt into this behavior with .UnblockOnCancel(), e.g.
client.Do(ctx, client.B().Blpop().Key("q").Timeout(0).Build().ToPipe().UnblockOnCancel())

Note: the blcksig release in pipe.Do's abort path is independent of UnblockOnCancel. At present, an aborted blocking command doesn't release blcksig. For pooled connections this seems fine because the connection is closed, but on a DedicatedClient the connection keeps skipping keepalive pings and is closed on release even after the late reply was consumed. Releasing it when the reply later arrives is safe regardless of the modifier, since the server has finished the command by then. Added a test for this, TestBlockingCancelReleasesBlcksig_Do.


Note

Medium Risk
Changes blocking-connection lifecycle, async unblock, and pipe blocking-state handling; misconfiguration or proxy routing could leave connections stuck or unblock the wrong client.

Overview
Adds an opt-in .UnblockOnCancel() command modifier so canceled or deadline-expired blocking Do calls (e.g. BLPOP) can CLIENT UNBLOCK the connection and return it to the pool instead of closing it.

mux.blocking runs unblock asynchronously when the error is context.Canceled / DeadlineExceeded, the command is blocking and flagged, and the wire still has a Redis client id from HELLO; it closes the wire if unblock does not return success. pipe.Do routes UnblockOnCancel blocking commands through the queued path even when the context has a deadline, and the abort goroutine decrements blcksig when the late blocking reply is consumed.

Dedicated clients (standalone and cluster) call ClearUnblockOnCancel before Do, so they keep the prior behavior of closing the connection on cancel. Documented constraints: Redis 6+, CLIENT UNBLOCK permission, mainly Do with auto-pipelining disabled and ToPipe(); not for DoMulti or proxies that may route CLIENT UNBLOCK to another server.

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

@KartikYZ
KartikYZ force-pushed the unblock-on-blocking-cancel branch 8 times, most recently from c7a9f01 to 007646e Compare October 6, 2026 20:08
KartikYZ and others added 2 commits October 6, 2026 20:19
When a blocking command (ex. BLPOP) on a pooled connection is aborted
by its context being canceled or exceeding its deadline, the connection
is closed today, so workers polling with short timeouts reconnect on
every timeout.

With the new opt-in ClientOption.UnblockOnCancel, send CLIENT UNBLOCK
<id> for the aborted connection over the shared pipeline to the same
node instead, and return the connection to the pool if the server
replies 1. Any other reply closes the connection as before.

- mux.blocking: unblock only for blocking cmds aborted by a context
  error on a still-healthy wire whose client id is known from HELLO.
- pipe.Do: with UnblockOnCancel, route blocking cmds with a deadline
  through the queue, so the deadline aborts the call without closing
  the conn.
- pipe.Do: release blcksig when the abandoned blocking reply arrives,
  so a recycled pipe resumes keepalive pings and isn't closed by
  CleanSubscriptions. This is intentionally not gated on the option:
  without it, an aborted blocking cmd leaked blcksig forever, which only
  went unnoticed because the pooled conn was closed. The leak is still
  observable on a DedicatedClient, whose conn kept skipping pings and
  was closed on release even after the late reply was consumed.

It only applies to blocking commands sent with Do, or with ToPipe()
when DisableAutoPipelining is set. It does not cover DoMulti or
DedicatedClient, requires Redis 6+ (for the client id from HELLO) and
the permission to run CLIENT UNBLOCK, and must not be enabled behind
proxies or load balancers that may route connections to different
servers. These are documented on the option.

Refs redis#897

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Run BLPOPs with random deadlines from concurrent workers on a smaller
BlockingPool, mixed with SET/GET of unique values, so that unblocked
conns are reused across workers. Any reply other than a context or
network timeout, or the exact pushed value, means a late reply of an
aborted cmd reached another caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread rueidis.go Outdated
// It is not compatible with DedicatedClient or proxies that may route connections to different servers as
// CLIENT UNBLOCK may reach another server and unblock an unrelated client.
// Ref: https://github.com/redis/rueidis/issues/897
UnblockOnCancel bool

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.

Hi @KartikYZ, thanks for the PR! Most of the implementations look good to me. One thing I am not sure is whether should we have this global UnblockOnCancel option. Could we try to enable this behavior per command?

@KartikYZ KartikYZ Oct 11, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hi @rueian, thank you for your review! Is the suggestion that we have client options scoped to enabling each command in the @\blocking ACL? Will revise to do so.

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.

No. I am thinking something similar to the ToPipe() modifier that changes the behavior at per command level.

@KartikYZ KartikYZ Oct 12, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ah understood! That makes sense. Revising to do so.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Switched from a ClientOption to a per-command modifier in the latest commit, thanks.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Also added a safeguard to clear the flag for dedicated clients, following the precedent with ClearStaticTTL.

…ientOption

Replace ClientOption.UnblockOnCancel with Completed.UnblockOnCancel(), so
unblocking a canceled blocking command is opted into per invocation, like
ToPipe(). Commands without the modifier keep closing the connection when
canceled.

Co-authored-by: Cursor <cursoragent@cursor.com>

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

Comment thread pipe.go
A DedicatedClient never sends CLIENT UNBLOCK, so a flagged blocking cmd
aborted by its deadline left the dedicated connection open and blocked,
making later commands on it wait until the blocking cmd returned. Clear
the flag in the dedicated clients' Do, like ClearStaticTTL, so the
connection is closed on cancel as before.

Co-authored-by: Cursor <cursoragent@cursor.com>
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