Repository navigation
Add Redis Cluster support - #75
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds Redis Cluster support. It adds slot calculation, seed and topology management, redirect handling, cluster-aware commands, batch and transaction routing, configuration parsing, tests, documentation, and benchmarks. ChangesRedis Cluster support
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant RespireClient
participant ClusterRouter
participant RedisNode
RespireClient->>ClusterRouter: Select connection for command slot
ClusterRouter->>RedisNode: Send command
RedisNode-->>RespireClient: Result or MOVED/ASK response
RespireClient->>ClusterRouter: Resolve redirect
ClusterRouter-->>RespireClient: Target connection
RespireClient->>RedisNode: ASKING and retry command
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
🧪 Integration Test Results (net10.0)ℹ️ Execution Details
Automated integration testing with Redis containers using TUnit |
🧪 Integration Test Results (net8.0)ℹ️ Execution Details
Automated integration testing with Redis containers using TUnit |
Redis Container Benchmark Results (net8.0)BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat) Runtime=.NET 8.0 IterationCount=3 WarmupCount=1
View raw benchmark output |
Redis Container Benchmark Results (net10.0)BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat) Runtime=.NET 8.0 IterationCount=3 WarmupCount=1
View raw benchmark output |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9cb04c9e0b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| public readonly int RoutingKeyIndex; | ||
|
|
||
| public Verb(params string[] words) | ||
| public Verb(params string[] words) : this(0, words) |
There was a problem hiding this comment.
Traverse every master when scanning a cluster
Treating argument zero as the routing key by default makes ScanAsync hash its cursor and potentially send each page to a different master. Redis SCAN cursors are local to one node, so a cursor returned by one master is not valid traversal state for another; on a multi-master cluster this can omit keys, duplicate pages, or terminate before all nodes are visited. Cluster scans need to iterate each master independently rather than slot-route the cursor.
Useful? React with 👍 / 👎.
| var askingReply = await ClusterRouter.SendAskingAsync(connection, cancellationToken) | ||
| .ConfigureAwait(false); | ||
| askingReply.Dispose(); |
There was a problem hiding this comment.
Enqueue ASKING and its redirected command atomically
When an ASK redirect occurs under concurrent traffic, awaiting ASKING before separately enqueueing the original command allows another caller's command to be inserted between them on this multiplexed connection. Redis applies ASKING only to the immediately following command, so the unrelated command can consume the state and the redirected operation can repeatedly fail during slot migration. Both frames need to be appended atomically on the selected connection.
Useful? React with 👍 / 👎.
| await EnsureConnectedAsync(cancellationToken).ConfigureAwait(false); | ||
| var node = slot is { } value ? Volatile.Read(ref _slots[value]) : null; | ||
| node ??= Volatile.Read(ref _seed)!; |
There was a problem hiding this comment.
Use cached slot routes when a seed is unavailable
After topology has been loaded, every keyed command still reconnects the configured seed before consulting _slots. If the seed node is down while the cached slot owner remains healthy, EnsureConnectedAsync fails and prevents otherwise routable cluster traffic from reaching that owner, turning a single seed outage into a client-wide outage. For known slots, resolve and connect the cached node first and require seed discovery only when no route is available.
Useful? React with 👍 / 👎.
| public static readonly Verb XAck = new("XACK"); | ||
| public static readonly Verb XGroupCreate = new("XGROUP", "CREATE"); | ||
| public static readonly Verb XReadGroup = new("XREADGROUP"); | ||
| public static readonly Verb XReadGroup = new(7, "XREADGROUP"); |
There was a problem hiding this comment.
Route XREADGROUP using the stream key
In the argument array built by ReadGroupAsync, index 7 is the literal STREAMS token and the actual stream key is at index 8. Hashing index 7 therefore sends every polling iteration to the node owning the slot for "STREAMS"; Redis must redirect it to the real key's owner each time, and the learned route is never used because subsequent calls hash the same literal again. Set the routing index to 8.
Useful? React with 👍 / 👎.
| connection ??= await _client.AcquireConnectionAsync( | ||
| _hasClusterSlot ? _clusterSlot : null, token) | ||
| .ConfigureAwait(false); | ||
| return await connection.SendTransactionAsync(_buffer.WrittenMemory, _ops.Count, token).ConfigureAwait(false); |
There was a problem hiding this comment.
Follow cluster redirects for transactions
A transaction is sent directly on the cached slot connection without the redirect handling used by ordinary cluster commands. When ownership changes after CLUSTER SLOTS was loaded, MULTI and its queued command reach the former owner and the commit surfaces MOVED, ASK, or EXECABORT instead of retrying against the new owner, despite cluster redirects being handled elsewhere. Same-slot transactions therefore fail during normal resharding or failover until another command refreshes the route.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/Respire/Internal/ClientCore.cs (1)
28-33: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winA failing
ClusterRouterconstructor leaks the multiplexer and the dedicated pool.
ClusterRouterthrowsArgumentExceptionwhenoptions.Database != 0. At that pointMultiplexerandDedicatedPoolare already constructed, and theClientCoreconstructor unwinds without returning an instance. No caller can dispose them, so both the sockets and the pooled buffers leak. Validate the cluster preconditions before you open any connection.🛡️ Proposed fix
public ClientCore(RespireOptions options) { + if (options.Cluster && options.Database != 0) + { + throw new ArgumentException("Redis Cluster supports database 0 only.", nameof(options)); + } + Options = options; Logger = options.CreateLogger("Respire.RespireClient");🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Respire/Internal/ClientCore.cs` around lines 28 - 33, Validate the cluster-specific preconditions, including the supported options.Database value, before constructing RespireConnectionMultiplexer or DedicatedConnectionPool in the ClientCore constructor. Ensure ClusterRouter is only created after these checks pass, so invalid cluster configuration throws before any connection resources are opened.
🧹 Nitpick comments (5)
src/Respire/Internal/ClusterRouter.cs (1)
357-357: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueDisposing
_seedGatecan race an in-flightEnsureConnectedAsync.
DisposeAsyncdisposes_seedGatewhile another caller can still hold it. Disposing aSemaphoreSlimdoes not release pending waiters, and thefinally { _seedGate.Release(); }inEnsureConnectedAsyncthen throwsObjectDisposedExceptionthat replaces the real outcome.SemaphoreSlimrequires disposal only whenAvailableWaitHandlewas used, and this code never reads it. Drop the call.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Respire/Internal/ClusterRouter.cs` at line 357, Remove the _seedGate.Dispose() call from DisposeAsync. Keep _seedGate undisposed so an in-flight EnsureConnectedAsync can complete its finally release without ObjectDisposedException.src/Respire/RespireOptions.cs (1)
220-222: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winA connection string cannot express more than one seed.
Parsealways returns a single endpoint fromuri.Hostanduri.Port(Line 230).ClusterRoutertreatsEndpointsas the seed list and fails over across it, so seed failover is unreachable for users who configure Respire with a connection string. Consider accepting a comma-separated host list in the authority, or state in theParsedocumentation that multiple seeds requireEndpoints.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Respire/RespireOptions.cs` around lines 220 - 222, Update the connection-string parsing flow in RespireOptions.Parse to support multiple comma-separated hosts in the authority and populate the full seed list used by ClusterRouter, while preserving the existing single-host behavior; alternatively, document in Parse that connection strings support only one seed and multiple seed failover requires the Endpoints configuration.src/Respire/Protocol/RespWriter.cs (1)
101-105: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winAvoid boxing default
IRespCommand.TryGetClusterSlotduring cluster routing.
SendClusterAsyncandSendBlockingClusterAsynccall this member onwhere TCommand : struct, IRespCommand. Structs without an explicit implementation, such asCmd, use default interface dispatch and can allocate one box per cluster-routed send. Add an explicit no-slot implementation to command structs used by these paths. Keep the default for external custom commands.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Respire/Protocol/RespWriter.cs` around lines 101 - 105, Add explicit TryGetClusterSlot implementations returning false to the built-in command structs used by SendClusterAsync and SendBlockingClusterAsync, including Cmd, so generic cluster routing avoids default interface dispatch and boxing. Leave IRespCommand.TryGetClusterSlot’s default implementation unchanged for external custom commands.src/Respire/RespireClient.cs (2)
623-628: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTelemetry records the seed endpoint, not the routed node.
RespireTelemetry.StartOperationreceivescore.Multiplexer.Hostandcore.Multiplexer.Port. In cluster mode the command runs on a slot-owning node frompool, which can be a different node. Lines 536-537 and 859-860 changed the other paths to record the real connection endpoint. This path keeps the seed endpoint, so blocking-command spans in cluster mode point at the wrong server.The connection is not known before
pool.RentAsync. Consider movingStartOperationafter the first rent, or resolving the endpoint frompool.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Respire/RespireClient.cs` around lines 623 - 628, Update the telemetry setup in the blocking-command path to use the routed connection’s actual endpoint rather than core.Multiplexer.Host and core.Multiplexer.Port. Move RespireTelemetry.StartOperation until after pool.RentAsync or resolve the endpoint from the rented connection, while preserving the existing operation, database, and storedProcedureName values.
778-791: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueReuse the sliced script tail
BuildScriptTailalways creates at least one element, sotail[0]is safe on both paths. Createtail[1..]once beforetryand reuse it for theEVALSHAandEVALcommands.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Respire/RespireClient.cs` around lines 778 - 791, In the script execution flow around BuildScriptTail, compute tail[1..] once before the try block and reuse that sliced tail for both SendClusterAsync calls. Keep tail[0] as the key-count argument in the EVALSHA and EVAL command construction, relying on BuildScriptTail’s guaranteed non-empty result.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Around line 87-110: Update the Redis Cluster section near the existing WATCH
limitation to state that cluster sharded pub/sub is unsupported, including that
SSUBSCRIBE is unavailable in cluster mode.
In `@src/Respire/Commands/CommandStructs.cs`:
- Around line 19-20: Update Verbs.XReadGroup.RoutingKeyIndex to 8 so cluster
routing hashes the resolved stream key placed at argument index 8 by
ReadGroupAsync, rather than the "STREAMS" marker at index 7. Leave the existing
Eval and EvalSha Cmd2N routing behavior unchanged.
In `@src/Respire/Commands/Verbs.cs`:
- Line 126: Update the XReadGroup routing key index from 7 to 8 in the Verb
declaration, preserving the existing XREADGROUP verb name and ensuring it
matches ReadGroupAsync’s argument layout where index 7 is “STREAMS”.
In `@src/Respire/Internal/ClusterRouter.cs`:
- Around line 202-205: Update SendAskingAsync and the SendClusterAsync
redirect-retry flow so ASKING and the redirected command are submitted as one
indivisible connection-level batch, or execute both on a dedicated connection.
Do not enqueue ASKING and then wait for its reply before enqueueing the
redirected command; preserve the existing retry behavior while preventing other
multiplexed callers from interposing commands.
In `@src/Respire/RespireBatch.cs`:
- Around line 113-128: Update the XML documentation for the public SendAsync
method to describe cluster mode accurately: operations are dispatched
independently through RespireClient.SendAsync rather than sent in one flush,
connection-acquisition failures fault only the affected operation while
SendAsync completes normally and records the first error through telemetry, and
ordering across different slots is not guaranteed.
In `@src/Respire/RespireClient.cs`:
- Around line 646-664: In the error-handling branch of the cluster send flow,
return the healthy connection to the current pool and set returned to true
before throwing any non-redirect server error. Preserve the existing redirect
handling and ensure the catch path does not discard the connection after
ResponseReader.ServerError has consumed the response.
In `@src/Respire/RespireTransaction.cs`:
- Around line 257-268: The cluster transaction commit path lacks handling for
MOVED redirects when its slot-to-node mapping is stale. Update CommitAsync to
retry the complete transaction against the redirected owner, using the existing
ClusterRouter.RedirectLimit behavior and refreshing the slot mapping before
retrying; preserve the same-slot validation in the shown transaction setup.
In `@tests/Respire.Tests/Networking/ClusterTests.cs`:
- Around line 164-183: Update Connect_TriesLaterSeedWhenFirstIsUnavailable so
the fallback FakeRespServer returns a valid RESP CLUSTER SLOTS topology
identifying an advertised node, rather than only PONG. After connection, execute
a keyed command routed through that topology and retain assertions confirming
the fallback seed handled CLUSTER SLOTS and the keyed request.
- Around line 48-68: Extend AskRedirect_SendsAskingOnTargetWithoutCachingSlot
with a second GetStringAsync("key") call, then assert the seed receives the
second GET request and the target receives ASKING followed by GET key again,
confirming the ASK redirect target is not cached.
---
Outside diff comments:
In `@src/Respire/Internal/ClientCore.cs`:
- Around line 28-33: Validate the cluster-specific preconditions, including the
supported options.Database value, before constructing
RespireConnectionMultiplexer or DedicatedConnectionPool in the ClientCore
constructor. Ensure ClusterRouter is only created after these checks pass, so
invalid cluster configuration throws before any connection resources are opened.
---
Nitpick comments:
In `@src/Respire/Internal/ClusterRouter.cs`:
- Line 357: Remove the _seedGate.Dispose() call from DisposeAsync. Keep
_seedGate undisposed so an in-flight EnsureConnectedAsync can complete its
finally release without ObjectDisposedException.
In `@src/Respire/Protocol/RespWriter.cs`:
- Around line 101-105: Add explicit TryGetClusterSlot implementations returning
false to the built-in command structs used by SendClusterAsync and
SendBlockingClusterAsync, including Cmd, so generic cluster routing avoids
default interface dispatch and boxing. Leave IRespCommand.TryGetClusterSlot’s
default implementation unchanged for external custom commands.
In `@src/Respire/RespireClient.cs`:
- Around line 623-628: Update the telemetry setup in the blocking-command path
to use the routed connection’s actual endpoint rather than core.Multiplexer.Host
and core.Multiplexer.Port. Move RespireTelemetry.StartOperation until after
pool.RentAsync or resolve the endpoint from the rented connection, while
preserving the existing operation, database, and storedProcedureName values.
- Around line 778-791: In the script execution flow around BuildScriptTail,
compute tail[1..] once before the try block and reuse that sliced tail for both
SendClusterAsync calls. Keep tail[0] as the key-count argument in the EVALSHA
and EVAL command construction, relying on BuildScriptTail’s guaranteed non-empty
result.
In `@src/Respire/RespireOptions.cs`:
- Around line 220-222: Update the connection-string parsing flow in
RespireOptions.Parse to support multiple comma-separated hosts in the authority
and populate the full seed list used by ClusterRouter, while preserving the
existing single-host behavior; alternatively, document in Parse that connection
strings support only one seed and multiple seed failover requires the Endpoints
configuration.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 826861bc-3108-47cb-8c1f-45c2239c5010
📒 Files selected for processing (18)
README.mdbenchmarks/Respire.Benchmarks/RespireKeyBenchmarks.csdocs/API_DESIGN.mddocs/WHY_RESPIRE.mdsrc/Respire/Commands/CommandStructs.cssrc/Respire/Commands/Verbs.cssrc/Respire/Internal/ClientCore.cssrc/Respire/Internal/ClusterHash.cssrc/Respire/Internal/ClusterRouter.cssrc/Respire/Protocol/RespWriter.cssrc/Respire/PubSub/SubscriptionHub.cssrc/Respire/RespireBatch.cssrc/Respire/RespireClient.cssrc/Respire/RespireKey.cssrc/Respire/RespireOptions.cssrc/Respire/RespireTransaction.cssrc/Respire/RespireValue.cstests/Respire.Tests/Networking/ClusterTests.cs
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/Respire/Internal/ClusterRouter.cs`:
- Around line 89-100: Before fallback selection in ClusterRouter’s cached-owner
path (src/Respire/Internal/ClusterRouter.cs#L89-L100), invalidate the failed
slot owner conditionally or force a slot refresh so the fallback can use a seed
and receive updated MOVED information; apply the same change before selecting
the dedicated pool in src/Respire/Internal/ClusterRouter.cs#L133-L144.
In `@tests/Respire.Tests/Networking/FakeRespServer.cs`:
- Around line 139-147: Update the reply-flushing condition in the connection
handling logic to compare pendingReplies.Count with MinimumCommandsBeforeReply
instead of the global CommandsSeen value, while preserving the existing reply
iteration and queue-clearing behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 330815a5-0d38-417a-99aa-5f052db6af29
📒 Files selected for processing (13)
README.mdsrc/Respire/Commands/CommandStructs.cssrc/Respire/Commands/Verbs.cssrc/Respire/Facets/KeyCommands.cssrc/Respire/Internal/ClientCore.cssrc/Respire/Internal/ClusterRouter.cssrc/Respire/Networking/RespireConnection.cssrc/Respire/RespireBatch.cssrc/Respire/RespireClient.cssrc/Respire/RespireOptions.cssrc/Respire/RespireTransaction.cstests/Respire.Tests/Networking/ClusterTests.cstests/Respire.Tests/Networking/FakeRespServer.cs
🚧 Files skipped from review as they are similar to previous changes (5)
- src/Respire/Internal/ClientCore.cs
- src/Respire/RespireOptions.cs
- src/Respire/RespireBatch.cs
- src/Respire/Commands/CommandStructs.cs
- src/Respire/RespireClient.cs
🧪 Integration Test Results (net10.0)ℹ️ Execution Details
Automated integration testing with Redis containers using TUnit |
🧪 Integration Test Results (net8.0)ℹ️ Execution Details
Automated integration testing with Redis containers using TUnit |
Redis Container Benchmark Results (net10.0)BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat) Runtime=.NET 8.0 IterationCount=3 WarmupCount=1
View raw benchmark output |
Redis Container Benchmark Results (net8.0)BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat) Runtime=.NET 8.0 IterationCount=3 WarmupCount=1
View raw benchmark output |
🧪 Integration Test Results (net10.0)ℹ️ Execution Details
Automated integration testing with Redis containers using TUnit |
🧪 Integration Test Results (net8.0)ℹ️ Execution Details
Automated integration testing with Redis containers using TUnit |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/Respire.Tests/Networking/ClusterTests.cs`:
- Around line 281-293: Update the retry loop around client.GetStringAsync so it
performs only one request after the cached owner becomes unavailable, and assert
that the returned value is "fallback". Remove the repeated exception-swallowing
and delay behavior so the test verifies fallback during the failed operation
itself.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 01c52725-bbd0-493d-9b00-d20581816ec0
📒 Files selected for processing (8)
README.mddocs/API_DESIGN.mddocs/WHY_RESPIRE.mdsrc/Respire/Internal/ClusterRouter.cssrc/Respire/Networking/RespireConnection.cssrc/Respire/RespireOptions.cstests/Respire.Tests/Networking/ClusterTests.cstests/Respire.Tests/Networking/FakeRespServer.cs
🚧 Files skipped from review as they are similar to previous changes (5)
- docs/WHY_RESPIRE.md
- README.md
- src/Respire/Networking/RespireConnection.cs
- src/Respire/RespireOptions.cs
- src/Respire/Internal/ClusterRouter.cs
Redis Container Benchmark Results (net8.0)BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat) Runtime=.NET 8.0 IterationCount=3 WarmupCount=1
View raw benchmark output |
Redis Container Benchmark Results (net10.0)BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat) Runtime=.NET 8.0 IterationCount=3 WarmupCount=1
View raw benchmark output |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5d9feb7c89
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (core.Cluster is { } cluster) | ||
| { | ||
| var arguments = tail[1..]; | ||
| RespValue clusterReply; | ||
| try | ||
| { | ||
| clusterReply = await SendClusterAsync( |
There was a problem hiding this comment.
Route tracked cache scripts through the cluster router
When Respire.Extensions.Caching uses a real client whose ACL permits CLIENT ID (the normal default), GetTrackedWireAsync selects StartTrackedScriptExecution for cache reads and writes, bypassing this new cluster-aware ExecuteScriptAsync branch and sending the script directly through the seed multiplexer. For any cache key owned by another master, GET/SET/refresh therefore surface MOVED instead of following it; the tracked execution path must acquire its connection by the script key's slot and handle redirects too.
Useful? React with 👍 / 👎.
| var reply = await (sendAsking | ||
| ? ClusterRouter.SendAskingTransactionAsync( | ||
| connection, _buffer.WrittenMemory, _ops.Count, token) | ||
| : connection.SendTransactionAsync(_buffer.WrittenMemory, _ops.Count, token)) | ||
| .ConfigureAwait(false); |
There was a problem hiding this comment.
Preserve queue-time redirects when retrying transactions
When ownership changes before commit, Redis returns MOVED/ASK for the keyed command while it is being queued inside MULTI, then returns EXECABORT from EXEC. SendTransactionAsync discards the MULTI and queued-command replies, so this new loop sees only EXECABORT, which is not recognized as a redirect and cannot be retried. Fresh evidence in this revision is that the redirect loop consumes only the final reply, while Transaction_FollowsMovedRedirectAsOneUnit unrealistically scripts MOVED as that final reply; queue-time errors must be preserved or routing must be validated before entering MULTI.
Useful? React with 👍 / 👎.
Route tracked cache scripts and correction fences through their key's node, including redirects. Retain transaction queue errors so MOVED and ASK can retry the full MULTI/EXEC unit.
🧪 Integration Test Results (net10.0)ℹ️ Execution Details
Automated integration testing with Redis containers using TUnit |
🧪 Integration Test Results (net8.0)ℹ️ Execution Details
Automated integration testing with Redis containers using TUnit |
Redis Container Benchmark Results (net10.0)BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat) Runtime=.NET 8.0 IterationCount=3 WarmupCount=1
View raw benchmark output |
Redis Container Benchmark Results (net8.0)BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat) Runtime=.NET 8.0 IterationCount=3 WarmupCount=1
View raw benchmark output |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 883d7a6b51
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| await multiplexer.SendToAllConnectionsAsync( | ||
| new Cmd2N(Verbs.Eval, script.Source, tail[0], tail[1..]), CancellationToken.None).ConfigureAwait(false); |
There was a problem hiding this comment.
Preserve ASKING when broadcasting cache corrections
When a tracked cache SET or sliding-refresh script follows an ASK redirect and then loses or times out waiting for that retry's reply, ConnectionIdentity points at the importing node, but this correction broadcasts a plain EVAL. The importing node requires a fresh one-shot ASKING before every keyed correction, so it returns MOVED instead; RunCorrectionAsync propagates that server error and the uncertain write can retain the stale or overlong TTL the correction was meant to prevent. Send the correction with ASKING on each connection, or fence the original connection and reroute the correction normally.
Useful? React with 👍 / 👎.
| var response = await (sendAsking | ||
| ? ClusterRouter.SendAskingUncheckedAsync(connection, in command, cancellationToken) | ||
| : connection.SendAsync(in command, cancellationToken)) |
There was a problem hiding this comment.
Suppress the response watchdog for cluster blocking calls
When RespireOptions.ResponseTimeout is configured and a cluster blocking operation such as BLPOP legitimately waits longer than that interval, this path uses the ordinary SendAsync/prefixed send instead of SendWithoutResponseTimeoutAsync, so the receive watchdog aborts the dedicated connection and the blocking call fails. The non-cluster path explicitly suppresses this watchdog because blocking is intentional; the cluster path needs the same suppression for both normal and ASKING sends.
Useful? React with 👍 / 👎.
🧪 Integration Test Results (net10.0)ℹ️ Execution Details
Automated integration testing with Redis containers using TUnit |
🧪 Integration Test Results (net8.0)ℹ️ Execution Details
Automated integration testing with Redis containers using TUnit |
Redis Container Benchmark Results (net10.0)BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat) Runtime=.NET 8.0 IterationCount=3 WarmupCount=1
View raw benchmark output |
Redis Container Benchmark Results (net8.0)BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat) Runtime=.NET 8.0 IterationCount=3 WarmupCount=1
View raw benchmark output |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4203a9778f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| var clusterTasks = new Task<Exception?>[_ops.Count]; | ||
| for (var i = 0; i < _ops.Count; i++) | ||
| { | ||
| clusterTasks[i] = _ops[i].RunClusterAsync(_client, cancellationToken); |
There was a problem hiding this comment.
Preserve same-slot batch ordering
When cluster mode uses Connections > 1, launching every operation independently lets two commands for the same slot be round-robined onto different connections to the same master. Redis can then process a queued GetStringAsync(key) before the preceding SetAsync(key), returning stale data even though the batch documentation only permits reordering across different slots. Commands sharing a slot need to retain queue order, for example by grouping them onto one connection.
Useful? React with 👍 / 👎.
| if (_core.Cluster is { } cluster | ||
| && DynamicCommandRouting.IsClusterWideFunctionMutation(command.Name, args)) |
There was a problem hiding this comment.
Fan out raw script-cache mutations
When callers use the advertised catalog API for RespireCommands.Scripting.SCRIPT_LOAD (or raw SCRIPT LOAD/SCRIPT FLUSH) in cluster mode, this condition is false and the unkeyed command reaches only one master. A subsequent EVALSHA on another master can therefore return NOSCRIPT, and SCRIPT FLUSH leaves inconsistent caches. Fresh evidence beyond the fixed IScriptCommands.LoadAsync path is that the raw and catalog branches gate cluster fan-out solely on IsClusterWideFunctionMutation; script-cache mutations need the same treatment.
Useful? React with 👍 / 👎.
🧪 Integration Test Results (net10.0)ℹ️ Execution Details
Automated integration testing with Redis containers using TUnit |
🧪 Integration Test Results (net8.0)ℹ️ Execution Details
Automated integration testing with Redis containers using TUnit |
Redis Container Benchmark Results (net10.0)BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat) Runtime=.NET 8.0 IterationCount=3 WarmupCount=1
View raw benchmark output |
Redis Container Benchmark Results (net8.0)BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat) Runtime=.NET 8.0 IterationCount=3 WarmupCount=1
View raw benchmark output |
🧪 Integration Test Results (net10.0)ℹ️ Execution Details
Automated integration testing with Redis containers using TUnit |
🧪 Integration Test Results (net8.0)ℹ️ Execution Details
Automated integration testing with Redis containers using TUnit |
Redis Container Benchmark Results (net8.0)BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat) Runtime=.NET 8.0 IterationCount=3 WarmupCount=1
View raw benchmark output |
Redis Container Benchmark Results (net10.0)BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat) Runtime=.NET 8.0 IterationCount=3 WarmupCount=1
View raw benchmark output |
Closes #60
Summary
CLUSTER SLOTS, including advertised hostnamesMOVEDandASKredirectsRespireKey.ClusterSlot, connection-stringcluster=true, docs, benchmarks, and deterministic wire testsWATCH, and cluster sharded pub/sub explicitly instead of routing them incorrectlyVerification
dotnet build Respire.sln --no-restoredotnet test Respire.sln --no-build --no-restore— 592 passed across net8.0/net10.0git diff --checkPerformance
Respire_Ping: 390.9 µs / 184 B before; 381.9 µs / 184 B after (network noise, no regression)Summary by CodeRabbit
New Features
ClusterSlotaccess for keys.Bug Fixes
Documentation