Skip to content

Commit 181cee7

Browse files
committed
Add SentinelConnect and SentinelMasterConnect to ConnectionMultiplexer for working with sentinel setups (#1427) Fix issue with duplicate endpoints being added in the UpdateSentinelAddressList method (#1430).
Add string configuration overloads for sentinel connect methods. Remove password from sentinel servers as it seems the windows port does not support it. Add some new tests.
1 parent 69cbf69 commit 181cee7

File tree

10 files changed

+224
-23
lines changed

10 files changed

+224
-23
lines changed

StackExchange.Redis.sln

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RedisConfigs", "RedisConfig
2323
tests\RedisConfigs\cli-master.cmd = tests\RedisConfigs\cli-master.cmd
2424
tests\RedisConfigs\cli-secure.cmd = tests\RedisConfigs\cli-secure.cmd
2525
tests\RedisConfigs\cli-slave.cmd = tests\RedisConfigs\cli-slave.cmd
26+
tests\RedisConfigs\docker-compose.yml = tests\RedisConfigs\docker-compose.yml
27+
tests\RedisConfigs\Dockerfile = tests\RedisConfigs\Dockerfile
2628
tests\RedisConfigs\start-all.cmd = tests\RedisConfigs\start-all.cmd
2729
tests\RedisConfigs\start-all.sh = tests\RedisConfigs\start-all.sh
2830
tests\RedisConfigs\start-basic.cmd = tests\RedisConfigs\start-basic.cmd
@@ -126,6 +128,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestConsoleBaseline", "toys
126128
EndProject
127129
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = ".github", ".github\.github.csproj", "{8FB98E7D-DAE2-4465-BD9A-104000E0A2D4}"
128130
EndProject
131+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docker", "Docker", "{A9F81DA3-DA82-423E-A5DD-B11C37548E06}"
132+
ProjectSection(SolutionItems) = preProject
133+
tests\RedisConfigs\Docker\docker-entrypoint.sh = tests\RedisConfigs\Docker\docker-entrypoint.sh
134+
tests\RedisConfigs\Docker\supervisord.conf = tests\RedisConfigs\Docker\supervisord.conf
135+
EndProjectSection
136+
EndProject
129137
Global
130138
GlobalSection(SolutionConfigurationPlatforms) = preSolution
131139
Debug|Any CPU = Debug|Any CPU
@@ -197,6 +205,7 @@ Global
197205
{3DA1EEED-E9FE-43D9-B293-E000CFCCD91A} = {E25031D3-5C64-430D-B86F-697B66816FD8}
198206
{153A10E4-E668-41AD-9E0F-6785CE7EED66} = {3AD17044-6BFF-4750-9AC2-2CA466375F2A}
199207
{D58114AE-4998-4647-AFCA-9353D20495AE} = {E25031D3-5C64-430D-B86F-697B66816FD8}
208+
{A9F81DA3-DA82-423E-A5DD-B11C37548E06} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05}
200209
EndGlobalSection
201210
GlobalSection(ExtensibilityGlobals) = postSolution
202211
SolutionGuid = {193AA352-6748-47C1-A5FC-C9AA6B5F000B}

