Skip to content

Commit 989528f

Browse files
Add support for SORT_RO (#2111)
Introducing [`SORT_RO`](https://redis.io/commands/sort_ro/) as part of #2055 @NickCraver - I modified the path for message creation for the Sort command to point to using `SORT_RO` when possible, this again had to do a version-check against the multiplexer, which I'm not certain is the correct way - thoughts? Also, SORT is considered a write command and will be rejected out of hand by a replica (so I'm moving that as well) ```text 127.0.0.1:6378> SORT test (error) READONLY You can't write against a read only replica. ``` Co-authored-by: Nick Craver <[email protected]>
1 parent f0fa79c commit 989528f

File tree

7 files changed

+81
-27
lines changed

7 files changed

+81
-27
lines changed

docs/ReleaseNotes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
- Adds: Support for `ZMPOP` with `.SortedSetPop()`/`.SortedSetPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094))
2828
- Adds: Support for `XAUTOCLAIM` with `.StreamAutoClaim()`/.`StreamAutoClaimAsync()` and `.StreamAutoClaimIdsOnly()`/.`StreamAutoClaimIdsOnlyAsync()` ([#2095 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2095))
2929
- Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2105))
30+
- Adds: Support for `SORT_RO` with `.Sort()`/`.SortAsync()` ([#2111 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2111))
3031

3132
## 2.5.61
3233

src/StackExchange.Redis/Enums/RedisCommand.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ internal enum RedisCommand
166166
SMISMEMBER,
167167
SMOVE,
168168
SORT,
169+
SORT_RO,
169170
SPOP,
170171
SRANDMEMBER,
171172
SREM,
@@ -320,6 +321,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
320321
case RedisCommand.SETRANGE:
321322
case RedisCommand.SINTERSTORE:
322323
case RedisCommand.SMOVE:
324+
case RedisCommand.SORT:
323325
case RedisCommand.SPOP:
324326
case RedisCommand.SREM:
325327
case RedisCommand.SUNIONSTORE:
@@ -428,7 +430,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
428430
case RedisCommand.SLOWLOG:
429431
case RedisCommand.SMEMBERS:
430432
case RedisCommand.SMISMEMBER:
431-
case RedisCommand.SORT:
433+
case RedisCommand.SORT_RO:
432434
case RedisCommand.SRANDMEMBER:
433435
case RedisCommand.STRLEN:
434436
case RedisCommand.SUBSCRIBE:

src/StackExchange.Redis/Interfaces/IDatabase.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1551,6 +1551,7 @@ public interface IDatabase : IRedis, IDatabaseAsync
15511551
/// the <c>get</c> parameter (note that <c>#</c> specifies the element itself, when used in <c>get</c>).
15521552
/// Referring to the <a href="https://redis.io/commands/sort">redis SORT documentation </a> for examples is recommended.
15531553
/// When used in hashes, <c>by</c> and <c>get</c> can be used to specify fields using <c>-&gt;</c> notation (again, refer to redis documentation).
1554+
/// Uses <a href="https://redis.io/commands/sort_ro">SORT_RO</a> when possible.
15541555
/// </summary>
15551556
/// <param name="key">The key of the list, set, or sorted set.</param>
15561557
/// <param name="skip">How many entries to skip on the return.</param>
@@ -1562,6 +1563,7 @@ public interface IDatabase : IRedis, IDatabaseAsync
15621563
/// <param name="flags">The flags to use for this operation.</param>
15631564
/// <returns>The sorted elements, or the external values if <c>get</c> is specified.</returns>
15641565
/// <remarks><seealso href="https://redis.io/commands/sort"/></remarks>
1566+
/// <remarks><seealso href="https://redis.io/commands/sort_ro"/></remarks>
15651567
RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None);
15661568

15671569
/// <summary>

src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,6 +1516,7 @@ public interface IDatabaseAsync : IRedisAsync
15161516
/// the <c>get</c> parameter (note that <c>#</c> specifies the element itself, when used in <c>get</c>).
15171517
/// Referring to the <a href="https://redis.io/commands/sort">redis SORT documentation </a> for examples is recommended.
15181518
/// When used in hashes, <c>by</c> and <c>get</c> can be used to specify fields using <c>-&gt;</c> notation (again, refer to redis documentation).
1519+
/// Uses <a href="https://redis.io/commands/sort_ro">SORT_RO</a> when possible.
15191520
/// </summary>
15201521
/// <param name="key">The key of the list, set, or sorted set.</param>
15211522
/// <param name="skip">How many entries to skip on the return.</param>

src/StackExchange.Redis/RedisDatabase.cs

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1775,26 +1775,26 @@ private CursorEnumerable<RedisValue> SetScanAsync(RedisKey key, RedisValue patte
17751775

17761776
public RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None)
17771777
{
1778-
var msg = GetSortedSetAddMessage(default(RedisKey), key, skip, take, order, sortType, by, get, flags);
1779-
return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty<RedisValue>());
1778+
var msg = GetSortMessage(RedisKey.Null, key, skip, take, order, sortType, by, get, flags, out var server);
1779+
return ExecuteSync(msg, ResultProcessor.RedisValueArray, server: server, defaultValue: Array.Empty<RedisValue>());
17801780
}
17811781

17821782
public long SortAndStore(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None)
17831783
{
1784-
var msg = GetSortedSetAddMessage(destination, key, skip, take, order, sortType, by, get, flags);
1785-
return ExecuteSync(msg, ResultProcessor.Int64);
1784+
var msg = GetSortMessage(destination, key, skip, take, order, sortType, by, get, flags, out var server);
1785+
return ExecuteSync(msg, ResultProcessor.Int64, server);
17861786
}
17871787

17881788
public Task<long> SortAndStoreAsync(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None)
17891789
{
1790-
var msg = GetSortedSetAddMessage(destination, key, skip, take, order, sortType, by, get, flags);
1791-
return ExecuteAsync(msg, ResultProcessor.Int64);
1790+
var msg = GetSortMessage(destination, key, skip, take, order, sortType, by, get, flags, out var server);
1791+
return ExecuteAsync(msg, ResultProcessor.Int64, server);
17921792
}
17931793

17941794
public Task<RedisValue[]> SortAsync(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None)
17951795
{
1796-
var msg = GetSortedSetAddMessage(default(RedisKey), key, skip, take, order, sortType, by, get, flags);
1797-
return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty<RedisValue>());
1796+
var msg = GetSortMessage(RedisKey.Null, key, skip, take, order, sortType, by, get, flags, out var server);
1797+
return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty<RedisValue>(), server: server);
17981798
}
17991799

18001800
public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags)
@@ -3513,28 +3513,25 @@ private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double s
35133513
}
35143514
}
35153515

3516-
private Message GetSortedSetAddMessage(RedisKey destination, RedisKey key, long skip, long take, Order order, SortType sortType, RedisValue by, RedisValue[]? get, CommandFlags flags)
3516+
private Message GetSortMessage(RedisKey destination, RedisKey key, long skip, long take, Order order, SortType sortType, RedisValue by, RedisValue[]? get, CommandFlags flags, out ServerEndPoint? server)
35173517
{
3518+
server = null;
3519+
var command = destination.IsNull && GetFeatures(key, flags, out server).ReadOnlySort
3520+
? RedisCommand.SORT_RO
3521+
: RedisCommand.SORT;
3522+
35183523
// most common cases; no "get", no "by", no "destination", no "skip", no "take"
35193524
if (destination.IsNull && skip == 0 && take == -1 && by.IsNull && (get == null || get.Length == 0))
35203525
{
3521-
switch (order)
3526+
return order switch
35223527
{
3523-
case Order.Ascending:
3524-
switch (sortType)
3525-
{
3526-
case SortType.Numeric: return Message.Create(Database, flags, RedisCommand.SORT, key);
3527-
case SortType.Alphabetic: return Message.Create(Database, flags, RedisCommand.SORT, key, RedisLiterals.ALPHA);
3528-
}
3529-
break;
3530-
case Order.Descending:
3531-
switch (sortType)
3532-
{
3533-
case SortType.Numeric: return Message.Create(Database, flags, RedisCommand.SORT, key, RedisLiterals.DESC);
3534-
case SortType.Alphabetic: return Message.Create(Database, flags, RedisCommand.SORT, key, RedisLiterals.DESC, RedisLiterals.ALPHA);
3535-
}
3536-
break;
3537-
}
3528+
Order.Ascending when sortType == SortType.Numeric => Message.Create(Database, flags, command, key),
3529+
Order.Ascending when sortType == SortType.Alphabetic => Message.Create(Database, flags, command, key, RedisLiterals.ALPHA),
3530+
Order.Descending when sortType == SortType.Numeric => Message.Create(Database, flags, command, key, RedisLiterals.DESC),
3531+
Order.Descending when sortType == SortType.Alphabetic => Message.Create(Database, flags, command, key, RedisLiterals.DESC, RedisLiterals.ALPHA),
3532+
Order.Ascending or Order.Descending => throw new ArgumentOutOfRangeException(nameof(sortType)),
3533+
_ => throw new ArgumentOutOfRangeException(nameof(order)),
3534+
};
35383535
}
35393536

35403537
// and now: more complicated scenarios...
@@ -3578,7 +3575,7 @@ private Message GetSortedSetAddMessage(RedisKey destination, RedisKey key, long
35783575
values.Add(item);
35793576
}
35803577
}
3581-
if (destination.IsNull) return Message.Create(Database, flags, RedisCommand.SORT, key, values.ToArray());
3578+
if (destination.IsNull) return Message.Create(Database, flags, command, key, values.ToArray());
35823579

35833580
// Because we are using STORE, we need to push this to a primary
35843581
if (Message.GetPrimaryReplicaFlags(flags) == CommandFlags.DemandReplica)

src/StackExchange.Redis/RedisFeatures.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@ public RedisFeatures(Version version)
132132
/// </summary>
133133
public bool PushIfNotExists => Version >= v2_1_1;
134134

135+
/// <summary>
136+
/// Does this support <see href="https://redis.io/commands/sort_ro">SORT_RO</see>?
137+
/// </summary>
138+
internal bool ReadOnlySort => Version >= v7_0_0_rc1;
139+
135140
/// <summary>
136141
/// Is <see href="https://redis.io/commands/scan/">SCAN</see> (cursor-based scanning) available?
137142
/// </summary>

tests/StackExchange.Redis.Tests/Sets.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,4 +343,50 @@ public void SetPopMulti_Nil()
343343
var arr = db.SetPop(key, 1);
344344
Assert.Empty(arr);
345345
}
346+
347+
[Fact]
348+
public async Task TestSortReadonlyPrimary()
349+
{
350+
using var conn = Create();
351+
352+
var db = conn.GetDatabase();
353+
var key = Me();
354+
await db.KeyDeleteAsync(key);
355+
356+
var random = new Random();
357+
var items = Enumerable.Repeat(0, 200).Select(_ => random.Next()).ToList();
358+
await db.SetAddAsync(key, items.Select(x=>(RedisValue)x).ToArray());
359+
items.Sort();
360+
361+
var result = db.Sort(key).Select(x=>(int)x);
362+
Assert.Equal(items, result);
363+
364+
result = (await db.SortAsync(key)).Select(x => (int)x);
365+
Assert.Equal(items, result);
366+
}
367+
368+
[Fact]
369+
public async Task TestSortReadonlyReplica()
370+
{
371+
using var conn = Create(require: RedisFeatures.v7_0_0_rc1);
372+
373+
var db = conn.GetDatabase();
374+
var key = Me();
375+
await db.KeyDeleteAsync(key);
376+
377+
var random = new Random();
378+
var items = Enumerable.Repeat(0, 200).Select(_ => random.Next()).ToList();
379+
await db.SetAddAsync(key, items.Select(x=>(RedisValue)x).ToArray());
380+
381+
using var readonlyConn = Create(configuration: TestConfig.Current.ReplicaServerAndPort, require: RedisFeatures.v7_0_0_rc1);
382+
var readonlyDb = conn.GetDatabase();
383+
384+
items.Sort();
385+
386+
var result = readonlyDb.Sort(key).Select(x => (int)x);
387+
Assert.Equal(items, result);
388+
389+
result = (await readonlyDb.SortAsync(key)).Select(x => (int)x);
390+
Assert.Equal(items, result);
391+
}
346392
}

0 commit comments

Comments
 (0)