redis: register the subscribe() listener only after the server confirms the subscription - #33290
redis: register the subscribe() listener only after the server confirms the subscription#33290robobun wants to merge 2 commits into
Conversation
|
Updated 1:02 AM PT - Jul 15th, 2026
❌ @robobun, your commit e2445ab has 2 failures in
🧪 To try this PR locally: bunx bun-pr 33290That installs a local version of the PR into your bun-33290 --bun |
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
|
This does not fix #33103, so I'm leaving the #33103 is about a successful subscribe followed by Checked against the issue's scenario (scripted RESP3 server, successful subscribe, then Same behaviour either way. #33104 is the one that fixes #33103, by teaching The two do overlap on one symptom: a rejected SUBSCRIBE today also pins the loop, and #33104 would mask that for the |
WalkthroughThis PR defers SUBSCRIBE listener registration until server confirmation. It adds pending-subscription state to command queuing and send paths, snapshots requested channels before sending, wires confirmed subscriptions in the Valkey runtime, and expands tests around rejection, reconnect, and channel immutability. ChangesDeferred subscription confirmation
Compact metadata
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/runtime/valkey_jsc/js_valkey_functions.rs`:
- Around line 1658-1661: The explanatory comment in the subscription flow is too
long and exceeds the 3-line code comment limit. Trim the block near
SubscriptionCtx::register_subscription / the listener wiring so it stays within
3 lines, and remove the last sentence about snapshotting since that point is
already clear from the code.
In `@test/js/valkey/valkey-subscribe-listener.test.ts`:
- Around line 72-163: The three subscribe listener tests are independent and
each uses its own confirmingRedis server on an ephemeral port, so they should be
marked concurrent instead of running sequentially. Update the test declarations
in valkey-subscribe-listener.test.ts to use test.concurrent (or wrap them in
describe.concurrent) for the cases around RedisClient.subscribe, keeping the
existing assertions and cleanup intact so the tests can run safely in parallel.
- Around line 72-163: Add coverage for the pending-SUBSCRIBE
close/never-confirmed path in the subscribe listener tests. The current tests in
valkey-subscribe-listener.test.ts only cover rejection before send and
channel-array mutation, so add a scripted server scenario where Redis accepts
SUBSCRIBE but never sends the confirmation (or closes mid-flight) and verify the
listener is not registered. Use the existing RedisClient subscribe flow and
assert that shutdown()/fail() leave the handler map clean with no duplicate
delivery or lingering listener.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 7d0a74ff-6850-49b1-8e57-c60fc338f7a0
📒 Files selected for processing (6)
src/runtime/valkey_jsc/ValkeyCommand.rssrc/runtime/valkey_jsc/js_valkey.rssrc/runtime/valkey_jsc/js_valkey_functions.rssrc/runtime/valkey_jsc/mod.rssrc/runtime/valkey_jsc/valkey.rstest/js/valkey/valkey-subscribe-listener.test.ts
Status: diff is green, CI red on unrelated lanesRebased onto The three hard failures are repo-wide, none touch
The warning-annotation flakes ( Not pushing further retriggers. Ready for a maintainer to merge past the red lanes. |
subscribe() added the listener to the receive-handler map before sending the SUBSCRIBE, with no way to remove it if the command failed. The listener outlived the rejected promise: a retry registered it a second time and delivered every message twice, and the non-empty handler map kept the event loop pinned so the process never exited. Carry the channels and listener on the command's promise pair instead, and register them from the subscribe confirmation. A SUBSCRIBE that is rejected or never answered now leaves the map untouched.
Add a test for a SUBSCRIBE that is still in flight when the connection closes: it must not leave its listener registered either. Run the tests in this file concurrently, and keep the subscribe() comment within the 3-line limit.
5b2c6aa to
e2445ab
Compare
Repro
subscribe()registers its listener before theSUBSCRIBEis sent. If the command thenfails, the listener stays registered. Retry the subscribe and every message is delivered
twice; the now-permanently-populated handler map also keeps the event loop pinned.
Cause
JSValkeyClient::subscribecalledupsert_receive_handlerfor each channel up front, asthe source itself noted:
Nothing does roll it back. The handler map is what
has_subscriptions()reads, soupdate_poll_refkeeps the event-loop ref (and the strongthis_value) forever, and alater
subscribe()for the same channel appends the same listener to the channel'scallback array a second time.
Rolling back at the failure site is not enough either: the map stores one array of
listeners per channel, so removing "the listener this command added" cannot be told apart
from an identical listener that an earlier, confirmed subscribe added.
Fix
Carry the channels and the listener on the command's promise pair (
PendingSubscription,two
Strongrefs, boxed so it costs one pointer on every other command) and register themfrom the
subscribeconfirmation push instead. A SUBSCRIBE that is rejected, dropped by aclosing connection, or never answered simply drops the pair and leaves the map untouched.
The channel list is snapshotted into a fresh array at call time, since the caller's array is
no longer read synchronously.
This also removes the
clear_all_receive_handlers()call on the synchronous send-failurepath, which used to wipe out every other channel's listeners too.
Verification
Without the
src/change, three of the four fail: two with the repro's duplicate delivery(
["hi", "hi"]for one published message, once for a subscribe rejected before it is sentand once for a subscribe still in flight when the connection closes), and one with a pinned
event loop. The fourth is a regression guard for the channel snapshot.
Unchanged behaviour against a real redis
Single- and multi-channel subscribe, message delivery, multiple listeners per channel,
unsubscribe(channel, listener),unsubscribe(), subscribing before the connection is up,and back-to-back un-awaited subscribes all behave identically to
main, with onecorrection: two un-awaited
subscribe()calls now resolve with1then2(the realchannel counts) rather than
2then2, because the count is no longer inflated bylisteners whose SUBSCRIBE has not been confirmed yet.
Notes
Found while investigating a report that
await redis.subscribe(ch, cb)hangs forever whenthe server answers the SUBSCRIBE with
-ERR LOADING. That hang is a separate bug and isalready fixed by #32858, which I verified by building its branch and running the repro
against it. This PR fixes what #32858 leaves behind, and is independent of it (they touch
adjacent lines in
handle_subscribe_response, so whichever lands second needs a trivialrebase).
[review] gate passed · iteration 1 · 6 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 1 passed · 0 rejected · iteration 1
evidence per changed file