Skip to content

Commit 3d1b0da

Browse files
authored
Fix incorrect routing of some sorted-set and stream commands (#3080)
* 1. test and fix incorrect routing of some "sorted set" and "stream" commands 2. fix via new custom efficient message cores 3. add reusable helpers for common null checks on key/value arrays 4. prep work for likely ZDIFFCARD/ZUNIONCARD * rationalize * prefer async tests * add release note
1 parent ce62271 commit 3d1b0da

10 files changed

Lines changed: 422 additions & 190 deletions

File tree

docs/ReleaseNotes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Current package versions:
1111
- Add experimental Redis 8.8 array support, including array APIs on `IDatabase`/`IDatabaseAsync`,
1212
array helper types, `RedisType.Array`, and array delete keyspace notification event types. ([#3076 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3076))
1313
- Enable TCP keep-alives ([#3078 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3078))
14+
- Fix incorrect routing of some sorted-set and stream commands ([#3080 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3080))
1415
- `ConfigurationOptions` : don't persist `Protocol` when it comes from the defaults-provider. ([#3082 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3082))
1516

1617
## 2.13.1

src/StackExchange.Redis/Enums/SetOperation.cs

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,39 @@ public enum SetOperation
2525

2626
internal static class SetOperationExtensions
2727
{
28-
internal static RedisCommand ToCommand(this SetOperation operation, bool store) => operation switch
28+
internal static RedisCommand ToSetCommand(this SetOperation operation) => operation switch
29+
{
30+
SetOperation.Union => RedisCommand.SUNION,
31+
SetOperation.Intersect => RedisCommand.SINTER,
32+
SetOperation.Difference => RedisCommand.SDIFF,
33+
_ => OutOfRange(operation),
34+
};
35+
36+
internal static RedisCommand ToSetStoreCommand(this SetOperation operation) => operation switch
37+
{
38+
SetOperation.Union => RedisCommand.SUNIONSTORE,
39+
SetOperation.Intersect => RedisCommand.SINTERSTORE,
40+
SetOperation.Difference => RedisCommand.SDIFFSTORE,
41+
_ => OutOfRange(operation),
42+
};
43+
44+
internal static RedisCommand ToSortedSetCommand(this SetOperation operation) => operation switch
2945
{
30-
SetOperation.Intersect when store => RedisCommand.ZINTERSTORE,
31-
SetOperation.Intersect => RedisCommand.ZINTER,
32-
SetOperation.Union when store => RedisCommand.ZUNIONSTORE,
3346
SetOperation.Union => RedisCommand.ZUNION,
34-
SetOperation.Difference when store => RedisCommand.ZDIFFSTORE,
47+
SetOperation.Intersect => RedisCommand.ZINTER,
3548
SetOperation.Difference => RedisCommand.ZDIFF,
36-
_ => throw new ArgumentOutOfRangeException(nameof(operation)),
49+
_ => OutOfRange(operation),
50+
};
51+
52+
internal static RedisCommand ToSortedSetStoreCommand(this SetOperation operation) => operation switch
53+
{
54+
SetOperation.Union => RedisCommand.ZUNIONSTORE,
55+
SetOperation.Intersect => RedisCommand.ZINTERSTORE,
56+
SetOperation.Difference => RedisCommand.ZDIFFSTORE,
57+
_ => OutOfRange(operation),
3758
};
59+
60+
private static RedisCommand OutOfRange(SetOperation operation) =>
61+
throw new ArgumentOutOfRangeException(nameof(operation), operation, null);
3862
}
3963
}

src/StackExchange.Redis/ExtensionMethods.Internal.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Diagnostics.CodeAnalysis;
34

45
namespace StackExchange.Redis
@@ -11,6 +12,26 @@ internal static bool IsNullOrEmpty([NotNullWhen(false)] this string? s) =>
1112
internal static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? s) =>
1213
string.IsNullOrWhiteSpace(s);
1314

15+
internal static RedisKey[] AssertAllNonNull(this RedisKey[] keys)
16+
{
17+
if (keys is null) throw new ArgumentNullException(nameof(keys));
18+
for (var i = 0; i < keys.Length; i++)
19+
{
20+
keys[i].AssertNotNull();
21+
}
22+
return keys;
23+
}
24+
25+
internal static RedisValue[] AssertAllNonNull(this RedisValue[] values)
26+
{
27+
if (values is null) throw new ArgumentNullException(nameof(values));
28+
for (var i = 0; i < values.Length; i++)
29+
{
30+
values[i].AssertNotNull();
31+
}
32+
return values;
33+
}
34+
1435
#if !NET
1536
internal static bool TryDequeue<T>(this Queue<T> queue, [NotNullWhen(true)] out T? result)
1637
{

src/StackExchange.Redis/Message.cs

Lines changed: 35 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,11 @@ public static Message Create(
393393
public static Message CreateInSlot(int db, int slot, CommandFlags flags, RedisCommand command, RedisValue[] values) =>
394394
new CommandSlotValuesMessage(db, slot, flags, command, values);
395395

396+
// The key here is used only to route the message in cluster mode; it is not written as an argument.
397+
// Use this for command shapes where the key appears in a non-standard position in the values payload.
398+
public static Message CreateInKeySlot(int db, in RedisKey key, CommandFlags flags, RedisCommand command, RedisValue[] values) =>
399+
new CommandKeySlotValuesMessage(db, flags, command, key, values);
400+
396401
public static Message Create(int db, CommandFlags flags, RedisCommand command, KeyValuePair<RedisKey, RedisValue>[] values, Expiration expiry, When when)
397402
=> new MultiSetMessage(db, flags, command, values, expiry, when);
398403

@@ -984,21 +989,13 @@ private sealed class CommandKeyKeysMessage : CommandKeyBase
984989
private readonly RedisKey[] keys;
985990
public CommandKeyKeysMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key, RedisKey[] keys) : base(db, flags, command, key)
986991
{
987-
for (int i = 0; i < keys.Length; i++)
988-
{
989-
keys[i].AssertNotNull();
990-
}
991-
this.keys = keys;
992+
this.keys = keys.AssertAllNonNull();
992993
}
993994

994995
public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
995996
{
996997
var slot = serverSelectionStrategy.HashSlot(Key);
997-
for (int i = 0; i < keys.Length; i++)
998-
{
999-
slot = serverSelectionStrategy.CombineSlot(slot, keys[i]);
1000-
}
1001-
return slot;
998+
return serverSelectionStrategy.CombineSlot(slot, keys);
1002999
}
10031000

10041001
protected override void WriteImpl(PhysicalConnection physical)
@@ -1050,11 +1047,7 @@ private sealed class CommandValuesMessage : Message
10501047
private readonly RedisValue[] values;
10511048
public CommandValuesMessage(int db, CommandFlags flags, RedisCommand command, RedisValue[] values) : base(db, flags, command)
10521049
{
1053-
for (int i = 0; i < values.Length; i++)
1054-
{
1055-
values[i].AssertNotNull();
1056-
}
1057-
this.values = values;
1050+
this.values = values.AssertAllNonNull();
10581051
}
10591052

10601053
protected override void WriteImpl(PhysicalConnection physical)
@@ -1073,22 +1066,10 @@ private sealed class CommandKeysMessage : Message
10731066
private readonly RedisKey[] keys;
10741067
public CommandKeysMessage(int db, CommandFlags flags, RedisCommand command, RedisKey[] keys) : base(db, flags, command)
10751068
{
1076-
for (int i = 0; i < keys.Length; i++)
1077-
{
1078-
keys[i].AssertNotNull();
1079-
}
1080-
this.keys = keys;
1069+
this.keys = keys.AssertAllNonNull();
10811070
}
10821071

1083-
public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
1084-
{
1085-
int slot = ServerSelectionStrategy.NoSlot;
1086-
for (int i = 0; i < keys.Length; i++)
1087-
{
1088-
slot = serverSelectionStrategy.CombineSlot(slot, keys[i]);
1089-
}
1090-
return slot;
1091-
}
1072+
public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(keys);
10921073

10931074
protected override void WriteImpl(PhysicalConnection physical)
10941075
{
@@ -1125,11 +1106,7 @@ private sealed class CommandKeyValuesKeyMessage : CommandKeyBase
11251106
private readonly RedisValue[] values;
11261107
public CommandKeyValuesKeyMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, RedisValue[] values, in RedisKey key1) : base(db, flags, command, key0)
11271108
{
1128-
for (int i = 0; i < values.Length; i++)
1129-
{
1130-
values[i].AssertNotNull();
1131-
}
1132-
this.values = values;
1109+
this.values = values.AssertAllNonNull();
11331110
key1.AssertNotNull();
11341111
this.key1 = key1;
11351112
}
@@ -1155,11 +1132,7 @@ private sealed class CommandKeyValuesMessage : CommandKeyBase
11551132
private readonly RedisValue[] values;
11561133
public CommandKeyValuesMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key, RedisValue[] values) : base(db, flags, command, key)
11571134
{
1158-
for (int i = 0; i < values.Length; i++)
1159-
{
1160-
values[i].AssertNotNull();
1161-
}
1162-
this.values = values;
1135+
this.values = values.AssertAllNonNull();
11631136
}
11641137

11651138
protected override void WriteImpl(PhysicalConnection physical)
@@ -1177,14 +1150,9 @@ private sealed class CommandKeyKeyValuesMessage : CommandKeyBase
11771150
private readonly RedisValue[] values;
11781151
public CommandKeyKeyValuesMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisKey key1, RedisValue[] values) : base(db, flags, command, key)
11791152
{
1180-
for (int i = 0; i < values.Length; i++)
1181-
{
1182-
values[i].AssertNotNull();
1183-
}
1184-
11851153
key1.AssertNotNull();
11861154
this.key1 = key1;
1187-
this.values = values;
1155+
this.values = values.AssertAllNonNull();
11881156
}
11891157

11901158
protected override void WriteImpl(PhysicalConnection physical)
@@ -1204,16 +1172,11 @@ private sealed class CommandKeyValueValueValuesMessage : CommandKeyBase
12041172
private readonly RedisValue[] values;
12051173
public CommandKeyValueValueValuesMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, RedisValue[] values) : base(db, flags, command, key)
12061174
{
1207-
for (int i = 0; i < values.Length; i++)
1208-
{
1209-
values[i].AssertNotNull();
1210-
}
1211-
12121175
value0.AssertNotNull();
12131176
value1.AssertNotNull();
12141177
this.value0 = value0;
12151178
this.value1 = value1;
1216-
this.values = values;
1179+
this.values = values.AssertAllNonNull();
12171180
}
12181181

12191182
protected override void WriteImpl(PhysicalConnection physical)
@@ -1683,14 +1646,32 @@ public CommandSlotValuesMessage(int db, int slot, CommandFlags flags, RedisComma
16831646
: base(db, flags, command)
16841647
{
16851648
this.slot = slot;
1649+
this.values = values.AssertAllNonNull();
1650+
}
1651+
1652+
public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => slot;
1653+
1654+
protected override void WriteImpl(PhysicalConnection physical)
1655+
{
1656+
physical.WriteHeader(command, values.Length);
16861657
for (int i = 0; i < values.Length; i++)
16871658
{
1688-
values[i].AssertNotNull();
1659+
physical.WriteBulkString(values[i]);
16891660
}
1690-
this.values = values;
16911661
}
1662+
public override int ArgCount => values.Length;
1663+
}
16921664

1693-
public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => slot;
1665+
private sealed class CommandKeySlotValuesMessage : CommandKeyBase
1666+
{
1667+
private readonly RedisValue[] values;
1668+
1669+
public CommandKeySlotValuesMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key, RedisValue[] values)
1670+
: base(db, flags, command, key)
1671+
{
1672+
// Key is captured by CommandKeyBase for routing only; values are the complete serialized arguments.
1673+
this.values = values.AssertAllNonNull();
1674+
}
16941675

16951676
protected override void WriteImpl(PhysicalConnection physical)
16961677
{

0 commit comments

Comments
 (0)