Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions src/StackExchange.Redis/PhysicalBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -730,13 +730,39 @@ internal bool TryEnqueue(List<Message> 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
Expand Down
7 changes: 7 additions & 0 deletions src/StackExchange.Redis/RedisBatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
64 changes: 64 additions & 0 deletions tests/StackExchange.Redis.Tests/MovedUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,68 @@ public async Task MovedToSameEndpoint_TriggersReconnectAndRetry_CommandSucceeds(
id = await server.ExecuteAsync("client", "id");
log?.WriteLine($"Client id after: {id}");
}

/// <summary>
/// Integration test: Verifies that batch commands issued during a MOVED-triggered
/// reconnection are queued to the backlog and succeed after reconnection completes,
/// rather than throwing NoConnectionAvailable immediately.
///
/// Test scenario:
/// 1. Client connects to test server
/// 2. Client sends a batch containing SET commands, with the trigger key as the last command
/// 3. Server returns MOVED error for the trigger key pointing to same endpoint
/// 4. Client triggers reconnection and queues the MOVED command's retry in the backlog
/// 5. All batch commands complete successfully after reconnection
///
/// Expected behavior:
/// - All batch tasks should complete successfully (no exceptions)
/// - MOVED response count should be 1
/// - Connection count should increase by 1 (reconnection after MOVED)
/// - Values should be stored correctly
/// </summary>
[Theory]
[InlineData(ServerType.Cluster)]
[InlineData(ServerType.Standalone)]
public async Task MovedToSameEndpoint_BatchCommands_QueuedDuringReconnect(ServerType serverType)
{
RedisKey key = Me();

using var testServer = new MovedTestServer(
triggerKey: key,
log: log) { ServerType = serverType, };

await using var conn = await testServer.ConnectAsync(withPubSub: false);
var server = conn.GetServer(testServer.DefaultEndPoint);
await server.PingAsync(); // init everything
Assert.Equal(serverType, server.ServerType);
var db = conn.GetDatabase();

// Record baseline counters
Assert.Equal(0, testServer.SetCmdCount);
Assert.Equal(0, testServer.MovedResponseCount);
var initialConnectionCount = testServer.TotalClientCount;

// Create a batch: normal commands + trigger key as last command
var batch = db.CreateBatch();
var setTask1 = batch.StringSetAsync("normalkey1", "value1");
var setTask2 = batch.StringSetAsync("normalkey2", "value2");
var triggerTask = batch.StringSetAsync(key, "triggervalue"); // this will get MOVED
batch.Execute();

// All tasks should complete successfully (trigger command retried after reconnect)
Assert.True(await setTask1, "First SET should succeed");
Assert.True(await setTask2, "Second SET should succeed");
Assert.True(await triggerTask, "Trigger SET should succeed after reconnect+retry");

// Verify values were stored
Assert.Equal("value1", (string?)await db.StringGetAsync("normalkey1"));
Assert.Equal("value2", (string?)await db.StringGetAsync("normalkey2"));
Assert.Equal("triggervalue", (string?)await db.StringGetAsync(key));

// Verify MOVED was returned exactly once
Assert.Equal(1, testServer.MovedResponseCount);

// Verify reconnection occurred
Assert.Equal(initialConnectionCount + 1, testServer.TotalClientCount);
}
}
Loading