From d0e12c641b5451d05861a9a1b5bda7f75ca2c832 Mon Sep 17 00:00:00 2001 From: advaMosh Date: Tue, 21 Jul 2026 09:36:47 +0000 Subject: [PATCH] fix: Queue batch commands to backlog during MOVED reconnection When a MOVED-to-same-endpoint triggers reconnection (PR #3003), batch commands issued during the ~50-100ms reconnection window fail immediately with NoConnectionAvailable. This is because: 1. RedisBatch.Execute() calls SelectServer() which returns null for disconnected/reconnecting servers. 2. PhysicalBridge.TryEnqueue() returns false when !IsConnected, unlike TryWriteAsync/TryWriteSync which queue to the backlog. This creates an inconsistency where individual async commands succeed (queued via BacklogPolicy) but batch commands throw during the same reconnection window. Fix: - RedisBatch.Execute(): When SelectServer returns null and BacklogPolicy.QueueWhileDisconnected is enabled, retry with allowDisconnected=true to find the server endpoint. - PhysicalBridge.TryEnqueue(): When disconnected/reconnecting and BacklogPolicy.QueueWhileDisconnected is enabled, queue messages to the backlog instead of returning false. This matches the existing behavior of individual command paths and ensures batch commands are transparently queued and sent after reconnection completes. Tested with ElastiCache Serverless proxy redirect scenario: - Before: 1-3 NoConnectionAvailable errors per MOVED redirect - After: 0 errors, batch waits for reconnection via backlog --- src/StackExchange.Redis/PhysicalBridge.cs | 30 +++++++++++++++++++++-- src/StackExchange.Redis/RedisBatch.cs | 7 ++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 21047efa2..b92576f03 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -730,13 +730,39 @@ internal bool TryEnqueue(List messages, bool isReplica) if (isDisposed) throw new ObjectDisposedException(Name); - if (!IsConnected) + if (!IsConnected || NeedsReconnect) { + // During reconnection (e.g. MOVED-to-same-endpoint), queue messages to the backlog + // so they can be sent once the connection is re-established. + // This matches the behavior of TryWriteAsync/TryWriteSync for individual commands. + if (Multiplexer.RawConfig.BacklogPolicy.QueueWhileDisconnected) + { + foreach (var message in messages) + { + message.SetEnqueued(null); + BacklogEnqueue(message); + } + StartBacklogProcessor(); + return true; + } return false; } var physical = this.physical; - if (physical == null) return false; + if (physical == null) + { + if (Multiplexer.RawConfig.BacklogPolicy.QueueWhileDisconnected) + { + foreach (var message in messages) + { + message.SetEnqueued(null); + BacklogEnqueue(message); + } + StartBacklogProcessor(); + return true; + } + return false; + } foreach (var message in messages) { // deliberately not taking a single lock here; we don't care if diff --git a/src/StackExchange.Redis/RedisBatch.cs b/src/StackExchange.Redis/RedisBatch.cs index beda224af..9863bc018 100644 --- a/src/StackExchange.Redis/RedisBatch.cs +++ b/src/StackExchange.Redis/RedisBatch.cs @@ -25,6 +25,13 @@ public void Execute() foreach (var message in snapshot) { var server = multiplexer.SelectServer(message); + // If no server found and we're allowed to queue while disconnected (e.g. during + // MOVED-triggered reconnection), retry with allowDisconnected to find the server + // so we can queue batch messages to its backlog. + if (server == null && multiplexer.RawConfig.BacklogPolicy.QueueWhileDisconnected) + { + server = multiplexer.ServerSelectionStrategy.Select(message, allowDisconnected: true); + } if (server == null) { FailNoServer(multiplexer, snapshot);