Repository navigation
Fix ZREMRANGEBYRANK missing reply and unnormalized negative ranks - #2028
Merged
kevin-montrose merged 3 commits intoAug 7, 2026
Merged
kevin-montrose merged 3 commits into
kevin-montrose merged 3 commits into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes ZREMRANGEBYRANK compatibility and protocol correctness by normalizing rank ranges (including negative and out-of-bounds values) and guaranteeing a single integer reply is always written, preventing client desynchronization and avoiding unintended deletions.
Changes:
- Normalize
start/stopranks (shift negatives, floor/clamp, detect empty ranges) and always write:0for empty/out-of-bounds ranges. - Add regression tests covering
start > stop, overly-negative ranks, and the “no reply” pipeline desync case.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
test/standalone/Garnet.test.collections/RespSortedSetTests.cs |
Adds raw-RESP regression coverage for empty/out-of-bounds rank ranges and pipelining reply correctness. |
libs/server/Objects/SortedSet/SortedSetObjectImpl.cs |
Fixes rank normalization and ensures ZREMRANGEBYRANK always writes exactly one integer reply, matching Redis empty-range behavior. |
ZREMRANGEBYRANK returned early without writing anything when start was past the end of the set, leaving the RespMemoryWriter empty. The command emitted zero bytes and desynchronized the connection, so the next reply was consumed as the answer to ZREMRANGEBYRANK. Negative ranks were also shifted by the set size without being floored at 0, and start > stop was never rejected, so the count written back was computed from unclamped indexes. ZREMRANGEBYRANK board 2 0 answered :-1 and ZREMRANGEBYRANK board -100 -50 answered :51 while emptying a 3 element set. Normalize the indexes the way Redis does in zremrangeGenericCommand: shift negatives by the set size, floor start at 0, reply :0 for an empty range, then clamp stop to count - 1. elementCount now always matches the number of elements actually removed.
hexonal (hexonal)
force-pushed
the
fix-zremrangebyrank-rank-normalization
branch
from
August 7, 2026 05:25
3b69a89 to
8757fa5
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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
ZREMRANGEBYRANKmishandles every rank range that Redis treats as empty or out of bounds. On a three-element set:The
-100 -50case is silent data loss. The3 5case desynchronizes the connection: onGarnetStatus.OKthe RESP layer forwards whatever the object wrote (libs/server/Resp/Objects/SortedSetCommands.cs:785,ProcessOutput), and the object wrote nothing, so a pipelining client reads the next command's reply as the answer toZREMRANGEBYRANK.Root cause
libs/server/Objects/SortedSet/SortedSetObjectImpl.cs:584-594Negative indexes are shifted by the set size but never floored at 0,
start > stopis never rejected, and the early return leaves theRespMemoryWriterempty.elementCountis then computed from unclamped indexes, so it no longer describes whatSkip(start).Take(elementCount)actually removed:-100 -50givesstart = -97,stop = -47,elementCount = 51, andSkip(-97)is a no-op, so all three members go.Fix
Normalize the indexes before use, in the order
ListObjectImpl.ListRangealready uses forLRANGE(libs/server/Objects/List/ListObjectImpl.cs:143-150): shift negatives by the count, floorstartat 0, reply:0for an empty range, then clampstoptocount - 1. It is also the orderzremrangeGenericCommanduses in Redis, which is worth naming here because the reply values themselves are the compatibility question.After clamping,
0 <= start <= stop <= count - 1always holds, soelementCountequals the number of members actually removed and every path writes exactly one reply. Thecount == 0case (a set left empty byDeleteExpiredItems) now also replies:0instead of nothing.Test
CanDoZRemRangeByRankintest/standalone/Garnet.test.collections/RespSortedSetTests.cs, using raw RESP throughLightClientRequest— a missing or duplicated reply is invisible to a StackExchange.Redis assertion.Precisely what is red today, and what is not:
ZREMRANGEBYRANK board 2 0expecting:0— fails on main (:-1). It is the first new assertion, so it is what a revert reports.ZREMRANGEBYRANK board -100 -50expecting:0— also wrong on main (:51, andboardis emptied), but the run aborts at the assertion above before reaching it.ZCARD boardexpecting:3— regression guard.SendCommands("ZREMRANGEBYRANK board 3 5", "PING")expecting:0\r\n+PONG\r\n— regression guard, not a red signal. On main that command writes nothing, soLightClient.CompletePendingRequestsspins on a token that never arrives and the test hangs rather than fails. That is why it is ordered last, behind an assertion that fails cleanly.None of the new commands remove anything, so the pre-existing assertions after them are unaffected.