src/StackExchange.Redis/ConnectionMultiplexer.cs

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -994,14 +994,74 @@ public static ConnectionMultiplexer Connect(string configuration, TextWriter log
994994
/// <summary>
995995
/// Create a new ConnectionMultiplexer instance
996996
/// </summary>
997-
/// <param name="configuration">The configurtion options to use for this multiplexer.</param>
997+
/// <param name="configuration">The configuration options to use for this multiplexer.</param>
998998
/// <param name="log">The <see cref="TextWriter"/> to log to.</param>
999999
public static ConnectionMultiplexer Connect(ConfigurationOptions configuration, TextWriter log = null)
10001000
{
10011001
SocketConnection.AssertDependencies();
10021002
return ConnectImpl(configuration, log);
10031003
}
10041004

1005+
/// <summary>
1006+
/// Create a new ConnectionMultiplexer instance that connects to a sentinel server
1007+
/// </summary>
1008+
/// <param name="configuration">The string configuration to use for this multiplexer.</param>
1009+
/// <param name="log">The <see cref="TextWriter"/> to log to.</param>
1010+
public static ConnectionMultiplexer SentinelConnect(string configuration, TextWriter log = null)
1011+
{
1012+
var options = ConfigurationOptions.Parse(configuration);
1013+
return SentinelConnect(options);
1014+
}
1015+
1016+
/// <summary>
1017+
/// Create a new ConnectionMultiplexer instance that connects to a sentinel server
1018+
/// </summary>
1019+
/// <param name="configuration">The configuration options to use for this multiplexer.</param>
1020+
/// <param name="log">The <see cref="TextWriter"/> to log to.</param>
1021+
public static ConnectionMultiplexer SentinelConnect(ConfigurationOptions configuration, TextWriter log = null)
1022+
{
1023+
if (string.IsNullOrEmpty(configuration.ServiceName))
1024+
throw new ArgumentException("A ServiceName must be specified.");
1025+
1026+
var sentinelConfigurationOptions = configuration.Clone();
1027+
1028+
// this is required when connecting to sentinel servers
1029+
sentinelConfigurationOptions.TieBreaker = "";
1030+
sentinelConfigurationOptions.CommandMap = CommandMap.Sentinel;
1031+
1032+
// use default sentinel port
1033+
sentinelConfigurationOptions.EndPoints.SetDefaultPorts(26379);
1034+
1035+
return Connect(sentinelConfigurationOptions, log);
1036+
}
1037+
1038+
/// <summary>
1039+
/// Create a new ConnectionMultiplexer instance that connects to a sentinel server, discovers the current master server
1040+
/// for the specified ServiceName in the config and returns a managed connection to the current master server
1041+
/// </summary>
1042+
/// <param name="configuration">The string configuration to use for this multiplexer.</param>
1043+
/// <param name="log">The <see cref="TextWriter"/> to log to.</param>
1044+
public static ConnectionMultiplexer SentinelMasterConnect(string configuration, TextWriter log = null)
1045+
{
1046+
var options = ConfigurationOptions.Parse(configuration);
1047+
var sentinelConnection = SentinelConnect(options, log);
1048+
1049+
return sentinelConnection.GetSentinelMasterConnection(options, log);
1050+
}
1051+
1052+
/// <summary>
1053+
/// Create a new ConnectionMultiplexer instance that connects to a sentinel server, discovers the current master server
1054+
/// for the specified ServiceName in the config and returns a managed connection to the current master server
1055+
/// </summary>
1056+
/// <param name="configuration">The configuration options to use for this multiplexer.</param>
1057+
/// <param name="log">The <see cref="TextWriter"/> to log to.</param>
1058+
public static ConnectionMultiplexer SentinelMasterConnect(ConfigurationOptions configuration, TextWriter log = null)
1059+
{
1060+
var sentinelConnection = SentinelConnect(configuration, log);
1061+
1062+
return sentinelConnection.GetSentinelMasterConnection(configuration, log);
1063+
}
1064+
10051065
private static ConnectionMultiplexer ConnectImpl(object configuration, TextWriter log)
10061066
{
10071067
IDisposable killMe = null;
@@ -2400,7 +2460,7 @@ internal void UpdateSentinelAddressList(string serviceName)
24002460
foreach (EndPoint newSentinel in firstCompleteRequest.Where(x => !RawConfig.EndPoints.Contains(x)))
24012461
{
24022462
hasNew = true;
2403-
RawConfig.EndPoints.Add(newSentinel);
2463+
RawConfig.EndPoints.TryAdd(newSentinel);
24042464
}
24052465

24062466
if (hasNew)

src/StackExchange.Redis/EndPointCollection.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,26 @@ public void Add(string hostAndPort)
5959
/// <param name="port">The port for <paramref name="host"/> to add.</param>
6060
public void Add(IPAddress host, int port) => Add(new IPEndPoint(host, port));
6161

62+
/// <summary>
63+
/// Try adding a new endpoint to the list.
64+
/// </summary>
65+
/// <param name="endpoint">The endpoint to add.</param>
66+
/// <returns>True if the endpoint was added or false if not.</returns>
67+
public bool TryAdd(EndPoint endpoint)
68+
{
69+
if (endpoint == null) throw new ArgumentNullException(nameof(endpoint));
70+
71+
if (!Contains(endpoint))
72+
{
73+
base.InsertItem(Count, endpoint);
74+
return true;
75+
}
76+
else
77+
{
78+
return false;
79+
}
80+
}
81+
6282
/// <summary>
6383
/// See Collection&lt;T&gt;.InsertItem()
6484
/// </summary>

tests/RedisConfigs/Docker/docker-entrypoint.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ if [ "$#" -ne 0 ]; then
44
exec "$@"
55
else
66
mkdir -p /var/log/supervisor
7-
mkdir Temp/
7+
mkdir -p Temp/
88

99
supervisord -c /etc/supervisord.conf
1010
sleep 3

tests/RedisConfigs/Sentinel/redis-7010.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
port 7010
2+
#requirepass changeme
3+
#masterauth changeme
24
repl-diskless-sync yes
35
repl-diskless-sync-delay 0
46
maxmemory 100mb
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
port 7011
2+
#requirepass changeme
23
slaveof 127.0.0.1 7010
4+
#masterauth changeme
35
repl-diskless-sync yes
46
repl-diskless-sync-delay 0
57
maxmemory 100mb
68
appendonly no
79
dir "../Temp"
810
dbfilename "sentinel-target-7011.rdb"
9-
save ""
11+
save ""

tests/RedisConfigs/Sentinel/sentinel-26379.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
port 26379
2+
#requirepass "changeme"
23
sentinel monitor mymaster 127.0.0.1 7010 1
4+
#sentinel auth-pass mymaster changeme
35
sentinel down-after-milliseconds mymaster 1000
46
sentinel failover-timeout mymaster 1000
57
sentinel config-epoch mymaster 0

tests/RedisConfigs/Sentinel/sentinel-26380.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
port 26380
2+
#requirepass "changeme"
23
sentinel monitor mymaster 127.0.0.1 7010 1
4+
#sentinel auth-pass mymaster changeme
35
sentinel down-after-milliseconds mymaster 1000
46
sentinel failover-timeout mymaster 1000
57
sentinel config-epoch mymaster 0
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
port 26381
2+
#requirepass "changeme"
23
sentinel monitor mymaster 127.0.0.1 7010 1
4+
#sentinel auth-pass mymaster changeme
35
sentinel down-after-milliseconds mymaster 1000
46
sentinel failover-timeout mymaster 1000
57
sentinel config-epoch mymaster 0
6-
dir "../Temp"
8+
dir "../Temp"

tests/StackExchange.Redis.Tests/Sentinel.cs

Lines changed: 120 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Net;
@@ -12,7 +12,7 @@ namespace StackExchange.Redis.Tests
1212
public class Sentinel : TestBase
1313
{
1414
private string ServiceName => TestConfig.Current.SentinelSeviceName;
15-
private ConfigurationOptions ServiceOptions => new ConfigurationOptions { ServiceName = ServiceName, AllowAdmin = true };
15+
private ConfigurationOptions ServiceOptions => new ConfigurationOptions { ServiceName = ServiceName, AllowAdmin = true, Password = "changeme" };
1616

1717
private ConnectionMultiplexer Conn { get; }
1818
private IServer SentinelServerA { get; }
@@ -28,24 +28,16 @@ public Sentinel(ITestOutputHelper output) : base(output)
2828
Skip.IfNoConfig(nameof(TestConfig.Config.SentinelServer), TestConfig.Current.SentinelServer);
2929
Skip.IfNoConfig(nameof(TestConfig.Config.SentinelSeviceName), TestConfig.Current.SentinelSeviceName);
3030

31-
var options = new ConfigurationOptions()
32-
{
33-
CommandMap = CommandMap.Sentinel,
34-
EndPoints = {
35-
{ TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA },
36-
{ TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortB },
37-
{ TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortC }
38-
},
39-
AllowAdmin = true,
40-
TieBreaker = "",
41-
ServiceName = TestConfig.Current.SentinelSeviceName,
42-
SyncTimeout = 5000
43-
};
44-
Conn = ConnectionMultiplexer.Connect(options, ConnectionLog);
31+
var options = ServiceOptions.Clone();
32+
options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA);
33+
options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortB);
34+
options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortC);
35+
36+
Conn = ConnectionMultiplexer.SentinelConnect(options, ConnectionLog);
4537
for (var i = 0; i < 150; i++)
4638
{
4739
Thread.Sleep(20);
48-
if (Conn.IsConnected && Conn.GetSentinelMasterConnection(ServiceOptions).IsConnected)
40+
if (Conn.IsConnected && Conn.GetSentinelMasterConnection(options).IsConnected)
4941
{
5042
break;
5143
}
@@ -57,6 +49,84 @@ public Sentinel(ITestOutputHelper output) : base(output)
5749
SentinelsServers = new IServer[] { SentinelServerA, SentinelServerB, SentinelServerC };
5850
}
5951

