diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 463da7c7f..9b366cd7e 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -27,6 +27,7 @@ - Adds: Support for `ZMPOP` with `.SortedSetPop()`/`.SortedSetPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094)) - Adds: Support for `XAUTOCLAIM` with `.StreamAutoClaim()`/.`StreamAutoClaimAsync()` and `.StreamAutoClaimIdsOnly()`/.`StreamAutoClaimIdsOnlyAsync()` ([#2095 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2095)) - Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2105)) +- Adds: Support for `SORT_RO` with `.Sort()`/`.SortAsync()` ([#2111 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2111)) ## 2.5.61 diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 9c8a6aa16..786316303 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -166,6 +166,7 @@ internal enum RedisCommand SMISMEMBER, SMOVE, SORT, + SORT_RO, SPOP, SRANDMEMBER, SREM, @@ -320,6 +321,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.SETRANGE: case RedisCommand.SINTERSTORE: case RedisCommand.SMOVE: + case RedisCommand.SORT: case RedisCommand.SPOP: case RedisCommand.SREM: case RedisCommand.SUNIONSTORE: @@ -428,7 +430,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.SLOWLOG: case RedisCommand.SMEMBERS: case RedisCommand.SMISMEMBER: - case RedisCommand.SORT: + case RedisCommand.SORT_RO: case RedisCommand.SRANDMEMBER: case RedisCommand.STRLEN: case RedisCommand.SUBSCRIBE: diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 5ca31542f..2485ad27f 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1551,6 +1551,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// the get parameter (note that # specifies the element itself, when used in get). /// Referring to the redis SORT documentation for examples is recommended. /// When used in hashes, by and get can be used to specify fields using -> notation (again, refer to redis documentation). + /// Uses SORT_RO when possible. /// /// The key of the list, set, or sorted set. /// How many entries to skip on the return. @@ -1562,6 +1563,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The sorted elements, or the external values if get is specified. /// + /// 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); /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 724be9caa..50d129eea 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1516,6 +1516,7 @@ public interface IDatabaseAsync : IRedisAsync /// the get parameter (note that # specifies the element itself, when used in get). /// Referring to the redis SORT documentation for examples is recommended. /// When used in hashes, by and get can be used to specify fields using -> notation (again, refer to redis documentation). + /// Uses SORT_RO when possible. /// /// The key of the list, set, or sorted set. /// How many entries to skip on the return. diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 47553653e..c3c189909 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1775,26 +1775,26 @@ private CursorEnumerable SetScanAsync(RedisKey key, RedisValue patte 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) { - var msg = GetSortedSetAddMessage(default(RedisKey), key, skip, take, order, sortType, by, get, flags); - return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + var msg = GetSortMessage(RedisKey.Null, key, skip, take, order, sortType, by, get, flags, out var server); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, server: server, defaultValue: Array.Empty()); } 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) { - var msg = GetSortedSetAddMessage(destination, key, skip, take, order, sortType, by, get, flags); - return ExecuteSync(msg, ResultProcessor.Int64); + var msg = GetSortMessage(destination, key, skip, take, order, sortType, by, get, flags, out var server); + return ExecuteSync(msg, ResultProcessor.Int64, server); } public Task 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) { - var msg = GetSortedSetAddMessage(destination, key, skip, take, order, sortType, by, get, flags); - return ExecuteAsync(msg, ResultProcessor.Int64); + var msg = GetSortMessage(destination, key, skip, take, order, sortType, by, get, flags, out var server); + return ExecuteAsync(msg, ResultProcessor.Int64, server); } public Task 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) { - var msg = GetSortedSetAddMessage(default(RedisKey), key, skip, take, order, sortType, by, get, flags); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + var msg = GetSortMessage(RedisKey.Null, key, skip, take, order, sortType, by, get, flags, out var server); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty(), server: server); } public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags) @@ -3513,28 +3513,25 @@ private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double s } } - private Message GetSortedSetAddMessage(RedisKey destination, RedisKey key, long skip, long take, Order order, SortType sortType, RedisValue by, RedisValue[]? get, CommandFlags flags) + private Message GetSortMessage(RedisKey destination, RedisKey key, long skip, long take, Order order, SortType sortType, RedisValue by, RedisValue[]? get, CommandFlags flags, out ServerEndPoint? server) { + server = null; + var command = destination.IsNull && GetFeatures(key, flags, out server).ReadOnlySort + ? RedisCommand.SORT_RO + : RedisCommand.SORT; + // most common cases; no "get", no "by", no "destination", no "skip", no "take" if (destination.IsNull && skip == 0 && take == -1 && by.IsNull && (get == null || get.Length == 0)) { - switch (order) + return order switch { - case Order.Ascending: - switch (sortType) - { - case SortType.Numeric: return Message.Create(Database, flags, RedisCommand.SORT, key); - case SortType.Alphabetic: return Message.Create(Database, flags, RedisCommand.SORT, key, RedisLiterals.ALPHA); - } - break; - case Order.Descending: - switch (sortType) - { - case SortType.Numeric: return Message.Create(Database, flags, RedisCommand.SORT, key, RedisLiterals.DESC); - case SortType.Alphabetic: return Message.Create(Database, flags, RedisCommand.SORT, key, RedisLiterals.DESC, RedisLiterals.ALPHA); - } - break; - } + Order.Ascending when sortType == SortType.Numeric => Message.Create(Database, flags, command, key), + Order.Ascending when sortType == SortType.Alphabetic => Message.Create(Database, flags, command, key, RedisLiterals.ALPHA), + Order.Descending when sortType == SortType.Numeric => Message.Create(Database, flags, command, key, RedisLiterals.DESC), + Order.Descending when sortType == SortType.Alphabetic => Message.Create(Database, flags, command, key, RedisLiterals.DESC, RedisLiterals.ALPHA), + Order.Ascending or Order.Descending => throw new ArgumentOutOfRangeException(nameof(sortType)), + _ => throw new ArgumentOutOfRangeException(nameof(order)), + }; } // and now: more complicated scenarios... @@ -3578,7 +3575,7 @@ private Message GetSortedSetAddMessage(RedisKey destination, RedisKey key, long values.Add(item); } } - if (destination.IsNull) return Message.Create(Database, flags, RedisCommand.SORT, key, values.ToArray()); + if (destination.IsNull) return Message.Create(Database, flags, command, key, values.ToArray()); // Because we are using STORE, we need to push this to a primary if (Message.GetPrimaryReplicaFlags(flags) == CommandFlags.DemandReplica) diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index a2dcee19e..1a5bd9b98 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -132,6 +132,11 @@ public RedisFeatures(Version version) /// public bool PushIfNotExists => Version >= v2_1_1; + /// + /// Does this support SORT_RO? + /// + internal bool ReadOnlySort => Version >= v7_0_0_rc1; + /// /// Is SCAN (cursor-based scanning) available? /// diff --git a/tests/StackExchange.Redis.Tests/Sets.cs b/tests/StackExchange.Redis.Tests/Sets.cs index d32e12573..436691eff 100644 --- a/tests/StackExchange.Redis.Tests/Sets.cs +++ b/tests/StackExchange.Redis.Tests/Sets.cs @@ -343,4 +343,50 @@ public void SetPopMulti_Nil() var arr = db.SetPop(key, 1); Assert.Empty(arr); } + + [Fact] + public async Task TestSortReadonlyPrimary() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var key = Me(); + await db.KeyDeleteAsync(key); + + var random = new Random(); + var items = Enumerable.Repeat(0, 200).Select(_ => random.Next()).ToList(); + await db.SetAddAsync(key, items.Select(x=>(RedisValue)x).ToArray()); + items.Sort(); + + var result = db.Sort(key).Select(x=>(int)x); + Assert.Equal(items, result); + + result = (await db.SortAsync(key)).Select(x => (int)x); + Assert.Equal(items, result); + } + + [Fact] + public async Task TestSortReadonlyReplica() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + await db.KeyDeleteAsync(key); + + var random = new Random(); + var items = Enumerable.Repeat(0, 200).Select(_ => random.Next()).ToList(); + await db.SetAddAsync(key, items.Select(x=>(RedisValue)x).ToArray()); + + using var readonlyConn = Create(configuration: TestConfig.Current.ReplicaServerAndPort, require: RedisFeatures.v7_0_0_rc1); + var readonlyDb = conn.GetDatabase(); + + items.Sort(); + + var result = readonlyDb.Sort(key).Select(x => (int)x); + Assert.Equal(items, result); + + result = (await readonlyDb.SortAsync(key)).Select(x => (int)x); + Assert.Equal(items, result); + } }