Skip to content

RESP3: inline GET fast path replies $-1 instead of _, per-key depending on shard ownership #522

Description

@TinDang97

Under RESP3, GET <missing key> replies $-1 (the RESP2 null bulk) instead of
_. Redis replies _. The inline GET fast path writes the null as a hardcoded
byte string, so it never reaches the serializer that knows the connection's
protocol version.

src/server/conn/blocking.rs:1831:

None => {
    write_buf.extend_from_slice(b"$-1\r\n");
    crate::admin::metrics_setup::record_keyspace_miss();

It is per-KEY, not per-server

At --shards 4 the same command on the same RESP3 connection gives different
answers depending on which shard the key hashes to — keys the connection's own
shard owns take the inline path and get $-1; keys that route elsewhere come
back through the serializer and get _:

HELLO 3
GET m-a (absent) -> b'_\r\n'        <-- remote shard, correct
GET m-b (absent) -> b'$-1\r\n'      <-- local shard, wrong
GET m-c (absent) -> b'$-1\r\n'
GET m-d (absent) -> b'_\r\n'
GET m-e (absent) -> b'_\r\n'
GET m-f (absent) -> b'$-1\r\n'
GET m-g (absent) -> b'_\r\n'
GET m-h (absent) -> b'$-1\r\n'

At --shards 1 every key is local, so it is wrong 8/8.

A client cannot cache "this server speaks RESP3 correctly" — the answer changes
with the key. Any RESP3 decoder that treats an unexpected type as a protocol
error will fail intermittently and unreproducibly.

Every sibling command is correct

Same connection, same server, same absent-key shape:

HGET   absent -> _
ZSCORE absent -> _
LINDEX absent -> _
GETDEL absent -> _

Only GET, and only on its inline path.

Redis reference

redis-server 8.6.1, HELLO 3, GET <absent> -> _\r\n.

This is #462 with a concrete instance

#462 asks to make bypassing the reply-conversion choke point structurally
impossible. This is that bypass, caught in the wild — and it is the second time
the inline GET path has diverged from the dispatched path (the first was the
inline-GET ACL bypass, #457). A fix that only patches line 1831 leaves the class
open; the inline path needs the protocol version, or it needs to stop writing
reply bytes itself.

Scope

  • inline GET miss must emit _ on a RESP3 connection and $-1 on RESP2
  • src/io/static_responses.rs:16 defines NULL_BULK with no callers — either
    route the inline path through it with a RESP3 sibling, or delete it
  • a test that asserts the reply for BOTH protocol versions AND for a local vs a
    remote key at --shards >= 2, because a single-shard test cannot see the split
  • sweep the inline path for other hardcoded reply bytes with the same blindness

Provenance

Found by rna2_resp3_collapses_both_nulls_to_underscore while building the
red suite for #482 — the test asserted GET under RESP3 as a contrast case
to the null array and the contrast failed. Not a null-array bug; filed
separately so #482 stays one contract.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions