protected async Task WaitForReadyAsync(EndPoint expectedMaster = null, bool waitForReplication = false, TimeSpan?duration = null)
        {
            duration ??= TimeSpan.FromSeconds(30);

            var sw = Stopwatch.StartNew();

            // wait until we have 1 master and 1 replica and have verified their roles
            var master = SentinelServerA.SentinelGetMasterAddressByName(ServiceName);

            if (expectedMaster != null && expectedMaster.ToString() != master.ToString())
            {
                while (sw.Elapsed < duration.Value)
                {
                    await Task.Delay(1000).ForAwait();

                    try
                    {
                        master = SentinelServerA.SentinelGetMasterAddressByName(ServiceName);
                        if (expectedMaster.ToString() == master.ToString())
                        {
                            break;
                        }
                    }
                    catch (Exception)
                    {
                        // ignore
                    }
                }
            }
            if (expectedMaster != null && expectedMaster.ToString() != master.ToString())
            {
                throw new RedisException($"Master was expected to be {expectedMaster}");
            }
            Log($"Master is {master}");

            using var checkConn = Conn.GetSentinelMasterConnection(ServiceOptions);

            await WaitForRoleAsync(checkConn.GetServer(master), "master", duration.Value.Subtract(sw.Elapsed)).ForAwait();

            var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName);

            if (replicas.Length > 0)
            {
                await Task.Delay(1000).ForAwait();

                replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName);
                await WaitForRoleAsync(checkConn.GetServer(replicas[0]), "slave", duration.Value.Subtract(sw.Elapsed)).ForAwait();
            }

            if (waitForReplication)
            {
                await WaitForReplicationAsync(checkConn.GetServer(master), duration.Value.Subtract(sw.Elapsed)).ForAwait();
            }
        }
        private void DoFailover()
        {
            WaitForReady();

            // capture current replica
            var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName);

            Log("Starting failover...");
            var sw = Stopwatch.StartNew();

            SentinelServerA.SentinelFailover(ServiceName);

            // wait until the replica becomes the master
            WaitForReady(expectedMaster: replicas[0]);
            Log($"Time to failover: {sw.Elapsed}");
        }
        public async Task ReadOnlyConnectionReplicasTest()
        {
            var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName);
            var config   = new ConfigurationOptions();

            foreach (var replica in replicas)
            {
                config.EndPoints.Add(replica);
            }

            var readonlyConn = await ConnectionMultiplexer.ConnectAsync(config);

            await UntilCondition(TimeSpan.FromSeconds(2), () => readonlyConn.IsConnected);

            Assert.True(readonlyConn.IsConnected);
            var db = readonlyConn.GetDatabase();
            var s  = db.StringGet("test");

            Assert.True(s.IsNullOrEmpty);
            //var ex = Assert.Throws<RedisConnectionException>(() => db.StringSet("test", "try write to read only instance"));
            //Assert.StartsWith("No connection is available to service this operation", ex.Message);
        }
        public async Task ManagedPrimaryConnectionEndToEndWithFailoverTest()
        {
            var connectionString = $"{TestConfig.Current.SentinelServer}:{TestConfig.Current.SentinelPortA},serviceName={ServiceOptions.ServiceName},allowAdmin=true";
            var conn             = await ConnectionMultiplexer.ConnectAsync(connectionString);

            conn.ConfigurationChanged += (s, e) => Log($"Configuration changed: {e.EndPoint}");
            var sub = conn.GetSubscriber();

            sub.Subscribe("*", (channel, message) => Log($"Sub: {channel}, message:{message}"));

            var db = conn.GetDatabase();
            await db.PingAsync();

            var endpoints = conn.GetEndPoints();

            Assert.Equal(2, endpoints.Length);

            var servers = endpoints.Select(e => conn.GetServer(e)).ToArray();

            Assert.Equal(2, servers.Length);

            var primary = servers.FirstOrDefault(s => !s.IsReplica);

            Assert.NotNull(primary);
            var replica = servers.FirstOrDefault(s => s.IsReplica);

            Assert.NotNull(replica);
            Assert.NotEqual(primary.EndPoint.ToString(), replica.EndPoint.ToString());

            // Set string value on current primary
            var expected = DateTime.Now.Ticks.ToString();

            Log("Tick Key: " + expected);
            var key = Me();
            await db.KeyDeleteAsync(key, CommandFlags.FireAndForget);

            await db.StringSetAsync(key, expected);

            var value = await db.StringGetAsync(key);

            Assert.Equal(expected, value);

            Log("Waiting for first replication check...");
            // force read from replica, replication has some lag
            await WaitForReplicationAsync(servers[0]).ForAwait();

            value = await db.StringGetAsync(key, CommandFlags.DemandReplica);

            Assert.Equal(expected, value);

            Log("Waiting for ready pre-failover...");
            await WaitForReadyAsync();

            // capture current replica
            var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName);

            Log("Starting failover...");
            var sw = Stopwatch.StartNew();

            SentinelServerA.SentinelFailover(ServiceName);

            // There's no point in doing much for 10 seconds - this is a built-in delay of how Sentinel works.
            // The actual completion invoking the replication of the former primary is handled via
            // https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L4799-L4808
            // ...which is invoked by INFO polls every 10 seconds (https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L81)
            // ...which is calling https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L2666
            // However, the quicker iteration on INFO during an o_down does not apply here: https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L3089-L3104
            // So...we're waiting 10 seconds, no matter what. Might as well just idle to be more stable.
            await Task.Delay(TimeSpan.FromSeconds(10));

            // wait until the replica becomes the primary
            Log("Waiting for ready post-failover...");
            await WaitForReadyAsync(expectedPrimary : replicas[0]);

            Log($"Time to failover: {sw.Elapsed}");

            endpoints = conn.GetEndPoints();
            Assert.Equal(2, endpoints.Length);

            servers = endpoints.Select(e => conn.GetServer(e)).ToArray();
            Assert.Equal(2, servers.Length);

            var newPrimary = servers.FirstOrDefault(s => !s.IsReplica);

            Assert.NotNull(newPrimary);
            Assert.Equal(replica.EndPoint.ToString(), newPrimary.EndPoint.ToString());
            var newReplica = servers.FirstOrDefault(s => s.IsReplica);

            Assert.NotNull(newReplica);
            Assert.Equal(primary.EndPoint.ToString(), newReplica.EndPoint.ToString());
            Assert.NotEqual(primary.EndPoint.ToString(), replica.EndPoint.ToString());

            value = await db.StringGetAsync(key);

            Assert.Equal(expected, value);

            Log("Waiting for second replication check...");
            // force read from replica, replication has some lag
            await WaitForReplicationAsync(newPrimary).ForAwait();

            value = await db.StringGetAsync(key, CommandFlags.DemandReplica);

            Assert.Equal(expected, value);
        }