Skip to content

client cache: opt-in / opt-out by mode and by flag (CacheOptIn, CacheOptOut, TrackingOptIn, TrackingByPrefix) - #3297

Merged
mgravell merged 2 commits into
v4from
v4-cache-optin
Oct 23, 2026
Merged

mgravell merged 2 commits into
v4from
v4-cache-optin

Conversation

@mgravell

Copy link
Copy Markdown
Collaborator

Client-side caching gains a real opt-in / opt-out story, on both halves: what the cache keeps locally, and what the server is told to track.

Per-command flags

CommandFlags.NoClientCache (unshipped) is replaced by a two-bit field:

  • CacheOptOut (bit 20): not served from or stored in the cache; under Tracking the server is also told not to track the key.
  • CacheOptIn (bit 21): stored on first miss, skipping admission; under the OPTIN modes it is what makes a read cacheable at all, and the server is told to track it.
  • Both set: reserved, rejected with ArgumentException at the send funnel, cache or no cache.

Bit 19 was shared with the internal probe flag; the move resolves that. The command surface's "never cached" marker (random members, TTLs) is now a separate internal bit with no wire effect, so a CLIENT CACHING NO is not paid per HPTTL.

Tracking modes

mode negotiates unflagged read notes
Tracking (default) ON OPTOUT cached, tracked measured identical to plain ON on Redis 8.9, except that it permits CACHING NO
TrackingByPrefix (default when Prefixes set) ON OPTIN inside Prefixes: as CacheOptIn; outside: plain Prefixes stay client-side (PREFIX needs BCAST); the flags override the list
TrackingOptIn ON OPTIN plain, not cached opt-in by flag alone
Broadcast ON BCAST [PREFIX ...] cached if inside Prefixes flags keep their local meaning only; the list is final

Default with Prefixes now resolves to TrackingByPrefix rather than Broadcast, for the same reason Default moved off broadcast earlier: broadcast's cost is set by the server's write rate, the new mode's by this client's own misses. Broadcast remains available by name. Consequence: with prefixes set, a read inside them is cached on its first miss rather than its second.

How CLIENT CACHING gets on the wire

A request can carry a prefix: one complete frame that the connection writes immediately before the request, under the same write lock, enqueueing a per-connection discard sink for its reply (IRespMessage.Prefix, RespPrefixReplySink). Adjacency therefore holds by construction under every write path: single sends, pairs behind SELECT/ASKING/SCRIPT LOAD, batch runs, and the inbox drain. An error reply to a prefix is swallowed and counted (RespConnection.PrefixErrors, LastPrefixError), so a refused modifier degrades a read to uncached rather than failing it. The prefix is not part of the cache key.

Where frames go: YES ahead of the read that fills (and a refresh) under the OPTIN modes; NO ahead of a CacheOptOut read under Tracking, and ahead of a transaction's MULTI when the transaction contains a read, since reads inside MULTI are never cached. Never ahead of a write, never inside MULTI, never under Broadcast.

A RESP3 push arriving between the prefix's +OK and the body's reply consumes nothing: pushes are classified before any dequeue. Covered by a RESPite test with the raw frame order and an in-proc test that forces the interleave through a new toy-server hook.

High-integrity mode: ConfigurationOptions.HighIntegrity is not consumed by the v4 core yet, so there is nothing to interact with; if it is later built as a trailing per-command frame, the same stage-and-sink mechanism serves it.

Toy server

Models OPTIN/OPTOUT, CLIENT CACHING YES|NO with the real server's error strings, the "next command consumes it, MULTI carries it" rule, and exposes per-client counters, IsTrackingKey, and an AfterReply hook.

Tests

RESPite 1960 passed (6 new). Main suite net10.0: 7773 passed, 166 skipped. New coverage: every cell of the mode-by-flag table, admission bypass, the reserved encoding, the transaction prefix, a renamed CLIENT, prefix-not-in-key, OPTOUT/OPTIN negotiation, the OPTIN and by-prefix flows end to end with invalidation, the flag overrides under TrackingByPrefix, the transaction opt-out, and the forced push interleave.

Docs

docs/ClientSideCaching.md (modes, flag table, admission, intro example), design/client-side-cache.md §6/§7, flag rename in docs/Extending.md and design/command-flow.md.

…OptIn/CacheOptOut (two bits, both-set reserved) replace NoClientCache; Tracking negotiates OPTOUT, new TrackingOptIn negotiates OPTIN, new TrackingByPrefix (OPTIN, Prefixes applied client-side, flags win) is what Prefixes now select by default; CLIENT CACHING YES|NO travels as a per-request prefix frame written as one unit with the request (RESPite IRespMessage.Prefix + discard sink); a transaction that reads is sent behind CACHING NO under OPTOUT
…ases (the second zero-valued name, PreferMaster, is appended to every non-zero value on .NET Framework)
@mgravell
mgravell merged commit d6c6bed into v4 Oct 23, 2026
4 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.

1 participant