diff --git a/src/StackExchange.Redis/ArrayGrepRequest.cs b/src/StackExchange.Redis/ArrayGrepRequest.cs index 7f00cf147..21e0bac5f 100644 --- a/src/StackExchange.Redis/ArrayGrepRequest.cs +++ b/src/StackExchange.Redis/ArrayGrepRequest.cs @@ -337,7 +337,7 @@ internal override void WriteTo(in MessageWriter writer) } private sealed class ArrayGrepMessage(int db, RedisKey key, ArrayGrepRequest request, CommandFlags flags) - : Message(db, flags, RedisCommand.ARGREP) + : Message.CommandKeyBase(db, flags, RedisCommand.ARGREP, key) { public override int ArgCount { @@ -374,7 +374,7 @@ private static void AddIndex(in MessageWriter writer, RedisArrayIndex? index, Re protected override void WriteImpl(in MessageWriter writer) { writer.WriteHeader(Command, ArgCount); - writer.Write(key); + writer.Write(Key); if (request.IsReversed) { AddIndex(writer, request.End, "$1\r\n+\r\n"u8); diff --git a/src/StackExchange.Redis/Increx.IncrexMessage.cs b/src/StackExchange.Redis/Increx.IncrexMessage.cs index 6941e60e6..2649fb000 100644 --- a/src/StackExchange.Redis/Increx.IncrexMessage.cs +++ b/src/StackExchange.Redis/Increx.IncrexMessage.cs @@ -9,9 +9,8 @@ internal abstract class IncrexMessageBase( CommandFlags flags, RedisKey key, Expiration expiry, - IncrementOptions options) : Message(database, flags, RedisCommand.INCREX) + IncrementOptions options) : Message.CommandKeyBase(database, flags, RedisCommand.INCREX, key) { - protected RedisKey Key => key; protected Expiration Expiry => expiry; protected IncrementOptions Options => options; diff --git a/tests/StackExchange.Redis.Tests/KeyedMessageSlotTests.cs b/tests/StackExchange.Redis.Tests/KeyedMessageSlotTests.cs new file mode 100644 index 000000000..61c03fee6 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/KeyedMessageSlotTests.cs @@ -0,0 +1,96 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace StackExchange.Redis.Tests; + +/// +/// A message that carries a key must report that key's hash slot, or a cluster client sends it to an arbitrary node +/// and leans on -MOVED: an extra round trip in the usual case, and a failure under +/// . INCREX and ARGREP both shipped without one. +/// +public class KeyedMessageSlotTests(ITestOutputHelper output) : TestBase(output) +{ + private static readonly ServerSelectionStrategy Cluster = new(null!) { ServerType = ServerType.Cluster }; + + private static void AssertRoutedByKey(Message message, RedisKey key) + { + var expected = Cluster.HashSlot(key); + Assert.NotEqual(ServerSelectionStrategy.NoSlot, expected); + Assert.Equal(expected, message.GetHashSlot(Cluster)); + } + + [Fact] + public void Increx_Int64_IsRoutedByKey() + { + RedisKey key = "some-counter"; + var msg = new RedisDatabase.IncrexInt64Message(0, CommandFlags.None, key, 1, null, null, default, IncrementOptions.None); + AssertRoutedByKey(msg, key); + Assert.Equal("INCREX some-counter", msg.CommandAndKey); + } + + [Fact] + public void Increx_Double_IsRoutedByKey() + { + RedisKey key = "some-counter"; + var msg = new RedisDatabase.IncrexDoubleMessage(0, CommandFlags.None, key, 1.5, null, null, default, IncrementOptions.None); + AssertRoutedByKey(msg, key); + } + + [Fact] + public void ArrayGrep_IsRoutedByKey() + { + RedisKey key = "some-array"; + AssertRoutedByKey(CreateGrep().CreateMessage(0, key, CommandFlags.None), key); + } + + private static ArrayGrepRequest CreateGrep() + { + var request = new ArrayGrepRequest(); + request.AddPredicate(ArrayGrepRequest.Predicate.Match("alpha")); + return request; + } + + // enough keys that, sent to an arbitrary node, some would certainly miss their owner + private const int KeyCount = 32; + + [Fact] + public async Task Increx_OnCluster_NeedsNoRedirect() + { + await using var conn = Create(configuration: GetClusterConfiguration(), require: RedisFeatures.v8_8_0); + var db = conn.GetDatabase(); + var expiry = Expiration.CreateOrKeepTtl(TimeSpan.FromMinutes(1), keepTtl: false); + + for (int i = 0; i < KeyCount; i++) + { + RedisKey key = Me() + ":" + i; + await db.KeyDeleteAsync(key); + + // NoRedirect: lands on the slot owner first time, or throws + var result = await db.StringIncrementAsync(key, 5, expiry, flags: CommandFlags.NoRedirect); + Assert.Equal(5, result.Value); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + } + } + + [Fact] + public async Task ArrayGrep_OnCluster_NeedsNoRedirect() + { + await using var conn = Create(configuration: GetClusterConfiguration(), require: RedisFeatures.v8_8_0); + var db = conn.GetDatabase(); + var grep = CreateGrep(); + + for (int i = 0; i < KeyCount; i++) + { + RedisKey key = Me() + ":" + i; + await db.KeyDeleteAsync(key); + await db.ArraySetAsync(key, 0, "alpha"); + + var found = await db.ArrayGrepAsync(key, grep, CommandFlags.NoRedirect); + Assert.Equal(0, Assert.Single(found).Index); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + } + } +}