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
27 changes: 23 additions & 4 deletions src/StackExchange.Redis/ServerEndPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ public void SetClusterConfiguration(ClusterConfiguration configuration)

public void UpdateNodeRelations(ClusterConfiguration configuration)
{
var thisNode = configuration.Nodes.FirstOrDefault(x => x.EndPoint?.Equals(EndPoint) == true);
var thisNode = GetClusterNode(configuration);
if (thisNode != null)
{
Multiplexer.Trace($"Updating node relations for {Format.ToString(thisNode.EndPoint)}...");
Expand All @@ -379,6 +379,9 @@ public void UpdateNodeRelations(ClusterConfiguration configuration)
}
}

private ClusterNode? GetClusterNode(ClusterConfiguration? configuration) =>
configuration?.Nodes.FirstOrDefault(x => x.EndPoint?.Equals(EndPoint) == true);

public void SetUnselectable(UnselectableFlags flags)
{
if (flags != 0)
Expand Down Expand Up @@ -502,7 +505,9 @@ internal async Task AutoConfigureAsync(PhysicalConnection? connection, ILogger?
await WriteDirectOrQueueFireAndForgetAsync(connection, msg, autoConfigProcessor).ForAwait();
}
}
else if (commandMap.IsAvailable(RedisCommand.SET) && !(helloPending || RoleKnownFromHello))
else if (commandMap.IsAvailable(RedisCommand.SET)
&& !(helloPending || RoleKnownFromHello)
&& !(ServerType == ServerType.Cluster && GetClusterNode(ClusterConfiguration) is not null))
{
// This is a nasty way to find if we are a replica, and it will only work on up-level servers, but...
// (note we only get here when HELLO isn't going to tell us: the HELLO reply carries "role", and
Expand Down Expand Up @@ -535,7 +540,9 @@ internal async Task AutoConfigureAsync(PhysicalConnection? connection, ILogger?
}
// If we are going to fetch a tie breaker, do so last and we'll get it in before the tracer fires completing the connection
// But if GETs are disabled on this, do not fail the connection - we just don't get tiebreaker benefits
if (Multiplexer.RawConfig.TryGetTieBreaker(out var tieBreakerKey) && Multiplexer.CommandMap.IsAvailable(RedisCommand.GET))
if (ServerType != ServerType.Cluster
&& Multiplexer.RawConfig.TryGetTieBreaker(out var tieBreakerKey)
&& Multiplexer.CommandMap.IsAvailable(RedisCommand.GET))
{
log?.LogInformationRequestingTieBreak(new(EndPoint), tieBreakerKey);
msg = Message.Create(0, flags, RedisCommand.GET, tieBreakerKey);
Expand Down Expand Up @@ -680,12 +687,24 @@ internal Message GetTracerMessage(bool checkResponse)
else
{
map.AssertAvailable(RedisCommand.EXISTS);
msg = Message.Create(0, flags, RedisCommand.EXISTS, (RedisValue)Multiplexer.UniqueId);
msg = Message.Create(0, flags, RedisCommand.EXISTS, GetTracerKey());
}
msg.SetInternalCall();
return msg;
}

internal RedisKey GetTracerKey()
{
RedisKey key = Multiplexer.UniqueId;
if (ServerType == ServerType.Cluster
&& GetClusterNode(ClusterConfiguration) is { } node
&& node.Slots.Count > 0)
{
key = key.Prepend(ServerSelectionStrategy.GetHashTagPrefix(node.Slots[0].From));
}
return key;
}

internal UnselectableFlags GetUnselectableFlags() => unselectableReasons;

internal bool IsSelectable(RedisCommand command, bool allowDisconnected = false)
Expand Down
19 changes: 18 additions & 1 deletion src/StackExchange.Redis/ServerSelectionStrategy.HashTags.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System;
using System.Diagnostics;
using System.Text;
using System.Threading;

namespace StackExchange.Redis;

Expand All @@ -10,9 +11,25 @@ internal sealed partial class ServerSelectionStrategy
private static class HashTags
{
private static readonly string[] Cache = Populate();
private static readonly byte[]?[] PrefixCache = new byte[TotalSlots][];
private static readonly object PrefixCacheLock = new();

public static ReadOnlySpan<string> Tags => Cache;
public static string Get(int slot) => Cache[slot];

public static byte[] GetPrefix(int slot)
{
var prefix = Volatile.Read(ref PrefixCache[slot]);
if (prefix is null)
{
lock (PrefixCacheLock)
{
prefix = PrefixCache[slot] ??= Encoding.ASCII.GetBytes("{" + Get(slot) + "}");
}
}
return prefix;
}

private static string[] Populate()
{
// Via testing, we know that 3 characters is sufficient to populate all slots
Expand Down
2 changes: 2 additions & 0 deletions src/StackExchange.Redis/ServerSelectionStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -442,5 +442,7 @@ internal string GetHashTag(ServerEndPoint endpoint)
/// Gets a string that can be used as a hash-tag to reference a specific slot.
/// </summary>
internal static string GetHashTag(int slot) => slot < 0 ? "" : HashTags.Get(slot);

internal static RedisKey GetHashTagPrefix(int slot) => HashTags.GetPrefix(slot);
}
}
16 changes: 15 additions & 1 deletion tests/StackExchange.Redis.Tests/HashTagUnitTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Text;
using Xunit;
Expand Down Expand Up @@ -26,4 +26,18 @@ public void TestHashTagCoverage()
}
Assert.Equal(ServerSelectionStrategy.TotalSlots, uniques.Count);
}

[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(8191)]
[InlineData(16383)]
public void TestHashTagPrefixTargetsSlot(int slot)
{
var prefix = ServerSelectionStrategy.GetHashTagPrefix(slot);
RedisKey key = ((RedisKey)"probe-id").Prepend(prefix);

Assert.Equal(slot, ServerSelectionStrategy.GetHashSlot(key));
Assert.Same((byte[]?)prefix, (byte[]?)ServerSelectionStrategy.GetHashTagPrefix(slot));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Xunit;

namespace StackExchange.Redis.Tests;

public class ServerEndPointClusterProbeUnitTests
{
[Fact]
public async Task ExistsTracerUsesOwnedClusterSlot()
{
using var server = new InProcessTestServer { ServerType = ServerType.Cluster };
var config = server.GetClientConfig(defaultOnly: true);
var commands = server.GetCommands();
commands.Remove(nameof(RedisCommand.ECHO));
commands.Remove(nameof(RedisCommand.PING));
commands.Remove(nameof(RedisCommand.TIME));
config.CommandMap = CommandMap.Create(commands);

await using var connection = await ConnectionMultiplexer.ConnectAsync(config);
var endpoint = connection.GetServerEndPoint(server.DefaultEndPoint);
var node = endpoint.ClusterConfiguration?.Nodes.Single(x => x.EndPoint?.Equals(endpoint.EndPoint) == true);
Assert.NotNull(node);
var targetSlot = node.Slots[0].From;

var message = endpoint.GetTracerMessage(checkResponse: true);

Assert.Equal(RedisCommand.EXISTS, message.Command);
Assert.Equal(targetSlot, message.GetHashSlot(connection.ServerSelectionStrategy));
var key = endpoint.GetTracerKey();
var keyBytes = (byte[]?)key;
var prefixBytes = (byte[]?)ServerSelectionStrategy.GetHashTagPrefix(targetSlot);
Assert.NotNull(keyBytes);
Assert.NotNull(prefixBytes);
Assert.True(keyBytes.Take(prefixBytes.Length).SequenceEqual(prefixBytes));
Assert.True(keyBytes.Skip(keyBytes.Length - connection.UniqueId.Length).SequenceEqual(connection.UniqueId));
}

[Theory]
[InlineData(ServerType.Standalone)]
[InlineData(ServerType.Cluster)]
public async Task ExistsTracerUsesPlainKeyWithoutKnownOwnedSlots(ServerType serverType)
{
using var server = new InProcessTestServer();
var config = server.GetClientConfig(defaultOnly: true);
var commands = server.GetCommands();
commands.Remove(nameof(RedisCommand.ECHO));
commands.Remove(nameof(RedisCommand.PING));
commands.Remove(nameof(RedisCommand.TIME));
config.CommandMap = CommandMap.Create(commands);

await using var connection = await ConnectionMultiplexer.ConnectAsync(config);
var endpoint = new ServerEndPoint(connection, new IPEndPoint(IPAddress.Loopback, 12345))
{
ServerType = serverType,
};

var message = endpoint.GetTracerMessage(checkResponse: true);
var clusterStrategy = new ServerSelectionStrategy(null) { ServerType = ServerType.Cluster };

Assert.Equal(RedisCommand.EXISTS, message.Command);
Assert.Equal(ServerSelectionStrategy.GetHashSlot((RedisKey)connection.UniqueId), message.GetHashSlot(clusterStrategy));
Assert.Equal(connection.UniqueId, (byte[]?)endpoint.GetTracerKey());
}
}
Loading