Skip to content

Commit

Permalink
Sentinel improvements (#1431)
Browse files Browse the repository at this point in the history
Add SentinelConnect and SentinelMasterConnect to ConnectionMultiplexer for working with sentinel setups (#1427)
Fix issue with duplicate endpoints being added in the UpdateSentinelAddressList method (#1430).

This change makes it a lot easier and more discoverable how to connect to a sentinel server while also allowing connecting with just a connection string change which allows existing libs that are using SE.Redis to be used in sentinel mode.

Adding a `serviceName` parameter to the connection string triggers sentinel mode. It will connect to the sentinel and discover the current master and return a managed connection that follows the master. 

```csharp
var conn = ConnectionMultiplexer.Connect("localhost,serviceName=mymaster");
var db = conn.GetDatabase();
db.StringSet("key", "value");
```
  • Loading branch information
ejsmith committed Jun 8, 2020
1 parent 8dddbde commit 6fa8d44
Show file tree
Hide file tree
Showing 10 changed files with 654 additions and 291 deletions.
9 changes: 9 additions & 0 deletions StackExchange.Redis.sln
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RedisConfigs", "RedisConfig
tests\RedisConfigs\cli-master.cmd = tests\RedisConfigs\cli-master.cmd
tests\RedisConfigs\cli-secure.cmd = tests\RedisConfigs\cli-secure.cmd
tests\RedisConfigs\cli-slave.cmd = tests\RedisConfigs\cli-slave.cmd
tests\RedisConfigs\docker-compose.yml = tests\RedisConfigs\docker-compose.yml
tests\RedisConfigs\Dockerfile = tests\RedisConfigs\Dockerfile
tests\RedisConfigs\start-all.cmd = tests\RedisConfigs\start-all.cmd
tests\RedisConfigs\start-all.sh = tests\RedisConfigs\start-all.sh
tests\RedisConfigs\start-basic.cmd = tests\RedisConfigs\start-basic.cmd
Expand Down Expand Up @@ -126,6 +128,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestConsoleBaseline", "toys
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = ".github", ".github\.github.csproj", "{8FB98E7D-DAE2-4465-BD9A-104000E0A2D4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docker", "Docker", "{A9F81DA3-DA82-423E-A5DD-B11C37548E06}"
ProjectSection(SolutionItems) = preProject
tests\RedisConfigs\Docker\docker-entrypoint.sh = tests\RedisConfigs\Docker\docker-entrypoint.sh
tests\RedisConfigs\Docker\supervisord.conf = tests\RedisConfigs\Docker\supervisord.conf
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -197,6 +205,7 @@ Global
{3DA1EEED-E9FE-43D9-B293-E000CFCCD91A} = {E25031D3-5C64-430D-B86F-697B66816FD8}
{153A10E4-E668-41AD-9E0F-6785CE7EED66} = {3AD17044-6BFF-4750-9AC2-2CA466375F2A}
{D58114AE-4998-4647-AFCA-9353D20495AE} = {E25031D3-5C64-430D-B86F-697B66816FD8}
{A9F81DA3-DA82-423E-A5DD-B11C37548E06} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {193AA352-6748-47C1-A5FC-C9AA6B5F000B}
Expand Down
10 changes: 9 additions & 1 deletion docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ This will connect to a single server on the local machine using the default redi
var conn = ConnectionMultiplexer.Connect("redis0:6380,redis1:6380,allowAdmin=true");
```

If you specify a serviceName in the connection string, it will trigger sentinel mode. This example will connect to a sentinel server on the local machine
using the default sentinel port (26379), discover the current master server for the `mymaster` service and return a managed connection
pointing to that master server that will automatically be updated if the master changes:

```csharp
var conn = ConnectionMultiplexer.Connect("localhost,serviceName=mymaster");
```

An overview of mapping between the `string` and `ConfigurationOptions` representation is shown below, but you can switch between them trivially:

```csharp
Expand Down Expand Up @@ -79,7 +87,7 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a
| proxy={proxy type} | `Proxy` | `Proxy.None` | Type of proxy in use (if any); for example "twemproxy" |
| resolveDns={bool} | `ResolveDns` | `false` | Specifies that DNS resolution should be explicit and eager, rather than implicit |
| responseTimeout={int} | `ResponseTimeout` | `SyncTimeout` | Time (ms) to decide whether the socket is unhealthy |
| serviceName={string} | `ServiceName` | `null` | Not currently implemented (intended for use with sentinel) |
| serviceName={string} | `ServiceName` | `null` | Used for connecting to a sentinel master service |
| ssl={bool} | `Ssl` | `false` | Specifies that SSL encryption should be used |
| sslHost={string} | `SslHost` | `null` | Enforces a particular SSL host identity on the server's certificate |
| sslProtocols={enum} | `SslProtocols` | `null` | Ssl/Tls versions supported when using an encrypted connection. Use '\|' to provide multiple values. |
Expand Down
273 changes: 229 additions & 44 deletions src/StackExchange.Redis/ConnectionMultiplexer.cs

Large diffs are not rendered by default.

22 changes: 21 additions & 1 deletion src/StackExchange.Redis/EndPointCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace StackExchange.Redis
/// <summary>
/// A list of endpoints
/// </summary>
public sealed class EndPointCollection : Collection<EndPoint>, IEnumerable, IEnumerable<EndPoint>
public sealed class EndPointCollection : Collection<EndPoint>, IEnumerable<EndPoint>
{
/// <summary>
/// Create a new EndPointCollection
Expand Down Expand Up @@ -59,6 +59,26 @@ public void Add(string hostAndPort)
/// <param name="port">The port for <paramref name="host"/> to add.</param>
public void Add(IPAddress host, int port) => Add(new IPEndPoint(host, port));

/// <summary>
/// Try adding a new endpoint to the list.
/// </summary>
/// <param name="endpoint">The endpoint to add.</param>
/// <returns>True if the endpoint was added or false if not.</returns>
public bool TryAdd(EndPoint endpoint)
{
if (endpoint == null) throw new ArgumentNullException(nameof(endpoint));

if (!Contains(endpoint))
{
base.InsertItem(Count, endpoint);
return true;
}
else
{
return false;
}
}

/// <summary>
/// See Collection&lt;T&gt;.InsertItem()
/// </summary>
Expand Down
31 changes: 29 additions & 2 deletions src/StackExchange.Redis/Interfaces/IServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,7 @@ public partial interface IServer : IRedis
/// for the given service name.
/// </summary>
/// <param name="serviceName">the sentinel service name</param>
/// <param name="flags"></param>
/// <param name="flags">The command flags to use.</param>
/// <returns>a list of the sentinel ips and ports</returns>
EndPoint[] SentinelGetSentinelAddresses(string serviceName, CommandFlags flags = CommandFlags.None);

Expand All @@ -801,10 +801,28 @@ public partial interface IServer : IRedis
/// for the given service name.
/// </summary>
/// <param name="serviceName">the sentinel service name</param>
/// <param name="flags"></param>
/// <param name="flags">The command flags to use.</param>
/// <returns>a list of the sentinel ips and ports</returns>
Task<EndPoint[]> SentinelGetSentinelAddressesAsync(string serviceName, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Returns the ip and port numbers of all known Sentinel replicas
/// for the given service name.
/// </summary>
/// <param name="serviceName">the sentinel service name</param>
/// <param name="flags">The command flags to use.</param>
/// <returns>a list of the replica ips and ports</returns>
EndPoint[] SentinelGetReplicaAddresses(string serviceName, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Returns the ip and port numbers of all known Sentinel replicas
/// for the given service name.
/// </summary>
/// <param name="serviceName">the sentinel service name</param>
/// <param name="flags">The command flags to use.</param>
/// <returns>a list of the replica ips and ports</returns>
Task<EndPoint[]> SentinelGetReplicaAddressesAsync(string serviceName, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Show the state and info of the specified master.
/// </summary>
Expand Down Expand Up @@ -1024,5 +1042,14 @@ internal static class IServerExtensions
/// </summary>
/// <param name="server">The server to simulate failure on.</param>
public static void SimulateConnectionFailure(this IServer server) => (server as RedisServer)?.SimulateConnectionFailure();

public static string Role(this IServer server)
{
var result = (RedisResult[])server.Execute("ROLE");
if (result != null && result.Length > 0)
return result[0].ToString();

return null;
}
}
}
2 changes: 2 additions & 0 deletions src/StackExchange.Redis/RedisLiterals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,14 @@ public static readonly RedisValue

// misc (config, etc)
databases = "databases",
master = "master",
no = "no",
normal = "normal",
pubsub = "pubsub",
replica = "replica",
replica_read_only = "replica-read-only",
replication = "replication",
sentinel = "sentinel",
server = "server",
slave = "slave",
slave_read_only = "slave-read-only",
Expand Down
14 changes: 14 additions & 0 deletions src/StackExchange.Redis/RedisServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,18 @@ public Task<EndPoint[]> SentinelGetSentinelAddressesAsync(string serviceName, Co
return ExecuteAsync(msg, ResultProcessor.SentinelAddressesEndPoints);
}

public EndPoint[] SentinelGetReplicaAddresses(string serviceName, CommandFlags flags = CommandFlags.None)
{
var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SLAVES, (RedisValue)serviceName);
return ExecuteSync(msg, ResultProcessor.SentinelAddressesEndPoints);
}

public Task<EndPoint[]> SentinelGetReplicaAddressesAsync(string serviceName, CommandFlags flags = CommandFlags.None)
{
var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SLAVES, (RedisValue)serviceName);
return ExecuteAsync(msg, ResultProcessor.SentinelAddressesEndPoints);
}

public KeyValuePair<string, string>[] SentinelMaster(string serviceName, CommandFlags flags = CommandFlags.None)
{
var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.MASTER, (RedisValue)serviceName);
Expand Down Expand Up @@ -866,6 +878,7 @@ public Task<KeyValuePair<string, string>[][]> SentinelMastersAsync(CommandFlags
return ExecuteAsync(msg, ResultProcessor.SentinelArrayOfArrays);
}

// For previous compat only
KeyValuePair<string, string>[][] IServer.SentinelSlaves(string serviceName, CommandFlags flags)
=> SentinelReplicas(serviceName, flags);

Expand All @@ -876,6 +889,7 @@ public KeyValuePair<string, string>[][] SentinelReplicas(string serviceName, Com
return ExecuteSync(msg, ResultProcessor.SentinelArrayOfArrays);
}

// For previous compat only
Task<KeyValuePair<string, string>[][]> IServer.SentinelSlavesAsync(string serviceName, CommandFlags flags)
=> SentinelReplicasAsync(serviceName, flags);

Expand Down
62 changes: 60 additions & 2 deletions src/StackExchange.Redis/ResultProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,10 @@ public static readonly ResultProcessor<EndPoint>
SentinelMasterEndpoint = new SentinelGetMasterAddressByNameProcessor();

public static readonly ResultProcessor<EndPoint[]>
SentinelAddressesEndPoints = new SentinelGetSentinelAddresses();
SentinelAddressesEndPoints = new SentinelGetSentinelAddressesProcessor();

public static readonly ResultProcessor<EndPoint[]>
SentinelReplicaEndPoints = new SentinelGetReplicaAddressesProcessor();

public static readonly ResultProcessor<KeyValuePair<string, string>[][]>
SentinelArrayOfArrays = new SentinelArrayOfArraysProcessor();
Expand Down Expand Up @@ -2036,7 +2039,62 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes
}
}

private sealed class SentinelGetSentinelAddresses : ResultProcessor<EndPoint[]>
private sealed class SentinelGetSentinelAddressesProcessor : ResultProcessor<EndPoint[]>
{
protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
{
List<EndPoint> endPoints = new List<EndPoint>();

switch (result.Type)
{
case ResultType.MultiBulk:
foreach (RawResult item in result.GetItems())
{
var arr = item.GetItemsAsValues();
string ip = null;
string portStr = null;

for (int i = 0; i < arr.Length && (ip == null || portStr == null); i += 2)
{
string name = arr[i];
string value = arr[i + 1];

switch (name)
{
case "ip":
ip = value;
break;
case "port":
portStr = value;
break;
}
}

if (ip != null && portStr != null && int.TryParse(portStr, out int port))
{
endPoints.Add(Format.ParseEndPoint(ip, port));
}
}
break;

case ResultType.SimpleString:
//We don't want to blow up if the master is not found
if (result.IsNull)
return true;
break;
}

if (endPoints.Count > 0)
{
SetResult(message, endPoints.ToArray());
return true;
}

return false;
}
}

private sealed class SentinelGetReplicaAddressesProcessor : ResultProcessor<EndPoint[]>
{
protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
{
Expand Down
2 changes: 1 addition & 1 deletion tests/RedisConfigs/Docker/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ if [ "$#" -ne 0 ]; then
exec "$@"
else
mkdir -p /var/log/supervisor
mkdir Temp/
mkdir -p Temp/

supervisord -c /etc/supervisord.conf
sleep 3
Expand Down
Loading

0 comments on commit 6fa8d44

Please sign in to comment.