diff --git a/tests/StackExchange.Redis.Tests/ClusterCommandsRefusedUnitTests.cs b/tests/StackExchange.Redis.Tests/ClusterCommandsRefusedUnitTests.cs index 1932824dc..303714560 100644 --- a/tests/StackExchange.Redis.Tests/ClusterCommandsRefusedUnitTests.cs +++ b/tests/StackExchange.Redis.Tests/ClusterCommandsRefusedUnitTests.cs @@ -65,20 +65,29 @@ public async Task AllClusterCommandsRefusedIsQuiet() config.LoggerFactory = logger; await using var conn = await ConnectionMultiplexer.ConnectAsync(config); - log.WriteLine(logger.All); - Assert.Empty(logger.Matching("Encountered error while updating cluster config")); - Assert.NotEmpty(logger.Matching("CLUSTER NODES was refused")); + try + { + Assert.Empty(logger.Matching("Encountered error while updating cluster config")); + Assert.NotEmpty(logger.Matching("CLUSTER NODES was refused")); - await conn.GetDatabase().PingAsync(); + await conn.GetDatabase().PingAsync(); - // ...and a later explicit reconfigure stays quiet too - Assert.True(await conn.ReconfigureAsync("test")); - Assert.Empty(logger.Matching("Encountered error while updating cluster config")); + // ...and a later explicit reconfigure stays quiet too; the connect's own follow-on reconfigures + // (connection restored, autoconfigure changes) may still be in flight, and ReconfigureAsync declines + // while one is, so insist on a pass that actually ran + await Poll.ReconfigureAsync(conn, "test"); + Assert.Empty(logger.Matching("Encountered error while updating cluster config")); - // with no slot map there is nothing to route by, so we must not *route* as a cluster - in particular, - // cross-slot commands go to the proxy rather than being refused client-side (#3284) - Assert.NotEmpty(logger.Matching("no slot map is available; routing as standalone")); - Assert.Equal(ServerType.Standalone, conn.ServerSelectionStrategy.ServerType); - await conn.GetDatabase().StringGetAsync(["a", "b"]); + // with no slot map there is nothing to route by, so we must not *route* as a cluster - in particular, + // cross-slot commands go to the proxy rather than being refused client-side (#3284) + Assert.NotEmpty(logger.Matching("no slot map is available; routing as standalone")); + Assert.Equal(ServerType.Standalone, conn.ServerSelectionStrategy.ServerType); + await conn.GetDatabase().StringGetAsync(["a", "b"]); + } + finally + { + // after the assertions, so a failure shows what the multiplexer actually logged + log.WriteLine(logger.All); + } } } diff --git a/tests/StackExchange.Redis.Tests/HelloClusterModeUnitTests.cs b/tests/StackExchange.Redis.Tests/HelloClusterModeUnitTests.cs index c5b827969..50336b9ab 100644 --- a/tests/StackExchange.Redis.Tests/HelloClusterModeUnitTests.cs +++ b/tests/StackExchange.Redis.Tests/HelloClusterModeUnitTests.cs @@ -63,12 +63,7 @@ public async Task CrossSlotSurvivesSubscriptionReconnect(RedisProtocol protocol) // the restored connection triggers a fire-and-forget reconfigure (and so can a server-type change); // ReconfigureAsync declines while one is in flight, so insist on one that actually ran - var deadline = DateTime.UtcNow.AddSeconds(10); - while (!await conn.ReconfigureAsync("after subscription reconnect")) - { - if (DateTime.UtcNow > deadline) throw new TimeoutException("Timed out waiting for a reconfigure to run"); - await Task.Delay(50); - } + await Poll.ReconfigureAsync(conn, "after subscription reconnect"); try { diff --git a/tests/StackExchange.Redis.Tests/Helpers/Poll.cs b/tests/StackExchange.Redis.Tests/Helpers/Poll.cs index a5a0dc4ff..dd55d1d08 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Poll.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Poll.cs @@ -37,4 +37,24 @@ public static async Task UntilAsync(Func predicate, int timeoutMilli return false; } + + /// + /// Runs a reconfigure that is guaranteed to have actually executed, rather than been declined. + /// + /// + /// A freshly connected multiplexer fires its own fire-and-forget reconfigures (connection-restored for each + /// physical connection, autoconfigure field changes), and + /// returns false while one is in flight. A test that asserts on that return value races those, and + /// loses on a slow runner; this keeps asking until a pass runs under the caller's cause. + /// + public static async Task ReconfigureAsync(ConnectionMultiplexer conn, string cause, int timeoutMilliseconds = 10000, int pollMilliseconds = 50) + { + var cancellationToken = TestContext.Current.CancellationToken; + for (int i = 0; i <= timeoutMilliseconds / pollMilliseconds; i++) + { + if (await conn.ReconfigureAsync(cause).ConfigureAwait(false)) return; + await Task.Delay(pollMilliseconds, cancellationToken).ConfigureAwait(false); + } + throw new TimeoutException("Timed out waiting for a reconfigure to run: " + cause); + } }