From dbd52f1807cfa513cb9a470232ef05442a02581c Mon Sep 17 00:00:00 2001 From: Steve Lorello <42971704+slorello89@users.noreply.github.com> Date: Tue, 19 Apr 2022 12:45:11 -0400 Subject: [PATCH] ZMPOP and LMPOP (#2094) Implementing [`ZMPOP`](https://redis.io/commands/zmpop/) and [`LMPOP`](https://redis.io/commands/lmpop/) as part of #2055 Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 2 + .../APITypes/ListPopResult.cs | 35 +++++ .../APITypes/SortedSetPopResult.cs | 35 +++++ src/StackExchange.Redis/Enums/RedisCommand.cs | 4 + .../Interfaces/IDatabase.cs | 34 +++++ .../Interfaces/IDatabaseAsync.cs | 34 +++++ .../KeyspaceIsolation/DatabaseWrapper.cs | 9 ++ .../KeyspaceIsolation/WrapperBase.cs | 9 ++ src/StackExchange.Redis/PublicAPI.Shipped.txt | 18 +++ src/StackExchange.Redis/RawResult.cs | 15 ++ src/StackExchange.Redis/RedisDatabase.cs | 92 ++++++++++++ src/StackExchange.Redis/ResultProcessor.cs | 49 +++++++ tests/StackExchange.Redis.Tests/Lists.cs | 126 ++++++++++++++++ tests/StackExchange.Redis.Tests/SortedSets.cs | 137 ++++++++++++++++++ 14 files changed, 599 insertions(+) create mode 100644 src/StackExchange.Redis/APITypes/ListPopResult.cs create mode 100644 src/StackExchange.Redis/APITypes/SortedSetPopResult.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 8cec3ee88..c21d6b6d4 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -23,6 +23,8 @@ - Adds: Support for `GEOSEARCH` with `.GeoSearch()`/`.GeoSearchAsync()` ([#2089 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2089)) - Adds: Support for `GEOSEARCHSTORE` with `.GeoSearchAndStore()`/`.GeoSearchAndStoreAsync()` ([#2089 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2089)) - Adds: Support for `HRANDFIELD` with `.HashRandomField()`/`.HashRandomFieldAsync()`, `.HashRandomFields()`/`.HashRandomFieldsAsync()`, and `.HashRandomFieldsWithValues()`/`.HashRandomFieldsWithValuesAsync()` ([#2090 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2090)) +- Adds: Support for `LMPOP` with `.ListLeftPop()`/`.ListLeftPopAsync()` and `.ListRightPop()`/`.ListRightPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094)) +- 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)) ## 2.5.61 diff --git a/src/StackExchange.Redis/APITypes/ListPopResult.cs b/src/StackExchange.Redis/APITypes/ListPopResult.cs new file mode 100644 index 000000000..149bed68a --- /dev/null +++ b/src/StackExchange.Redis/APITypes/ListPopResult.cs @@ -0,0 +1,35 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// A contiguous portion of a redis list. +/// +public readonly struct ListPopResult +{ + /// + /// A null ListPopResult, indicating no results. + /// + public static ListPopResult Null { get; } = new ListPopResult(RedisKey.Null, Array.Empty()); + + /// + /// Whether this object is null/empty. + /// + public bool IsNull => Key.IsNull && Values == Array.Empty(); + + /// + /// The key of the list that this set of entries came form. + /// + public RedisKey Key { get; } + + /// + /// The values from the list. + /// + public RedisValue[] Values { get; } + + internal ListPopResult(RedisKey key, RedisValue[] values) + { + Key = key; + Values = values; + } +} diff --git a/src/StackExchange.Redis/APITypes/SortedSetPopResult.cs b/src/StackExchange.Redis/APITypes/SortedSetPopResult.cs new file mode 100644 index 000000000..dcdc4c01e --- /dev/null +++ b/src/StackExchange.Redis/APITypes/SortedSetPopResult.cs @@ -0,0 +1,35 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// A contiguous portion of a redis sorted set. +/// +public readonly struct SortedSetPopResult +{ + /// + /// A null SortedSetPopResult, indicating no results. + /// + public static SortedSetPopResult Null { get; } = new SortedSetPopResult(RedisKey.Null, Array.Empty()); + + /// + /// Whether this object is null/empty. + /// + public bool IsNull => Key.IsNull && Entries == Array.Empty(); + + /// + /// The key of the sorted set these entries came form. + /// + public RedisKey Key { get; } + + /// + /// The provided entries of the sorted set. + /// + public SortedSetEntry[] Entries { get; } + + internal SortedSetPopResult(RedisKey key, SortedSetEntry[] entries) + { + Key = key; + Entries = entries; + } +} diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 6ec77ea01..9c8a6aa16 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -90,6 +90,7 @@ internal enum RedisCommand LINSERT, LLEN, LMOVE, + LMPOP, LPOP, LPOS, LPUSH, @@ -212,6 +213,7 @@ internal enum RedisCommand ZINTERCARD, ZINTERSTORE, ZLEXCOUNT, + ZMPOP, ZMSCORE, ZPOPMAX, ZPOPMIN, @@ -284,6 +286,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.INCRBYFLOAT: case RedisCommand.LINSERT: case RedisCommand.LMOVE: + case RedisCommand.LMPOP: case RedisCommand.LPOP: case RedisCommand.LPUSH: case RedisCommand.LPUSHX: @@ -335,6 +338,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.ZDIFFSTORE: case RedisCommand.ZINTERSTORE: case RedisCommand.ZINCRBY: + case RedisCommand.ZMPOP: case RedisCommand.ZPOPMAX: case RedisCommand.ZPOPMIN: case RedisCommand.ZRANGESTORE: diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 16975beb5..780ae0a47 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -922,6 +922,17 @@ public interface IDatabase : IRedis, IDatabaseAsync /// RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// + /// Removes and returns at most elements from the first non-empty list in . + /// Starts on the left side of the list. + /// + /// The keys to look through for elements to pop. + /// The maximum number of elements to pop from the list. + /// The flags to use for this operation. + /// A span of contiguous elements from the list, or if no non-empty lists are found. + /// + ListPopResult ListLeftPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None); + /// /// Scans through the list stored at looking for , returning the 0-based /// index of the first matching element. @@ -1065,6 +1076,17 @@ public interface IDatabase : IRedis, IDatabaseAsync /// RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// + /// Removes and returns at most elements from the first non-empty list in . + /// Starts on the right side of the list. + /// + /// The keys to look through for elements to pop. + /// The maximum number of elements to pop from the list. + /// The flags to use for this operation. + /// A span of contiguous elements from the list, or if no non-empty lists are found. + /// + ListPopResult ListRightPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None); + /// /// Atomically returns and removes the last element (tail) of the list stored at source, and pushes the element at the first element (head) of the list stored at destination. /// @@ -2110,6 +2132,18 @@ IEnumerable SortedSetScan(RedisKey key, /// SortedSetEntry[] SortedSetPop(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); + /// + /// Removes and returns up to entries from the first non-empty sorted set in . + /// Returns if none of the sets exist or contain any elements. + /// + /// The keys to check. + /// The maximum number of records to pop out of the sorted set. + /// The order to sort by when popping items out of the set. + /// The flags to use for the operation. + /// A contiguous collection of sorted set entries with the key they were popped from, or if no non-empty sorted sets are found. + /// + SortedSetPopResult SortedSetPop(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); + /// /// Allow the consumer to mark a pending message as correctly processed. Returns the number of messages acknowledged. /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 846e5ba2f..d9a53ee2d 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -898,6 +898,17 @@ public interface IDatabaseAsync : IRedisAsync /// Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// + /// Removes and returns at most elements from the first non-empty list in . + /// Starts on the left side of the list. + /// + /// The keys to look through for elements to pop. + /// The maximum number of elements to pop from the list. + /// The flags to use for this operation. + /// A span of contiguous elements from the list, or if no non-empty lists are found. + /// + Task ListLeftPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None); + /// /// Scans through the list stored at looking for , returning the 0-based /// index of the first matching element. @@ -1041,6 +1052,17 @@ public interface IDatabaseAsync : IRedisAsync /// Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// + /// Removes and returns at most elements from the first non-empty list in . + /// Starts on the right side of the list. + /// + /// The keys to look through for elements to pop. + /// The maximum number of elements to pop from the list. + /// The flags to use for this operation. + /// A span of contiguous elements from the list, or if no non-empty lists are found. + /// + Task ListRightPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None); + /// /// Atomically returns and removes the last element (tail) of the list stored at source, and pushes the element at the first element (head) of the list stored at destination. /// @@ -2063,6 +2085,18 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// Task SortedSetPopAsync(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); + /// + /// Removes and returns up to entries from the first non-empty sorted set in . + /// Returns if none of the sets exist or contain any elements. + /// + /// The keys to check. + /// The maximum number of records to pop out of the sorted set. + /// The order to sort by when popping items out of the set. + /// The flags to use for the operation. + /// A contiguous collection of sorted set entries with the key they were popped from, or if no non-empty sorted sets are found. + /// + Task SortedSetPopAsync(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); + /// /// Allow the consumer to mark a pending message as correctly processed. Returns the number of messages acknowledged. /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index d08834c08..ad58d37b1 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -227,6 +227,9 @@ public RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.No public RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => Inner.ListLeftPop(ToInner(key), count, flags); + public ListPopResult ListLeftPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => + Inner.ListLeftPop(ToInner(keys), count, flags); + public long ListPosition(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) => Inner.ListPosition(ToInner(key), element, rank, maxLength, flags); @@ -260,6 +263,9 @@ public RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.N public RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => Inner.ListRightPop(ToInner(key), count, flags); + public ListPopResult ListRightPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => + Inner.ListRightPop(ToInner(keys), count, flags); + public RedisValue ListRightPopLeftPush(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) => Inner.ListRightPopLeftPush(ToInner(source), ToInner(destination), flags); @@ -484,6 +490,9 @@ public long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue public SortedSetEntry[] SortedSetPop(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => Inner.SortedSetPop(ToInner(key), count, order, flags); + public SortedSetPopResult SortedSetPop(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetPop(ToInner(keys), count, order, flags); + public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None) => Inner.StreamAcknowledge(ToInner(key), groupName, messageId, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 0ccaed1fa..ec5b44539 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -238,6 +238,9 @@ public Task ListLeftPopAsync(RedisKey key, CommandFlags flags = Comm public Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => Inner.ListLeftPopAsync(ToInner(key), count, flags); + public Task ListLeftPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => + Inner.ListLeftPopAsync(ToInner(keys), count, flags); + public Task ListPositionAsync(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) => Inner.ListPositionAsync(ToInner(key), element, rank, maxLength, flags); @@ -271,6 +274,9 @@ public Task ListRightPopAsync(RedisKey key, CommandFlags flags = Com public Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => Inner.ListRightPopAsync(ToInner(key), count, flags); + public Task ListRightPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => + Inner.ListRightPopAsync(ToInner(keys), count, flags); + public Task ListRightPopLeftPushAsync(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) => Inner.ListRightPopLeftPushAsync(ToInner(source), ToInner(destination), flags); @@ -501,6 +507,9 @@ public IAsyncEnumerable SortedSetScanAsync(RedisKey key, RedisVa public Task SortedSetPopAsync(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => Inner.SortedSetPopAsync(ToInner(key), count, order, flags); + public Task SortedSetPopAsync(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetPopAsync(ToInner(keys), count, order, flags); + public Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None) => Inner.StreamAcknowledgeAsync(ToInner(key), groupName, messageId, flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 8347638de..d98c12330 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -424,6 +424,11 @@ StackExchange.Redis.GeoUnit.Feet = 3 -> StackExchange.Redis.GeoUnit StackExchange.Redis.GeoUnit.Kilometers = 1 -> StackExchange.Redis.GeoUnit StackExchange.Redis.GeoUnit.Meters = 0 -> StackExchange.Redis.GeoUnit StackExchange.Redis.GeoUnit.Miles = 2 -> StackExchange.Redis.GeoUnit +StackExchange.Redis.ListPopResult +StackExchange.Redis.ListPopResult.IsNull.get -> bool +StackExchange.Redis.ListPopResult.Key.get -> StackExchange.Redis.RedisKey +StackExchange.Redis.ListPopResult.ListPopResult() -> void +StackExchange.Redis.ListPopResult.Values.get -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.ListSide StackExchange.Redis.ListSide.Left = 0 -> StackExchange.Redis.ListSide StackExchange.Redis.ListSide.Right = 1 -> StackExchange.Redis.ListSide @@ -568,6 +573,7 @@ StackExchange.Redis.IDatabase.ListInsertBefore(StackExchange.Redis.RedisKey key, StackExchange.Redis.IDatabase.ListMove(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.ListSide sourceSide, StackExchange.Redis.ListSide destinationSide, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.ListLeftPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.ListLeftPop(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.ListLeftPop(StackExchange.Redis.RedisKey[]! keys, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ListPopResult StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags) -> long StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long @@ -578,6 +584,7 @@ StackExchange.Redis.IDatabase.ListRange(StackExchange.Redis.RedisKey key, long s StackExchange.Redis.IDatabase.ListRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, long count = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.ListRightPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.ListRightPop(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.ListRightPop(StackExchange.Redis.RedisKey[]! keys, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ListPopResult StackExchange.Redis.IDatabase.ListRightPopLeftPush(StackExchange.Redis.RedisKey source, StackExchange.Redis.RedisKey destination, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.ListRightPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.ListRightPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags) -> long @@ -630,6 +637,7 @@ StackExchange.Redis.IDatabase.SortedSetLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.IDatabase.SortedSetLengthByValue(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]! StackExchange.Redis.IDatabase.SortedSetPop(StackExchange.Redis.RedisKey key, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry? +StackExchange.Redis.IDatabase.SortedSetPop(StackExchange.Redis.RedisKey[]! keys, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetPopResult StackExchange.Redis.IDatabase.SortedSetRandomMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.SortedSetRandomMembers(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.SortedSetRandomMembersWithScores(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]! @@ -783,6 +791,7 @@ StackExchange.Redis.IDatabaseAsync.ListInsertBeforeAsync(StackExchange.Redis.Red StackExchange.Redis.IDatabaseAsync.ListMoveAsync(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.ListSide sourceSide, StackExchange.Redis.ListSide destinationSide, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListLeftPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListLeftPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListLeftPopAsync(StackExchange.Redis.RedisKey[]! keys, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -793,6 +802,7 @@ StackExchange.Redis.IDatabaseAsync.ListRangeAsync(StackExchange.Redis.RedisKey k StackExchange.Redis.IDatabaseAsync.ListRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, long count = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListRightPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListRightPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListRightPopAsync(StackExchange.Redis.RedisKey[]! keys, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListRightPopLeftPushAsync(StackExchange.Redis.RedisKey source, StackExchange.Redis.RedisKey destination, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListRightPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListRightPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! @@ -844,6 +854,7 @@ StackExchange.Redis.IDatabaseAsync.SortedSetLengthAsync(StackExchange.Redis.Redi StackExchange.Redis.IDatabaseAsync.SortedSetLengthByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey[]! keys, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetRandomMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetRandomMembersAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetRandomMembersWithScoresAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -1390,6 +1401,11 @@ StackExchange.Redis.SortedSetOrder StackExchange.Redis.SortedSetOrder.ByLex = 2 -> StackExchange.Redis.SortedSetOrder StackExchange.Redis.SortedSetOrder.ByRank = 0 -> StackExchange.Redis.SortedSetOrder StackExchange.Redis.SortedSetOrder.ByScore = 1 -> StackExchange.Redis.SortedSetOrder +StackExchange.Redis.SortedSetPopResult +StackExchange.Redis.SortedSetPopResult.Entries.get -> StackExchange.Redis.SortedSetEntry[]! +StackExchange.Redis.SortedSetPopResult.IsNull.get -> bool +StackExchange.Redis.SortedSetPopResult.Key.get -> StackExchange.Redis.RedisKey +StackExchange.Redis.SortedSetPopResult.SortedSetPopResult() -> void StackExchange.Redis.SortType StackExchange.Redis.SortType.Alphabetic = 1 -> StackExchange.Redis.SortType StackExchange.Redis.SortType.Numeric = 0 -> StackExchange.Redis.SortType @@ -1555,6 +1571,7 @@ static StackExchange.Redis.HashEntry.operator ==(StackExchange.Redis.HashEntry x static StackExchange.Redis.KeyspaceIsolation.DatabaseExtensions.WithKeyPrefix(this StackExchange.Redis.IDatabase! database, StackExchange.Redis.RedisKey keyPrefix) -> StackExchange.Redis.IDatabase! static StackExchange.Redis.Lease.Create(int length, bool clear = true) -> StackExchange.Redis.Lease! static StackExchange.Redis.Lease.Empty.get -> StackExchange.Redis.Lease! +static StackExchange.Redis.ListPopResult.Null.get -> StackExchange.Redis.ListPopResult static StackExchange.Redis.LuaScript.GetCachedScriptCount() -> int static StackExchange.Redis.LuaScript.Prepare(string! script) -> StackExchange.Redis.LuaScript! static StackExchange.Redis.LuaScript.PurgeCache() -> void @@ -1672,6 +1689,7 @@ static StackExchange.Redis.SortedSetEntry.implicit operator StackExchange.Redis. static StackExchange.Redis.SortedSetEntry.implicit operator System.Collections.Generic.KeyValuePair(StackExchange.Redis.SortedSetEntry value) -> System.Collections.Generic.KeyValuePair static StackExchange.Redis.SortedSetEntry.operator !=(StackExchange.Redis.SortedSetEntry x, StackExchange.Redis.SortedSetEntry y) -> bool static StackExchange.Redis.SortedSetEntry.operator ==(StackExchange.Redis.SortedSetEntry x, StackExchange.Redis.SortedSetEntry y) -> bool +static StackExchange.Redis.SortedSetPopResult.Null.get -> StackExchange.Redis.SortedSetPopResult static StackExchange.Redis.StreamAutoClaimIdsOnlyResult.Null.get -> StackExchange.Redis.StreamAutoClaimIdsOnlyResult static StackExchange.Redis.StreamAutoClaimResult.Null.get -> StackExchange.Redis.StreamAutoClaimResult static StackExchange.Redis.StreamEntry.Null.get -> StackExchange.Redis.StreamEntry diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index 919054411..11ca450af 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -284,6 +284,21 @@ internal bool GetBoolean() return AsGeoPosition(root.GetItems()); } + internal SortedSetEntry[]? GetItemsAsSortedSetEntryArray() => this.ToArray((in RawResult item) => AsSortedSetEntry(item.GetItems())); + + private static SortedSetEntry AsSortedSetEntry(in Sequence elements) + { + if (elements.IsSingleSegment) + { + var span = elements.FirstSpan; + return new SortedSetEntry(span[0].AsRedisValue(), span[1].TryGetDouble(out double val) ? val : double.NaN); + } + else + { + return new SortedSetEntry(elements[0].AsRedisValue(), elements[1].TryGetDouble(out double val) ? val : double.NaN); + } + } + private static GeoPosition AsGeoPosition(in Sequence coords) { double longitude, latitude; diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 7d05e46da..4f302f223 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1107,6 +1107,12 @@ public RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = C return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } + public ListPopResult ListLeftPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) + { + var msg = GetListMultiPopMessage(keys, RedisLiterals.LEFT, count, flags); + return ExecuteSync(msg, ResultProcessor.ListPopResult, defaultValue: ListPopResult.Null); + } + public long ListPosition(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) { var msg = CreateListPositionMessage(Database, flags, key, element, rank, maxLength); @@ -1131,6 +1137,12 @@ public Task ListLeftPopAsync(RedisKey key, long count, CommandFlag return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } + public Task ListLeftPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) + { + var msg = GetListMultiPopMessage(keys, RedisLiterals.LEFT, count, flags); + return ExecuteAsync(msg, ResultProcessor.ListPopResult, defaultValue: ListPopResult.Null); + } + public Task ListPositionAsync(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) { var msg = CreateListPositionMessage(Database, flags, key, element, rank, maxLength); @@ -1249,6 +1261,12 @@ public RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } + public ListPopResult ListRightPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) + { + var msg = GetListMultiPopMessage(keys, RedisLiterals.RIGHT, count, flags); + return ExecuteSync(msg, ResultProcessor.ListPopResult, defaultValue: ListPopResult.Null); + } + public Task ListRightPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.RPOP, key); @@ -1261,6 +1279,12 @@ public Task ListRightPopAsync(RedisKey key, long count, CommandFla return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } + public Task ListRightPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) + { + var msg = GetListMultiPopMessage(keys, RedisLiterals.RIGHT, count, flags); + return ExecuteAsync(msg, ResultProcessor.ListPopResult, defaultValue: ListPopResult.Null); + } + public RedisValue ListRightPopLeftPush(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.RPOPLPUSH, source, destination); @@ -2142,6 +2166,12 @@ public SortedSetEntry[] SortedSetPop(RedisKey key, long count, Order order = Ord return ExecuteSync(msg, ResultProcessor.SortedSetWithScores, defaultValue: Array.Empty()); } + public SortedSetPopResult SortedSetPop(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) + { + var msg = GetSortedSetMultiPopMessage(keys, order, count, flags); + return ExecuteSync(msg, ResultProcessor.SortedSetPopResult, defaultValue: SortedSetPopResult.Null); + } + public Task SortedSetPopAsync(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) { if (count == 0) return CompletedTask.FromDefault(Array.Empty(), asyncState); @@ -2151,6 +2181,12 @@ public Task SortedSetPopAsync(RedisKey key, long count, Order return ExecuteAsync(msg, ResultProcessor.SortedSetWithScores, defaultValue: Array.Empty()); } + public Task SortedSetPopAsync(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) + { + var msg = GetSortedSetMultiPopMessage(keys, order, count, flags); + return ExecuteAsync(msg, ResultProcessor.SortedSetPopResult, defaultValue: SortedSetPopResult.Null); + } + public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None) { var msg = GetStreamAcknowledgeMessage(key, groupName, messageId, flags); @@ -3154,6 +3190,62 @@ private Message GetExpiryMessage(in RedisKey key, }; } + private Message GetListMultiPopMessage(RedisKey[] keys, RedisValue side, long count, CommandFlags flags) + { + if (keys is null || keys.Length == 0) + { + throw new ArgumentOutOfRangeException(nameof(keys), "keys must have a size of at least 1"); + } + + var slot = multiplexer.ServerSelectionStrategy.HashSlot(keys[0]); + + var args = new RedisValue[2 + keys.Length + (count == 1 ? 0 : 2)]; + var i = 0; + args[i++] = keys.Length; + foreach (var key in keys) + { + args[i++] = key.AsRedisValue(); + } + + args[i++] = side; + + if (count != 1) + { + args[i++] = RedisLiterals.COUNT; + args[i++] = count; + } + + return Message.CreateInSlot(Database, slot, flags, RedisCommand.LMPOP, args); + } + + private Message GetSortedSetMultiPopMessage(RedisKey[] keys, Order order, long count, CommandFlags flags) + { + if (keys is null || keys.Length == 0) + { + throw new ArgumentOutOfRangeException(nameof(keys), "keys must have a size of at least 1"); + } + + var slot = multiplexer.ServerSelectionStrategy.HashSlot(keys[0]); + + var args = new RedisValue[2 + keys.Length + (count == 1 ? 0 : 2)]; + var i = 0; + args[i++] = keys.Length; + foreach (var key in keys) + { + args[i++] = key.AsRedisValue(); + } + + args[i++] = order == Order.Ascending ? RedisLiterals.MIN : RedisLiterals.MAX; + + if (count != 1) + { + args[i++] = RedisLiterals.COUNT; + args[i++] = count; + } + + return Message.CreateInSlot(Database, slot, flags, RedisCommand.ZMPOP, args); + } + private Message? GetHashSetMessage(RedisKey key, HashEntry[] hashFields, CommandFlags flags) { if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 9ae426e7d..9b3fce521 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -116,6 +116,12 @@ public static readonly SortedSetEntryProcessor public static readonly SortedSetEntryArrayProcessor SortedSetWithScores = new SortedSetEntryArrayProcessor(); + public static readonly SortedSetPopResultProcessor + SortedSetPopResult = new SortedSetPopResultProcessor(); + + public static readonly ListPopResultProcessor + ListPopResult = new ListPopResultProcessor(); + public static readonly SingleStreamProcessor SingleStream = new SingleStreamProcessor(); @@ -570,6 +576,49 @@ protected override SortedSetEntry Parse(in RawResult first, in RawResult second) new SortedSetEntry(first.AsRedisValue(), second.TryGetDouble(out double val) ? val : double.NaN); } + internal sealed class SortedSetPopResultProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Type == ResultType.MultiBulk) + { + if (result.IsNull) + { + SetResult(message, Redis.SortedSetPopResult.Null); + return true; + } + + var arr = result.GetItems(); + SetResult(message, new SortedSetPopResult(arr[0].AsRedisKey(), arr[1].GetItemsAsSortedSetEntryArray()!)); + return true; + } + + return false; + } + } + + internal sealed class ListPopResultProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Type == ResultType.MultiBulk) + { + if (result.IsNull) + { + SetResult(message, Redis.ListPopResult.Null); + return true; + } + + var arr = result.GetItems(); + SetResult(message, new ListPopResult(arr[0].AsRedisKey(), arr[1].GetItemsAsValues()!)); + return true; + } + + return false; + } + } + + internal sealed class HashEntryArrayProcessor : ValuePairInterleavedProcessorBase { protected override HashEntry Parse(in RawResult first, in RawResult second) => diff --git a/tests/StackExchange.Redis.Tests/Lists.cs b/tests/StackExchange.Redis.Tests/Lists.cs index 8263d5971..119df434e 100644 --- a/tests/StackExchange.Redis.Tests/Lists.cs +++ b/tests/StackExchange.Redis.Tests/Lists.cs @@ -831,4 +831,130 @@ public void ListPositionFireAndForget() Assert.Equal(-1, res); } + + [Fact] + public async Task ListMultiPopSingleKeyAsync() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + await db.KeyDeleteAsync(key); + + db.ListLeftPush(key, "yankees"); + db.ListLeftPush(key, "blue jays"); + db.ListLeftPush(key, "orioles"); + db.ListLeftPush(key, "red sox"); + db.ListLeftPush(key, "rays"); + + var res = await db.ListLeftPopAsync(new RedisKey[] { key }, 1); + + Assert.False(res.IsNull); + Assert.Single(res.Values); + Assert.Equal("rays", res.Values[0]); + + res = await db.ListRightPopAsync(new RedisKey[] { key }, 2); + + Assert.False(res.IsNull); + Assert.Equal(2, res.Values.Length); + Assert.Equal("yankees", res.Values[0]); + Assert.Equal("blue jays", res.Values[1]); + } + + [Fact] + public async Task ListMultiPopMultipleKeysAsync() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + await db.KeyDeleteAsync(key); + + db.ListLeftPush(key, "yankees"); + db.ListLeftPush(key, "blue jays"); + db.ListLeftPush(key, "orioles"); + db.ListLeftPush(key, "red sox"); + db.ListLeftPush(key, "rays"); + + var res = await db.ListLeftPopAsync(new RedisKey[] { "empty-key", key, "also-empty" }, 2); + + Assert.False(res.IsNull); + Assert.Equal(2, res.Values.Length); + Assert.Equal("rays", res.Values[0]); + Assert.Equal("red sox", res.Values[1]); + + res = await db.ListRightPopAsync(new RedisKey[] { "empty-key", key, "also-empty" }, 1); + + Assert.False(res.IsNull); + Assert.Single(res.Values); + Assert.Equal("yankees", res.Values[0]); + } + + [Fact] + public void ListMultiPopSingleKey() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key); + + db.ListLeftPush(key, "yankees"); + db.ListLeftPush(key, "blue jays"); + db.ListLeftPush(key, "orioles"); + db.ListLeftPush(key, "red sox"); + db.ListLeftPush(key, "rays"); + + var res = db.ListLeftPop(new RedisKey[] { key }, 1); + + Assert.False(res.IsNull); + Assert.Single(res.Values); + Assert.Equal("rays", res.Values[0]); + + res = db.ListRightPop(new RedisKey[] { key }, 2); + + Assert.False(res.IsNull); + Assert.Equal(2, res.Values.Length); + Assert.Equal("yankees", res.Values[0]); + Assert.Equal("blue jays", res.Values[1]); + } + + [Fact] + public async Task ListMultiPopZeroCount() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key); + + var exception = await Assert.ThrowsAsync(() => db.ListLeftPopAsync(new RedisKey[] { key }, 0)); + Assert.Contains("ERR count should be greater than 0", exception.Message); + } + + [Fact] + public async Task ListMultiPopEmpty() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key); + + var res = await db.ListLeftPopAsync(new RedisKey[] { key }, 1); + Assert.True(res.IsNull); + } + + [Fact] + public void ListMultiPopEmptyKeys() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var exception = Assert.Throws(() => db.ListRightPop(Array.Empty(), 5)); + Assert.Contains("keys must have a size of at least 1", exception.Message); + + exception = Assert.Throws(() => db.ListLeftPop(Array.Empty(), 5)); + Assert.Contains("keys must have a size of at least 1", exception.Message); + } } diff --git a/tests/StackExchange.Redis.Tests/SortedSets.cs b/tests/StackExchange.Redis.Tests/SortedSets.cs index b0eca7a87..94c21a996 100644 --- a/tests/StackExchange.Redis.Tests/SortedSets.cs +++ b/tests/StackExchange.Redis.Tests/SortedSets.cs @@ -966,6 +966,143 @@ public void SortedSetRangeStoreFailExclude() Assert.Equal("exclude", exception.ParamName); } + [Fact] + public void SortedSetMultiPopSingleKey() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key); + + db.SortedSetAdd(key, new SortedSetEntry[] { + new SortedSetEntry("rays", 100), + new SortedSetEntry("yankees", 92), + new SortedSetEntry("red sox", 92), + new SortedSetEntry("blue jays", 91), + new SortedSetEntry("orioles", 52), + }); + + var highest = db.SortedSetPop(new RedisKey[] { key }, 1, order: Order.Descending); + Assert.False(highest.IsNull); + Assert.Equal(key, highest.Key); + var entry = Assert.Single(highest.Entries); + Assert.Equal("rays", entry.Element); + Assert.Equal(100, entry.Score); + + var bottom2 = db.SortedSetPop(new RedisKey[] { key }, 2); + Assert.False(bottom2.IsNull); + Assert.Equal(key, bottom2.Key); + Assert.Equal(2, bottom2.Entries.Length); + Assert.Equal("orioles", bottom2.Entries[0].Element); + Assert.Equal(52, bottom2.Entries[0].Score); + Assert.Equal("blue jays", bottom2.Entries[1].Element); + Assert.Equal(91, bottom2.Entries[1].Score); + } + + [Fact] + public void SortedSetMultiPopMultiKey() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key); + + db.SortedSetAdd(key, new SortedSetEntry[] { + new SortedSetEntry("rays", 100), + new SortedSetEntry("yankees", 92), + new SortedSetEntry("red sox", 92), + new SortedSetEntry("blue jays", 91), + new SortedSetEntry("orioles", 52), + }); + + var highest = db.SortedSetPop(new RedisKey[] { "not a real key", key, "yet another not a real key" }, 1, order: Order.Descending); + Assert.False(highest.IsNull); + Assert.Equal(key, highest.Key); + var entry = Assert.Single(highest.Entries); + Assert.Equal("rays", entry.Element); + Assert.Equal(100, entry.Score); + + var bottom2 = db.SortedSetPop(new RedisKey[] { "not a real key", key, "yet another not a real key" }, 2); + Assert.False(bottom2.IsNull); + Assert.Equal(key, bottom2.Key); + Assert.Equal(2, bottom2.Entries.Length); + Assert.Equal("orioles", bottom2.Entries[0].Element); + Assert.Equal(52, bottom2.Entries[0].Score); + Assert.Equal("blue jays", bottom2.Entries[1].Element); + Assert.Equal(91, bottom2.Entries[1].Score); + } + + [Fact] + public void SortedSetMultiPopNoSet() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key); + var res = db.SortedSetPop(new RedisKey[] { key }, 1); + Assert.True(res.IsNull); + } + + [Fact] + public void SortedSetMultiPopCount0() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key); + var exception = Assert.Throws(() => db.SortedSetPop(new RedisKey[] { key }, 0)); + Assert.Contains("ERR count should be greater than 0", exception.Message); + } + + [Fact] + public async Task SortedSetMultiPopAsync() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key); + + db.SortedSetAdd(key, new SortedSetEntry[] { + new SortedSetEntry("rays", 100), + new SortedSetEntry("yankees", 92), + new SortedSetEntry("red sox", 92), + new SortedSetEntry("blue jays", 91), + new SortedSetEntry("orioles", 52), + }); + + var highest = await db.SortedSetPopAsync( + new RedisKey[] { "not a real key", key, "yet another not a real key" }, 1, order: Order.Descending); + Assert.False(highest.IsNull); + Assert.Equal(key, highest.Key); + var entry = Assert.Single(highest.Entries); + Assert.Equal("rays", entry.Element); + Assert.Equal(100, entry.Score); + + var bottom2 = await db.SortedSetPopAsync(new RedisKey[] { "not a real key", key, "yet another not a real key" }, 2); + Assert.False(bottom2.IsNull); + Assert.Equal(key, bottom2.Key); + Assert.Equal(2, bottom2.Entries.Length); + Assert.Equal("orioles", bottom2.Entries[0].Element); + Assert.Equal(52, bottom2.Entries[0].Score); + Assert.Equal("blue jays", bottom2.Entries[1].Element); + Assert.Equal(91, bottom2.Entries[1].Score); + } + + [Fact] + public void SortedSetMultiPopEmptyKeys() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var exception = Assert.Throws(() => db.SortedSetPop(Array.Empty(), 5)); + Assert.Contains("keys must have a size of at least 1", exception.Message); + } + [Fact] public void SortedSetRangeStoreFailForReplica() {