Fix BLMPOP writing a second reply when force-unblocked via CLIENT UNBLOCK - #2030
Merged
kevin-montrose merged 3 commits intoAug 7, 2026
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a RESP protocol correctness bug in the BLMPOP blocking path where a force-unblock via CLIENT UNBLOCK <id> ERROR could cause BLMPOP to emit two top-level replies (error + null), permanently desynchronizing subsequent replies on that connection.
Changes:
- Add the missing early
return true;inListBlockingPopMultiplewhenIsForceUnblockedis set, preventing the fallthrough null reply. - Add a regression test that reproduces the reply-shift on
main(via a raw RESPLightClientRequest) and verifiesPINGstill returns+PONGafter unblock. - Add coverage for sibling blocking commands to ensure they continue to emit only the unblock error reply.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| test/standalone/Garnet.test/RespTests.cs | Adds regression test ensuring only a single reply is written after CLIENT UNBLOCK ... ERROR, and verifies connection remains in-sync via PING. |
| libs/server/Resp/Objects/ListCommands.cs | Fixes BLMPOP force-unblock handling by returning immediately after writing the unblock error, preventing a stray null reply. |
ListBlockingPopMultiple wrote the -UNBLOCKED error and then fell through to the `!result.Found` branch, emitting a second top-level reply ($-1 on RESP2, _ on RESP3) and desynchronising the connection for every subsequent command. CollectionItemResult.ForceUnblocked leaves Key null, so Found (=> Key != default) is false and execution reached the null-reply path. Return right after writing the error, matching the four sibling handlers: BLPOP/BRPOP and BLMOVE/BRPOPLPUSH in ListCommands.cs, BZPOPMIN/BZPOPMAX and BZMPOP in SortedSetCommands.cs. Adds a regression test that unblocks each of the four blocking commands with CLIENT UNBLOCK <id> ERROR and asserts a following PING is answered with +PONG, which fails on the extra reply.
hexonal (hexonal)
force-pushed
the
fix-blmpop-double-reply-on-unblock
branch
from
August 7, 2026 05:25
284dfdf to
7e052af
Compare
kevin-montrose
approved these changes
Aug 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
A connection blocked in
BLMPOPand released withCLIENT UNBLOCK <id> ERRORreceives two top-level replies for that one command. Every reply on that connection afterwards is shifted by one, permanently.On the wire, the single
BLMPOPproduces:Under RESP3 the stray second reply is
_\r\ninstead of$-1\r\n.BLPOP,BRPOP,BLMOVE,BRPOPLPUSH,BZPOPMIN,BZPOPMAXandBZMPOPare not affected.Root cause
libs/server/Resp/Objects/ListCommands.cs:915-919— the force-unblock branch ofListBlockingPopMultiplewrites the error but does not return:CollectionItemResult.ForceUnblocked(libs/server/Objects/ItemBroker/CollectionItemResult.cs:91) leavesKeynull andIsTypeMismatchfalse, soFound => Key != defaultis false. Execution falls through theIsTypeMismatchcheck intoif (!result.Found) { WriteNull(); return true; }at line 928 and emits the second reply.Fix
Add the missing
return true;. All four sibling blocking handlers already return from this branch:ListCommands.cs:286—ListBlockingPop(BLPOP/BRPOP)ListCommands.cs:378—ListBlockingMove(BLMOVE/BRPOPLPUSH)SortedSetCommands.cs:1563—SortedSetBlockingPop(BZPOPMIN/BZPOPMAX)SortedSetCommands.cs:1671—SortedSetBlockingMPop(BZMPOP)ListBlockingPopMultiplewas the only one of the five missing it. The skipped code is write-only, so nothing else is affected.Test
ClientUnblockWithErrorWritesSingleReplyTestintest/standalone/Garnet.test/RespTests.csblocks a raw-RESP client (LightClientRequest), unblocks it withCLIENT UNBLOCK <id> ERROR, asserts the error is the first reply, then sendsPINGon that same connection and asserts+PONG. ThePINGcheck has to be raw RESP — a StackExchange.Redis multiplexer cannot observe an extra reply.Which assertions are actually red on
main:BLMPOP 10 1 keyA LEFT— the finalPINGassertion fails; the client reads the stray$-1where+PONGbelongs. This is the only new assertion that fails without the production change.BLPOP keyA 10,BLMOVE keyA keyB LEFT LEFT 10,BZMPOP 10 1 keyA MIN— green onmainalready. They are regression guards so the missingreturncannot reappear in the siblings.The
-UNBLOCKEDassertion inside the BLMPOP case also passes onmain; the error is still written first, and the desync only becomes visible on the next command.