Skip to content

WithRetry, WithKeyPrefix and the group database ride the context/executor core - #3338

Merged
mgravell merged 2 commits into
v4from
marc/v4-database-cleanup
Nov 13, 2026
Merged

mgravell merged 2 commits into
v4from
marc/v4-database-cleanup

Conversation

@mgravell

@mgravell mgravell commented Nov 6, 2026

Copy link
Copy Markdown
Member

First of a two-PR series (followed by the transaction-retry PR) replacing the old-API wrapper databases with context/executor composition. Net: +691 / −7,177 across 44 files.

What goes

  • RetryDatabase: WithRetry(IDatabaseAsync) now re-contexts via RespRetryExecutor, a decorator on the executor chain that replays the already-rendered frame — no capture, no generated per-command state. asyncState is now carried (previously dropped); a foreign IDatabaseAsync passes through unchanged. RetryTransaction stays for now, built by CreateTransaction when the chain retries — it goes in the follow-up PR.
  • The KeyPrefixed* family (KeyPrefixedDatabase/KeyPrefixedBatch/KeyPrefixedTransaction/KeyPrefixed<T> + partials): WithKeyPrefix(IDatabase) re-contexts via AppendKeyPrefix + AppendChannelPrefix, so the prefix is applied at render time. A foreign IDatabase now throws NotSupportedException — silently not prefixing is the wrong keyspace.
  • MultiGroupDatabase's 628 generated members: it is now a slim RedisDatabase subclass over RespGroupExecutor. Scans pin a whole enumeration to the member active at call time (a cursor only means something to the server that issued it); GetDatabase(-1) resolves from configuration, with members validated to agree on DefaultDatabase.

Gaps closed along the way

The render-time prefix misses anything that doesn't travel as a key hole, so the places the old wrapper handled specially are handled explicitly:

  • Lua scripts: @key in a LuaScript becomes ARGV[n] — a value. Extraction prefixes both the KEYS array and the ARGV copies, and the send goes through an unprefixed context view (WithoutKeyPrefix), else KEYS would be double-prefixed. This was a real bug caught by the existing ScriptingTests.
  • SORT: BY/GET patterns map under the prefix (nosort/# exempt), as the old wrapper's SortByToInner/SortGetToInner did.
  • RANDOMKEY refuses under a prefix (NotSupportedException), matching the old wrapper.
  • Routing questions (IsConnected, IdentifyEndpoint*, GetConnectionState) ask about the prefixed key.
  • AppendKeyPrefix now carries the topology cell + cache resolver it previously dropped.

Tests

The five member-by-member NSubstitute wrapper files (~4,100 lines) are replaced by KeyPrefixContextTests: RESP-measuring tests over the FakeExecutor harness that assert the actual bytes on the wire (prefix composition/nesting, SORT patterns, script KEYS vs ARGV, refusals, routing, the async surface). Full net10.0 suite green against the docker topology.

…utor core: RetryDatabase and the KeyPrefixed* family go

- WithRetry(IDatabaseAsync) re-contexts via RespRetryExecutor (asyncState now carried; foreign database passes through unchanged); RetryTransaction stays, built by CreateTransaction when the chain retries
- MultiGroupDatabase is a slim RedisDatabase over RespGroupExecutor; the 628 generated members go, scans pin to the active member, GetDatabase(-1) resolves from configuration
- WithKeyPrefix(IDatabase) re-contexts via AppendKeyPrefix + AppendChannelPrefix and THROWS for a foreign database (silently not prefixing is the wrong keyspace); KeyPrefixed*/KeyPrefixedBatch/KeyPrefixedTransaction deleted
- gaps closed: RANDOMKEY refuses under a prefix; SORT BY/GET patterns map (nosort/# exempt); Lua @key ARGV copies prefix via extraction over a WithoutKeyPrefix view; routing questions ask about the prefixed key; AppendKeyPrefix carries topology + cache resolver
- the member-by-member wrapper tests (~4,100 lines) become KeyPrefixContextTests over the FakeExecutor RESP-measuring harness
… RetryTransaction and the [AutoDatabase] generator go (#3339)

RespRetryTransactionExecutor records each queued command's rendered RespRequest plus a durable
caller proxy, then replays the frames into a fresh one-shot RespTransactionExecutor per attempt,
sharing the database's RetryController. Fresh attempt operations per replay are what dodge the
real blocker: enrolled pooled operations are faulted directly by connection teardown, so
re-enrolling one races pool recycling.

With its last consumer gone, the capture-and-replay tier is deleted whole: RetryTransaction,
AutoDatabase.cs, CapturedArgs.cs, the AutoDatabaseGenerator, SER352, and IInternalTransaction.
Nothing on IDatabase is source-generated any more.

Deliberate behavior changes: a retrying transaction now carries asyncState (previously refused)
and exposes a working context that queues into the recorded unit (previously threw). Reuse after
execute is still refused. Fire-and-forget parity is now real rather than claimed: both transaction
executors hand every declined reply the one shared completed-default task, and TaskBridge passes a
completed fire-and-forget task through untouched instead of rewrapping it.
@mgravell
mgravell merged commit f37fdae into v4 Nov 13, 2026
1 of 3 checks passed
@mgravell
mgravell deleted the marc/v4-database-cleanup branch November 13, 2026 11:52
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