Skip to content

fix(protocol): RESP3 keyed sorted-set pops send Double scores (#559) - #573

Merged
TinDang97 merged 1 commit into
mainfrom
fix/559-resp3-double-scores
Aug 19, 2026
Merged

fix(protocol): RESP3 keyed sorted-set pops send Double scores (#559)#573
TinDang97 merged 1 commit into
mainfrom
fix/559-resp3-double-scores

Conversation

@TinDang97

Copy link
Copy Markdown
Collaborator

Closes #559.

Under RESP3, zset scores must be Doubles (,score). BZPOPMIN/BZPOPMAX fell into ZPOPMIN's count-form arm (args.len() >= 2 is true for key timeout), so the 3-element [key, member, score] reply passed through with a BulkString score; ZMPOP/BZMPOP were unclassified.

Through the choke point (resp3.rs): new shapes KeyedScoredFlat + KeyedScoredPairs; the blocking intercept and blocking-in-MULTI arms now route through apply_resp3_conversion like every other arm. Forward-compatible rules pre-registered for ZRANK/ZREVRANK WITHSCORE (#564) and ZADD INCR — inert until those land. Bonus fix: ZREVRANGE arity 4-4 (WITHSCORES inside MULTI was EXECABORT-ing).

Full divergence table measured raw-socket vs Redis 8 oracle semantics on both protocol versions; RESP2 byte-identical everywhere (asserted). Red-first: 5 new integration tests failed on the pre-fix binary. Gates: lib 4714 passed, fidelity 18/18 on both runtimes, fmt/clippy/audits/differ 0. 5 new unwaived client-compat manifest probes exercise the oracle on the compat leg.

Deliberately out of scope (filed as #568): GEOSEARCH/GEORADIUS WITHCOORD coordinates.

@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@TinDang97, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bf58677a-0dda-48a9-8d78-a734883b640f

📥 Commits

Reviewing files that changed from the base of the PR and between c00e6f0 and 0914edd.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • scripts/client-compat/manifest.yaml
  • src/command/metadata.rs
  • src/protocol/resp3.rs
  • src/server/conn/handler_monoio/dispatch.rs
  • src/server/conn/shared.rs
  • tests/resp3_type_fidelity.rs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@TinDang97
TinDang97 force-pushed the fix/559-resp3-double-scores branch from 31210c5 to 637beb4 Compare August 19, 2026 12:00
…h the choke point

BZPOPMIN/BZPOPMAX/ZMPOP/BZMPOP answered `$3\r\n1.5` where redis-server 8.x
answers `,1.5`. Every one of those replies is built by Redis's
`genericZpopCommand`, which emits the score through `addReplyDouble` — the same
call ZPOPMIN uses, and the same call Moon already honoured for ZPOPMIN. Measured
both before and after with a raw-socket type-byte sweep of the whole
score-replying family (RESP2 + RESP3 x standalone/MULTI); the RESP2 column is
identical byte for byte across the change.

Two independent causes, both fixed at the ONE seam rather than per command:

1. Classification (`protocol::resp3::resp3_shape_of`). BZPOPMIN/BZPOPMAX shared
   an arm with ZPOPMIN, whose rule is "a second argument is a COUNT, so the
   reply is pair-wrapped". A blocking pop's second argument is the TIMEOUT, so
   the simplest possible call classified as ScoredPairs — and because the reply
   is the 3-element `[key, member, score]`, the pair-wrapper saw an odd length
   and passed it through untouched. The bug was therefore invisible: a shape was
   assigned, it just could never fire. ZMPOP/BZMPOP had no rule at all. They now
   have their own shapes, KeyedScoredFlat and KeyedScoredPairs, and the inner
   score re-typing reuses `scored_flat`/`bulk_to_double` so ZPOPMIN and the
   keyed forms cannot drift apart.

2. Path (moon#462's class). The monoio handler's blocking branch is an
   INTERCEPT: it short-circuits the dispatch exit where every other reply meets
   the RESP3 policy, and it never applied that policy itself. So on the SHIPPED
   runtime the whole blocking family answered RESP2 shapes to RESP3 clients,
   while the tokio handler — which does convert at its blocking site — was
   right. Fixed by routing the reply through `apply_resp3_conversion`, the same
   choke point, not a second table. The blocking-in-MULTI executor now converts
   too: its "deliberately no conversion here" note was written when the live
   monoio path did not convert either, and the two are equal again with both
   converting, on the side that matches redis-server.

Also pre-registered, inert until the commands land, so the reply is right the
day they do: ZRANK/ZREVRANK WITHSCORE (Double score, moon#521 / PR #564) and
ZADD ... INCR (Double reply). Both classified POSITIONALLY — a member literally
named WITHSCORE or INCR is not the modifier, and `zadd_has_incr` stops scanning
at the end of the leading flag run exactly where Redis's parser does.

Verified unchanged against the oracle: GEODIST (`addReplyDoubleDistance` -> bulk
in both protocols) and ZSCAN scores stay BulkStrings.

Second fix, found by the same sweep: ZREVRANGE carried arity 4 where Redis has
-4. The MULTI queue gate is the only consumer of that number, so the optional
WITHSCORES turned a legal command into a wrong-arity error at QUEUE time and
aborted the whole transaction — while the identical command answered fine
standalone, which is why no suite ever saw it.

Tests (red first; the four integration tests failed with `*3[$]` vs `*3[$|,]`
before the fix):
  tests/resp3_type_fidelity.rs
    r3f14 keyed pops carry a Double standalone, in MULTI and pipelined
    r3f15 RESP2 stays three BulkStrings AND the Double text equals the bulk text
    r3f16 a pop that really BLOCKED and was woken by another client
    r3f17 shape is shard-independent (BZPOPMIN intercept + ZMPOP cross-shard tag)
    r3f18 ZREVRANGE WITHSCORES is queueable inside MULTI
  src/protocol/resp3.rs (unit)
    blocking_pops_are_keyed_not_the_zpopmin_rule
    zrank_withscore_is_positional
    zadd_incr_is_a_double_and_only_in_the_flag_run
    keyed_scored_flat_types_only_the_score
    keyed_scored_pairs_types_every_inner_score

Gates: lib 4714/4714, resp3_type_fidelity 18/18 under BOTH runtimes (monoio and
runtime-tokio), blocking/multi/pubsub/resp integration suites green, fmt, clippy
--all-targets -D warnings, tokio feature check, audit-unsafe, audit-unwrap,
client-compat differ self-tests 34/34.

Refs #559, #462
author: Tin Dang
@TinDang97
TinDang97 force-pushed the fix/559-resp3-double-scores branch from 637beb4 to 0914edd Compare August 19, 2026 16:52
@TinDang97
TinDang97 merged commit 6e7b614 into main Aug 19, 2026
19 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.

RESP3: BZPOPMIN/BZPOPMAX score replies as BulkString, Redis 8 replies Double

1 participant