Skip to content

Commit 620c281

Browse files
adding some smarts to GetFeatures (#2191)
@NickCraver - This will fix #2016 The issue is that when `GetFeatures` does the server selection, it uses `PING` as the command it's asking about, so when the multiplexer responds with a server, it responds with any server. Of course, then when it goes to execute the command, it's been explicitly handed a server, so it honors that choice but checks it to make sure it's a valid server to send the command to, so if you're executing a write command and `GetFeatuers` just so happened to output a read-only replica, when the multiplexer does the 'is this a valid server?' check, it determines it is not and propagates the error. that's what causes the blowup. By passing in the RedisCommand to `GetFeatures`, we allow it to make an informed choice of server for that Redis Command. The other option is to simply not pass the server on down the line when we we've done a feature check, and allow the muxer to make it's own decision, this would cause an extra run of `Select` which the current pattern e.g. see [sort](https://github.com/StackExchange/StackExchange.Redis/blob/main/src/StackExchange.Redis/RedisDatabase.cs#L1816) tries to avoid One weird case, with SORT/SORT_RO, if the RO command is what we prefer to run (because we don't have a destination key to output it to), and `SORT_RO` is not available (because they aren't on Redis 7+), we cannot trust the output of GetFeatures, so I just set the selected server back to null and let the muxer figure it out down the line. Co-authored-by: Nick Craver <[email protected]>
1 parent 4f5ebac commit 620c281

File tree

6 files changed

+23
-16
lines changed

6 files changed

+23
-16
lines changed

docs/ReleaseNotes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Fix [#2182](https://github.com/StackExchange/StackExchange.Redis/issues/2182): Be more flexible in which commands are "primary only" in order to support users with replicas that are explicitly configured to allow writes ([#2183 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2183))
66
- Adds: `IConnectionMultiplexer` now implements `IAsyncDisposable` ([#2161 by kimsey0](https://github.com/StackExchange/StackExchange.Redis/pull/2161))
7+
- Fix [#2016](https://github.com/StackExchange/StackExchange.Redis/issues/2016): Align server selection with supported commands (e.g. with writable servers) to reduce `Command cannot be issued to a replica` errors ([#2191 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2191))
78

89
## 2.6.48
910

src/Directory.Build.props

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
<ItemGroup>
88
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" PrivateAssets="all" />
99
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="all" />
10-
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" PrivateAssets="All" />
1110
<PackageReference Include="Nerdbank.GitVersioning" PrivateAssets="all" Condition=" '$(DEVCONTAINER)' != 'true' " />
1211
</ItemGroup>
1312
</Project>

src/StackExchange.Redis/RedisBase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ internal virtual Task<T> ExecuteAsync<T>(Message? message, ResultProcessor<T>? p
6262
return multiplexer.ExecuteSyncImpl<T>(message, processor, server, defaultValue);
6363
}
6464

65-
internal virtual RedisFeatures GetFeatures(in RedisKey key, CommandFlags flags, out ServerEndPoint? server)
65+
internal virtual RedisFeatures GetFeatures(in RedisKey key, CommandFlags flags, RedisCommand command, out ServerEndPoint? server)
6666
{
67-
server = multiplexer.SelectServer(RedisCommand.PING, flags, key);
67+
server = multiplexer.SelectServer(command, flags, key);
6868
var version = server == null ? multiplexer.RawConfig.DefaultVersion : server.Version;
6969
return new RedisFeatures(version);
7070
}

src/StackExchange.Redis/RedisDatabase.cs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ public Task<bool> HyperLogLogAddAsync(RedisKey key, RedisValue[] values, Command
635635

636636
public long HyperLogLogLength(RedisKey key, CommandFlags flags = CommandFlags.None)
637637
{
638-
var features = GetFeatures(key, flags, out ServerEndPoint? server);
638+
var features = GetFeatures(key, flags, RedisCommand.PFCOUNT, out ServerEndPoint? server);
639639
var cmd = Message.Create(Database, flags, RedisCommand.PFCOUNT, key);
640640
// technically a write / primary-only command until 2.8.18
641641
if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetPrimaryOnly();
@@ -649,7 +649,7 @@ public long HyperLogLogLength(RedisKey[] keys, CommandFlags flags = CommandFlags
649649
var cmd = Message.Create(Database, flags, RedisCommand.PFCOUNT, keys);
650650
if (keys.Length != 0)
651651
{
652-
var features = GetFeatures(keys[0], flags, out server);
652+
var features = GetFeatures(keys[0], flags, RedisCommand.PFCOUNT, out server);
653653
// technically a write / primary-only command until 2.8.18
654654
if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetPrimaryOnly();
655655
}
@@ -658,7 +658,7 @@ public long HyperLogLogLength(RedisKey[] keys, CommandFlags flags = CommandFlags
658658

659659
public Task<long> HyperLogLogLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None)
660660
{
661-
var features = GetFeatures(key, flags, out ServerEndPoint? server);
661+
var features = GetFeatures(key, flags, RedisCommand.PFCOUNT, out ServerEndPoint? server);
662662
var cmd = Message.Create(Database, flags, RedisCommand.PFCOUNT, key);
663663
// technically a write / primary-only command until 2.8.18
664664
if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetPrimaryOnly();
@@ -672,7 +672,7 @@ public Task<long> HyperLogLogLengthAsync(RedisKey[] keys, CommandFlags flags = C
672672
var cmd = Message.Create(Database, flags, RedisCommand.PFCOUNT, keys);
673673
if (keys.Length != 0)
674674
{
675-
var features = GetFeatures(keys[0], flags, out server);
675+
var features = GetFeatures(keys[0], flags, RedisCommand.PFCOUNT, out server);
676676
// technically a write / primary-only command until 2.8.18
677677
if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetPrimaryOnly();
678678
}
@@ -773,7 +773,7 @@ public Task<long> KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFl
773773

774774
private RedisCommand GetDeleteCommand(RedisKey key, CommandFlags flags, out ServerEndPoint? server)
775775
{
776-
var features = GetFeatures(key, flags, out server);
776+
var features = GetFeatures(key, flags, RedisCommand.UNLINK, out server);
777777
if (server != null && features.Unlink && multiplexer.CommandMap.IsAvailable(RedisCommand.UNLINK))
778778
{
779779
return RedisCommand.UNLINK;
@@ -1036,7 +1036,7 @@ public Task KeyRestoreAsync(RedisKey key, byte[] value, TimeSpan? expiry = null,
10361036

10371037
public TimeSpan? KeyTimeToLive(RedisKey key, CommandFlags flags = CommandFlags.None)
10381038
{
1039-
var features = GetFeatures(key, flags, out ServerEndPoint? server);
1039+
var features = GetFeatures(key, flags, RedisCommand.TTL, out ServerEndPoint? server);
10401040
Message msg;
10411041
if (server != null && features.MillisecondExpiry && multiplexer.CommandMap.IsAvailable(RedisCommand.PTTL))
10421042
{
@@ -1049,7 +1049,7 @@ public Task KeyRestoreAsync(RedisKey key, byte[] value, TimeSpan? expiry = null,
10491049

10501050
public Task<TimeSpan?> KeyTimeToLiveAsync(RedisKey key, CommandFlags flags = CommandFlags.None)
10511051
{
1052-
var features = GetFeatures(key, flags, out ServerEndPoint? server);
1052+
var features = GetFeatures(key, flags, RedisCommand.TTL, out ServerEndPoint? server);
10531053
Message msg;
10541054
if (server != null && features.MillisecondExpiry && multiplexer.CommandMap.IsAvailable(RedisCommand.PTTL))
10551055
{
@@ -3278,7 +3278,7 @@ private Message GetExpiryMessage(in RedisKey key,
32783278
server = null;
32793279
if ((milliseconds % 1000) != 0)
32803280
{
3281-
var features = GetFeatures(key, flags, out server);
3281+
var features = GetFeatures(key, flags, RedisCommand.PEXPIRE, out server);
32823282
if (server is not null && features.MillisecondExpiry && multiplexer.CommandMap.IsAvailable(millisecondsCommand))
32833283
{
32843284
return when switch
@@ -3675,10 +3675,17 @@ private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double s
36753675
private Message GetSortMessage(RedisKey destination, RedisKey key, long skip, long take, Order order, SortType sortType, RedisValue by, RedisValue[]? get, CommandFlags flags, out ServerEndPoint? server)
36763676
{
36773677
server = null;
3678-
var command = destination.IsNull && GetFeatures(key, flags, out server).ReadOnlySort
3678+
var command = destination.IsNull && GetFeatures(key, flags, RedisCommand.SORT_RO, out server).ReadOnlySort
36793679
? RedisCommand.SORT_RO
36803680
: RedisCommand.SORT;
36813681

3682+
//if SORT_RO is not available, we cannot issue the command to a read-only replica
3683+
if (command == RedisCommand.SORT)
3684+
{
3685+
server = null;
3686+
}
3687+
3688+
36823689
// most common cases; no "get", no "by", no "destination", no "skip", no "take"
36833690
if (destination.IsNull && skip == 0 && take == -1 && by.IsNull && (get == null || get.Length == 0))
36843691
{
@@ -4338,7 +4345,7 @@ private Message GetStringGetWithExpiryMessage(RedisKey key, CommandFlags flags,
43384345
{
43394346
throw new NotSupportedException("This operation is not possible inside a transaction or batch; please issue separate GetString and KeyTimeToLive requests");
43404347
}
4341-
var features = GetFeatures(key, flags, out server);
4348+
var features = GetFeatures(key, flags, RedisCommand.PTTL, out server);
43424349
processor = StringGetWithExpiryProcessor.Default;
43434350
if (server != null && features.MillisecondExpiry && multiplexer.CommandMap.IsAvailable(RedisCommand.PTTL))
43444351
{
@@ -4495,7 +4502,7 @@ private Message GetStringSetAndGetMessage(
44954502
throw new ArgumentOutOfRangeException(nameof(pageSize));
44964503
if (!multiplexer.CommandMap.IsAvailable(command)) return null;
44974504

4498-
var features = GetFeatures(key, flags, out server);
4505+
var features = GetFeatures(key, flags, RedisCommand.SCAN, out server);
44994506
if (!features.Scan) return null;
45004507

45014508
if (CursorUtils.IsNil(pattern)) pattern = (byte[]?)null;

src/StackExchange.Redis/RedisServer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -742,7 +742,7 @@ internal override Task<T> ExecuteAsync<T>(Message? message, ResultProcessor<T>?
742742
return base.ExecuteSync<T>(message, processor, server, defaultValue);
743743
}
744744

745-
internal override RedisFeatures GetFeatures(in RedisKey key, CommandFlags flags, out ServerEndPoint server)
745+
internal override RedisFeatures GetFeatures(in RedisKey key, CommandFlags flags, RedisCommand command, out ServerEndPoint server)
746746
{
747747
server = this.server;
748748
return server.GetFeatures();

src/StackExchange.Redis/RedisSubscriber.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ private Message CreatePingMessage(CommandFlags flags)
307307
bool usePing = false;
308308
if (multiplexer.CommandMap.IsAvailable(RedisCommand.PING))
309309
{
310-
try { usePing = GetFeatures(default, flags, out _).PingOnSubscriber; }
310+
try { usePing = GetFeatures(default, flags, RedisCommand.PING, out _).PingOnSubscriber; }
311311
catch { }
312312
}
313313

0 commit comments

Comments
 (0)