52+
[Fact]
53+
public void MasterConnectTest()
54+
{
55+
var options = ServiceOptions.Clone();
56+
options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA);
57+
58+
var conn = ConnectionMultiplexer.SentinelMasterConnect(options);
59+
var db = conn.GetDatabase();
60+
61+
var test = db.Ping();
62+
Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer,
63+
TestConfig.Current.SentinelPortA, test.TotalMilliseconds);
64+
}
65+
66+
[Fact]
67+
public void MasterConnectWithDefaultPortTest()
68+
{
69+
var options = ServiceOptions.Clone();
70+
options.EndPoints.Add(TestConfig.Current.SentinelServer);
71+
72+
var conn = ConnectionMultiplexer.SentinelMasterConnect(options);
73+
var db = conn.GetDatabase();
74+
75+
var test = db.Ping();
76+
Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer,
77+
TestConfig.Current.SentinelPortA, test.TotalMilliseconds);
78+
}
79+
80+
[Fact]
81+
public void MasterConnectWithStringConfigurationTest()
82+
{
83+
var connectionString = $"{TestConfig.Current.SentinelServer}:{TestConfig.Current.SentinelPortA},password={ServiceOptions.Password},serviceName={ServiceOptions.ServiceName}";
84+
var conn = ConnectionMultiplexer.SentinelMasterConnect(connectionString);
85+
var db = conn.GetDatabase();
86+
87+
var test = db.Ping();
88+
Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer,
89+
TestConfig.Current.SentinelPortA, test.TotalMilliseconds);
90+
}
91+
92+
[Fact]
93+
public async Task MasterConnectFailoverTest()
94+
{
95+
var options = ServiceOptions.Clone();
96+
options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA);
97+
98+
// connection is managed and should switch to current master when failover happens
99+
var conn = ConnectionMultiplexer.SentinelMasterConnect(options);
100+
conn.ConfigurationChanged += (s, e) => {
101+
Log($"Configuration changed: {e.EndPoint}");
102+
};
103+
var db = conn.GetDatabase();
104+
105+
var test = await db.PingAsync();
106+
Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer,
107+
TestConfig.Current.SentinelPortA, test.TotalMilliseconds);
108+
109+
// set string value on current master
110+
var expected = DateTime.Now.Ticks.ToString();
111+
Log("Tick Key: " + expected);
112+
var key = Me();
113+
await db.KeyDeleteAsync(key, CommandFlags.FireAndForget);
114+
await db.StringSetAsync(key, expected);
115+
116+
// forces and verifies failover
117+
await DoFailoverAsync();
118+
119+
var value = await db.StringGetAsync(key);
120+
Assert.Equal(expected, value);
121+
122+
await db.StringSetAsync(key, expected);
123+
}
124+
125+
private void Conn_ConfigurationChanged(object sender, EndPointEventArgs e)
126+
{
127+
throw new NotImplementedException();
128+
}
129+
60130
[Fact]
61131
public void PingTest()
62132
{
@@ -584,7 +654,8 @@ public async Task ReadOnlyConnectionSlavesTest()
584654
var config = new ConfigurationOptions
585655
{
586656
TieBreaker = "",
587-
ServiceName = TestConfig.Current.SentinelSeviceName,
657+
ServiceName = ServiceOptions.ServiceName,
658+
Password = ServiceOptions.Password
588659
};
589660

590661
foreach (var kv in slaves)
@@ -604,5 +675,36 @@ public async Task ReadOnlyConnectionSlavesTest()
604675
//Assert.StartsWith("No connection is available to service this operation", ex.Message);
605676

606677
}
678+
679+
private async Task DoFailoverAsync()
680+
{
681+
// capture current master and slave
682+
var master = SentinelServerA.SentinelGetMasterAddressByName(ServiceName);
683+
var slaves = SentinelServerA.SentinelSlaves(ServiceName);
684+
685+
await Task.Delay(1000).ForAwait();
686+
try
687+
{
688+
Log("Failover attempted initiated");
689+
SentinelServerA.SentinelFailover(ServiceName);
690+
Log(" Success!");
691+
}
692+
catch (RedisServerException ex) when (ex.Message.Contains("NOGOODSLAVE"))
693+
{
694+
// Retry once
695+
Log(" Retry initiated");
696+
await Task.Delay(1000).ForAwait();
697+
SentinelServerA.SentinelFailover(ServiceName);
698+
Log(" Retry complete");
699+
}
700+
await Task.Delay(2000).ForAwait();
701+
702+
var newMaster = SentinelServerA.SentinelGetMasterAddressByName(ServiceName);
703+
var newSlave = SentinelServerA.SentinelSlaves(ServiceName);
704+
705+
// make sure master changed
706+
Assert.Equal(slaves[0].ToDictionary()["name"], newMaster.ToString());
707+
Assert.Equal(master.ToString(), newSlave[0].ToDictionary()["name"]);
708+
}
607709
}
608710
}

0 commit comments

Comments
 (0)