diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7c5e007b..ca1c09eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,7 @@ Here's how to get started with your code contribution: 3. Write your tests 4. Use the `docker run -p 6379:6379 -it redis/redis-stack-server:edge` as your local environment for running the functional tests. You can also use Development Container as described below. -5. Run dotnet format to make sure your code is formatted +5. Run `dotnet format` to make sure your code is formatted 6. Make sure your tests pass using `dotnet test` 7. Open a pull request @@ -121,6 +121,7 @@ e.g. : ```bash dotnet test --environment "REDIS_CLUSTER=127.0.0.1:16379" --environment "NUM_REDIS_CLUSTER_NODES=6" ``` + ## How to Report a Bug ### Security Vulnerabilities @@ -145,7 +146,7 @@ issue, so if you're unsure, just email [us](mailto:oss@redis.com). When filing an issue, make sure to answer these five questions: 1. What version of NRedisStack are you using? -2. What version of redis are you using? +2. What version of Redis are you using? 3. What did you do? 4. What did you expect to see? 5. What did you see instead? diff --git a/README.md b/README.md index bc1f65c4..0c96178e 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,17 @@ This project builds on [StackExchange.Redis](https://github.com/StackExchange/St The complete documentation for Redis module commands can be found at the [Redis commands website](https://redis.io/commands/). ### Redis OSS commands + You can use Redis OSS commands in the same way as you use them in [StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis). ### Stack commands + Each module has a command class with its own commands. + The supported modules are [Search](https://redis.io/commands/?group=search), [JSON](https://redis.io/commands/?group=json), [TimeSeries](https://redis.io/commands/?group=timeseries), [Bloom Filter](https://redis.io/commands/?group=bf), [Cuckoo Filter](https://redis.io/commands/?group=cf), [T-Digest](https://redis.io/commands/?group=tdigest), [Count-min Sketch](https://redis.io/commands/?group=cms), and [Top-K](https://redis.io/commands/?group=topk). **Note:** RedisGraph support has been deprecated starting from Redis Stack version 7.2. For more information, please refer to [this blog post](https://redis.com/blog/redisgraph-eol/). - # Usage ## 💻 Installation diff --git a/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs b/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs index 9677824d..6c71a8a2 100644 --- a/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs +++ b/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs @@ -1,6 +1,8 @@ using NRedisStack.RedisStackCommands; using NRedisStack.Core.Literals; using NRedisStack.Core; +using NRedisStack.Core.DataTypes; +using StackExchange.Redis; namespace NRedisStack { @@ -18,5 +20,56 @@ public static SerializedCommand ClientSetInfo(SetInfoAttr attr, string value) return new SerializedCommand(RedisCoreCommands.CLIENT, RedisCoreCommands.SETINFO, attrValue, value); } + + public static SerializedCommand BzmPop(double timeout, RedisKey[] keys, MinMaxModifier minMaxModifier, long? count) + { + if (keys.Length == 0) + { + throw new ArgumentException("At least one key must be provided."); + } + + List args = new List(); + + args.Add(timeout); + args.Add(keys.Length); + args.AddRange(keys.Cast()); + args.Add(minMaxModifier == MinMaxModifier.Min ? CoreArgs.MIN : CoreArgs.MAX); + + if (count != null) + { + args.Add(CoreArgs.COUNT); + args.Add(count); + } + + return new SerializedCommand(RedisCoreCommands.BZMPOP, args); + } + + public static SerializedCommand BzPopMin(RedisKey[] keys, double timeout) + { + if (keys.Length == 0) + { + throw new ArgumentException("At least one key must be provided."); + } + + List args = new List(); + args.AddRange(keys.Cast()); + args.Add(timeout); + + return new SerializedCommand(RedisCoreCommands.BZPOPMIN, args); + } + + public static SerializedCommand BzPopMax(RedisKey[] keys, double timeout) + { + if (keys.Length == 0) + { + throw new ArgumentException("At least one key must be provided."); + } + + List args = new List(); + args.AddRange(keys.Cast()); + args.Add(timeout); + + return new SerializedCommand(RedisCoreCommands.BZPOPMAX, args); + } } } diff --git a/src/NRedisStack/CoreCommands/CoreCommands.cs b/src/NRedisStack/CoreCommands/CoreCommands.cs index 4c8917bb..6b69074c 100644 --- a/src/NRedisStack/CoreCommands/CoreCommands.cs +++ b/src/NRedisStack/CoreCommands/CoreCommands.cs @@ -1,5 +1,7 @@ using NRedisStack.Core; +using NRedisStack.Core.DataTypes; using StackExchange.Redis; + namespace NRedisStack { @@ -19,5 +21,159 @@ public static bool ClientSetInfo(this IDatabase db, SetInfoAttr attr, string val return false; return db.Execute(CoreCommandBuilder.ClientSetInfo(attr, value)).OKtoBoolean(); } + + /// + /// The BZMPOP command. + ///

+ /// Removes and returns up to entries from the first non-empty sorted set in + /// . If none of the sets contain elements, the call blocks on the server until elements + /// become available, or the given expires. A of 0 + /// means to wait indefinitely server-side. Returns null if the server timeout expires. + ///

+ /// When using this, pay attention to the timeout configured in the client, on the + /// , which by default can be too small: + /// + /// ConfigurationOptions configurationOptions = new ConfigurationOptions(); + /// configurationOptions.SyncTimeout = 120000; // set a meaningful value here + /// configurationOptions.EndPoints.Add("localhost"); + /// ConnectionMultiplexer redis = ConnectionMultiplexer.Connect(configurationOptions); + /// + /// If the connection multiplexer timeout expires in the client, a StackExchange.Redis.RedisTimeoutException + /// is thrown. + ///

+ /// This is an extension method added to the class, for convenience. + ///

+ /// The class where this extension method is applied. + /// Server-side timeout for the wait. A value of 0 means to wait indefinitely. + /// The keys to check. + /// Specify from which end of the sorted set to pop values. If set to MinMaxModifier.Min + /// then the minimum elements will be popped, otherwise the maximum values. + /// The maximum number of records to pop out. If set to null then the server default + /// will be used. + /// A collection of sorted set entries paired with their scores, together with the key they were popped + /// from, or null if the server timeout expires. + /// + public static Tuple>? BzmPop(this IDatabase db, double timeout, RedisKey[] keys, MinMaxModifier minMaxModifier, long? count = null) + { + var command = CoreCommandBuilder.BzmPop(timeout, keys, minMaxModifier, count); + return db.Execute(command).ToSortedSetPopResults(); + } + + /// + /// Syntactic sugar for + /// , + /// where only one key is used. + /// + /// The class where this extension method is applied. + /// Server-side timeout for the wait. A value of 0 means to wait indefinitely. + /// The key to check. + /// Specify from which end of the sorted set to pop values. If set to MinMaxModifier.Min + /// then the minimum elements will be popped, otherwise the maximum values. + /// The maximum number of records to pop out. If set to null then the server default + /// will be used. + /// A collection of sorted set entries paired with their scores, together with the key they were popped + /// from, or null if the server timeout expires. + /// + public static Tuple>? BzmPop(this IDatabase db, double timeout, RedisKey key, MinMaxModifier minMaxModifier, long? count = null) + { + return BzmPop(db, timeout, new[] { key }, minMaxModifier, count); + } + + /// + /// The BZPOPMIN command. + ///

+ /// Removes and returns the entry with the smallest score from the first non-empty sorted set in + /// . If none of the sets contain elements, the call blocks on the server until elements + /// become available, or the given expires. A of 0 + /// means to wait indefinitely server-side. Returns null if the server timeout expires. + ///

+ /// When using this, pay attention to the timeout configured in the client, on the + /// , which by default can be too small: + /// + /// ConfigurationOptions configurationOptions = new ConfigurationOptions(); + /// configurationOptions.SyncTimeout = 120000; // set a meaningful value here + /// configurationOptions.EndPoints.Add("localhost"); + /// ConnectionMultiplexer redis = ConnectionMultiplexer.Connect(configurationOptions); + /// + /// If the connection multiplexer timeout expires in the client, a StackExchange.Redis.RedisTimeoutException + /// is thrown. + ///

+ /// This is an extension method added to the class, for convenience. + ///

+ /// The class where this extension method is applied. + /// The keys to check. + /// Server-side timeout for the wait. A value of 0 means to wait indefinitely. + /// A sorted set entry paired with its score, together with the key it was popped from, or null + /// if the server timeout expires. + /// + public static Tuple? BzPopMin(this IDatabase db, RedisKey[] keys, double timeout) + { + var command = CoreCommandBuilder.BzPopMin(keys, timeout); + return db.Execute(command).ToSortedSetPopResult(); + } + + /// + /// Syntactic sugar for , + /// where only one key is used. + /// + /// The class where this extension method is applied. + /// The key to check. + /// Server-side timeout for the wait. A value of 0 means to wait indefinitely. + /// A sorted set entry paired with its score, together with the key it was popped from, or null + /// if the server timeout expires. + /// + public static Tuple? BzPopMin(this IDatabase db, RedisKey key, double timeout) + { + return BzPopMin(db, new[] { key }, timeout); + } + + + /// + /// The BZPOPMAX command. + ///

+ /// Removes and returns the entry with the highest score from the first non-empty sorted set in + /// . If none of the sets contain elements, the call blocks on the server until elements + /// become available, or the given expires. A of 0 + /// means to wait indefinitely server-side. Returns null if the server timeout expires. + ///

+ /// When using this, pay attention to the timeout configured in the client, on the + /// , which by default can be too small: + /// + /// ConfigurationOptions configurationOptions = new ConfigurationOptions(); + /// configurationOptions.SyncTimeout = 120000; // set a meaningful value here + /// configurationOptions.EndPoints.Add("localhost"); + /// ConnectionMultiplexer redis = ConnectionMultiplexer.Connect(configurationOptions); + /// + /// If the connection multiplexer timeout expires in the client, a StackExchange.Redis.RedisTimeoutException + /// is thrown. + ///

+ /// This is an extension method added to the class, for convenience. + ///

+ /// The class where this extension method is applied. + /// The keys to check. + /// Server-side timeout for the wait. A value of 0 means to wait indefinitely. + /// A sorted set entry paired with its score, together with the key it was popped from, or null + /// if the server timeout expires. + /// + public static Tuple? BzPopMax(this IDatabase db, RedisKey[] keys, double timeout) + { + var command = CoreCommandBuilder.BzPopMax(keys, timeout); + return db.Execute(command).ToSortedSetPopResult(); + } + + /// + /// Syntactic sugar for , + /// where only one key is used. + /// + /// The class where this extension method is applied. + /// The key to check. + /// Server-side timeout for the wait. A value of 0 means to wait indefinitely. + /// A sorted set entry paired with its score, together with the key it was popped from, or null + /// if the server timeout expires. + /// + public static Tuple? BzPopMax(this IDatabase db, RedisKey key, double timeout) + { + return BzPopMax(db, new[] { key }, timeout); + } } } diff --git a/src/NRedisStack/CoreCommands/DataTypes/MinMaxModifier.cs b/src/NRedisStack/CoreCommands/DataTypes/MinMaxModifier.cs new file mode 100644 index 00000000..c38e2231 --- /dev/null +++ b/src/NRedisStack/CoreCommands/DataTypes/MinMaxModifier.cs @@ -0,0 +1,35 @@ +using StackExchange.Redis; + +namespace NRedisStack.Core.DataTypes; + +/// +/// Modifier that can be used for sorted set commands, where a MIN/MAX argument is expected by the Redis server. +/// +public enum MinMaxModifier +{ + /// + /// Maps to the MIN argument on the Redis server. + /// + Min, + + /// + /// Maps to the MAX argument on the Redis server. + /// + Max +} + +/// +/// Conversion methods from/to other common data types. +/// +public static class MinMaxModifierExtensions +{ + /// + /// Convert from to . + /// + public static MinMaxModifier ToMinMax(this Order order) => order switch + { + Order.Ascending => MinMaxModifier.Min, + Order.Descending => MinMaxModifier.Max, + _ => throw new ArgumentOutOfRangeException(nameof(order)) + }; +} \ No newline at end of file diff --git a/src/NRedisStack/CoreCommands/DataTypes/RedisValueWithScore.cs b/src/NRedisStack/CoreCommands/DataTypes/RedisValueWithScore.cs new file mode 100644 index 00000000..6587db45 --- /dev/null +++ b/src/NRedisStack/CoreCommands/DataTypes/RedisValueWithScore.cs @@ -0,0 +1,31 @@ +using StackExchange.Redis; + +namespace NRedisStack.Core.DataTypes; + +/// +/// Holds a with an associated score. +/// Used when working with sorted sets. +/// +public struct RedisValueWithScore +{ + /// + /// Pair a with a numeric score. + /// + public RedisValueWithScore(RedisValue value, double score) + { + Value = value; + Score = score; + } + + /// + /// The value of an item stored in a sorted set. For example, in the Redis command + /// ZADD my-set 5.1 my-value, the value is my-value. + /// + public RedisValue Value { get; } + + /// + /// The score of an item stored in a sorted set. For example, in the Redis command + /// ZADD my-set 5.1 my-value, the score is 5.1. + /// + public double Score { get; } +} \ No newline at end of file diff --git a/src/NRedisStack/CoreCommands/Literals/CommandArgs.cs b/src/NRedisStack/CoreCommands/Literals/CommandArgs.cs index 8d9aaf4f..4e6e4242 100644 --- a/src/NRedisStack/CoreCommands/Literals/CommandArgs.cs +++ b/src/NRedisStack/CoreCommands/Literals/CommandArgs.cs @@ -1,8 +1,11 @@ namespace NRedisStack.Core.Literals { - internal class CoreArgs + internal static class CoreArgs { + public const string COUNT = "COUNT"; public const string lib_name = "LIB-NAME"; public const string lib_ver = "LIB-VER"; + public const string MAX = "MAX"; + public const string MIN = "MIN"; } } diff --git a/src/NRedisStack/CoreCommands/Literals/Commands.cs b/src/NRedisStack/CoreCommands/Literals/Commands.cs index aaebaef6..e44c82ed 100644 --- a/src/NRedisStack/CoreCommands/Literals/Commands.cs +++ b/src/NRedisStack/CoreCommands/Literals/Commands.cs @@ -3,8 +3,11 @@ namespace NRedisStack.Core.Literals /// /// Redis Core command literals /// - internal class RedisCoreCommands + internal static class RedisCoreCommands { + public const string BZMPOP = "BZMPOP"; + public const string BZPOPMAX = "BZPOPMAX"; + public const string BZPOPMIN = "BZPOPMIN"; public const string CLIENT = "CLIENT"; public const string SETINFO = "SETINFO"; } diff --git a/src/NRedisStack/NRedisStack.csproj b/src/NRedisStack/NRedisStack.csproj index df5fbeb8..ad0fdb47 100644 --- a/src/NRedisStack/NRedisStack.csproj +++ b/src/NRedisStack/NRedisStack.csproj @@ -3,15 +3,16 @@ enable netstandard2.0;net6.0;net7.0;net8.0 + true latest enable Redis Open Source Redis OSS .Net Client for Redis Stack README.md - 0.11.0 - 0.11.0 - 0.11.0 + 0.11.1 + 0.11.1 + 0.11.1 diff --git a/src/NRedisStack/ResponseParser.cs b/src/NRedisStack/ResponseParser.cs index 81f22eb3..b3a7c315 100644 --- a/src/NRedisStack/ResponseParser.cs +++ b/src/NRedisStack/ResponseParser.cs @@ -3,6 +3,7 @@ using NRedisStack.Extensions; using StackExchange.Redis; using NRedisStack.Bloom.DataTypes; +using NRedisStack.Core.DataTypes; using NRedisStack.CuckooFilter.DataTypes; using NRedisStack.CountMinSketch.DataTypes; using NRedisStack.TopK.DataTypes; @@ -84,6 +85,16 @@ public static TimeStamp ToTimeStamp(this RedisResult result) return new TimeStamp((long)result); } + public static RedisKey ToRedisKey(this RedisResult result) + { + return new RedisKey(result.ToString()); + } + + public static RedisValue ToRedisValue(this RedisResult result) + { + return new RedisValue(result.ToString()); + } + public static IReadOnlyList ToTimeStampArray(this RedisResult result) { RedisResult[] redisResults = (RedisResult[])result!; @@ -715,5 +726,45 @@ public static Dictionary[] ToDictionarys(this RedisResult r return dicts; } + + public static Tuple? ToSortedSetPopResult(this RedisResult result) + { + if (result.IsNull) + { + return null; + } + + var resultArray = (RedisResult[])result!; + var resultKey = resultArray[0].ToRedisKey(); + var value = resultArray[1].ToRedisValue(); + var score = resultArray[2].ToDouble(); + var valuesWithScores = new RedisValueWithScore(value, score); + + return new Tuple(resultKey, valuesWithScores); + } + + public static Tuple>? ToSortedSetPopResults(this RedisResult result) + { + if (result.IsNull) + { + return null; + } + + var resultArray = (RedisResult[])result!; + var resultKey = resultArray[0].ToRedisKey(); + var resultSetItems = resultArray[1].ToArray(); + + List valuesWithScores = new List(); + + foreach (var resultSetItem in resultSetItems) + { + var resultSetItemArray = (RedisResult[])resultSetItem!; + var value = resultSetItemArray[0].ToRedisValue(); + var score = resultSetItemArray[1].ToDouble(); + valuesWithScores.Add(new RedisValueWithScore(value, score)); + } + + return new Tuple>(resultKey, valuesWithScores); + } } } \ No newline at end of file diff --git a/tests/NRedisStack.Tests/Core Commands/CoreTests.cs b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs index 9a3d763a..7c6e82fa 100644 --- a/tests/NRedisStack.Tests/Core Commands/CoreTests.cs +++ b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs @@ -1,10 +1,8 @@ using Xunit; using NRedisStack.Core; -using NRedisStack; using static NRedisStack.Auxiliary; using StackExchange.Redis; -using System.Xml.Linq; -using System.Reflection; +using NRedisStack.Core.DataTypes; using NRedisStack.RedisStackCommands; @@ -146,4 +144,274 @@ public async Task TestSetInfoNullAsync() // Assert that the extracted sub-strings are equal Assert.Equal(infoAfterLibNameToEnd, infoBeforeLibNameToEnd); } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] + public void TestBzmPop() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + var sortedSetKey = "my-set"; + + db.SortedSetAdd(sortedSetKey, "a", 1.5); + db.SortedSetAdd(sortedSetKey, "b", 5.1); + db.SortedSetAdd(sortedSetKey, "c", 3.7); + db.SortedSetAdd(sortedSetKey, "d", 9.4); + db.SortedSetAdd(sortedSetKey, "e", 7.76); + + // Pop two items with default order, which means it will pop the minimum values. + var resultWithDefaultOrder = db.BzmPop(0, sortedSetKey, MinMaxModifier.Min, 2); + + Assert.NotNull(resultWithDefaultOrder); + Assert.Equal(sortedSetKey, resultWithDefaultOrder!.Item1); + Assert.Equal(2, resultWithDefaultOrder.Item2.Count); + Assert.Equal("a", resultWithDefaultOrder.Item2[0].Value.ToString()); + Assert.Equal("c", resultWithDefaultOrder.Item2[1].Value.ToString()); + + // Pop one more item, with descending order, which means it will pop the maximum value. + var resultWithDescendingOrder = db.BzmPop(0, sortedSetKey, MinMaxModifier.Max, 1); + + Assert.NotNull(resultWithDescendingOrder); + Assert.Equal(sortedSetKey, resultWithDescendingOrder!.Item1); + Assert.Single(resultWithDescendingOrder.Item2); + Assert.Equal("d", resultWithDescendingOrder.Item2[0].Value.ToString()); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] + public void TestBzmPopNull() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + // Nothing in the set, and a short server timeout, which yields null. + var result = db.BzmPop(0.5, "my-set", MinMaxModifier.Min, null); + + Assert.Null(result); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] + public void TestBzmPopMultiplexerTimeout() + { + var configurationOptions = new ConfigurationOptions(); + configurationOptions.SyncTimeout = 1000; + configurationOptions.EndPoints.Add("localhost"); + var redis = ConnectionMultiplexer.Connect(configurationOptions); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + // Server would wait forever, but the multiplexer times out in 1 second. + Assert.Throws(() => db.BzmPop(0, "my-set", MinMaxModifier.Min)); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] + public void TestBzmPopMultipleSets() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + db.SortedSetAdd("set-one", "a", 1.5); + db.SortedSetAdd("set-one", "b", 5.1); + db.SortedSetAdd("set-one", "c", 3.7); + db.SortedSetAdd("set-two", "d", 9.4); + db.SortedSetAdd("set-two", "e", 7.76); + + var result = db.BzmPop(0, "set-two", MinMaxModifier.Max); + + Assert.NotNull(result); + Assert.Equal("set-two", result!.Item1); + Assert.Single(result.Item2); + Assert.Equal("d", result.Item2[0].Value.ToString()); + + result = db.BzmPop(0, new[] { new RedisKey("set-two"), new RedisKey("set-one") }, MinMaxModifier.Min); + + Assert.NotNull(result); + Assert.Equal("set-two", result!.Item1); + Assert.Single(result.Item2); + Assert.Equal("e", result.Item2[0].Value.ToString()); + + result = db.BzmPop(0, new[] { new RedisKey("set-two"), new RedisKey("set-one") }, MinMaxModifier.Max); + + Assert.NotNull(result); + Assert.Equal("set-one", result!.Item1); + Assert.Single(result.Item2); + Assert.Equal("b", result.Item2[0].Value.ToString()); + + result = db.BzmPop(0, "set-one", MinMaxModifier.Min, count: 2); + + Assert.NotNull(result); + Assert.Equal("set-one", result!.Item1); + Assert.Equal(2, result.Item2.Count); + Assert.Equal("a", result.Item2[0].Value.ToString()); + Assert.Equal("c", result.Item2[1].Value.ToString()); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] + public void TestBzmPopNoKeysProvided() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + // Server would wait forever, but the multiplexer times out in 1 second. + Assert.Throws(() => db.BzmPop(0, Array.Empty(), MinMaxModifier.Min)); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] + public void TestBzmPopWithOrderEnum() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + var sortedSetKey = "my-set"; + + db.SortedSetAdd(sortedSetKey, "a", 1.5); + db.SortedSetAdd(sortedSetKey, "b", 5.1); + db.SortedSetAdd(sortedSetKey, "c", 3.7); + + // Pop two items with default order, which means it will pop the minimum values. + var resultWithDefaultOrder = db.BzmPop(0, sortedSetKey, Order.Ascending.ToMinMax()); + + Assert.NotNull(resultWithDefaultOrder); + Assert.Equal(sortedSetKey, resultWithDefaultOrder!.Item1); + Assert.Single(resultWithDefaultOrder.Item2); + Assert.Equal("a", resultWithDefaultOrder.Item2[0].Value.ToString()); + + // Pop one more item, with descending order, which means it will pop the maximum value. + var resultWithDescendingOrder = db.BzmPop(0, sortedSetKey, Order.Descending.ToMinMax()); + + Assert.NotNull(resultWithDescendingOrder); + Assert.Equal(sortedSetKey, resultWithDescendingOrder!.Item1); + Assert.Single(resultWithDescendingOrder.Item2); + Assert.Equal("b", resultWithDescendingOrder.Item2[0].Value.ToString()); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] + public void TestBzPopMin() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + var sortedSetKey = "my-set"; + + db.SortedSetAdd(sortedSetKey, "a", 1.5); + db.SortedSetAdd(sortedSetKey, "b", 5.1); + + var result = db.BzPopMin(sortedSetKey, 0); + + Assert.NotNull(result); + Assert.Equal(sortedSetKey, result!.Item1); + Assert.Equal("a", result.Item2.Value.ToString()); + Assert.Equal(1.5, result.Item2.Score); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] + public void TestBzPopMinNull() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + // Nothing in the set, and a short server timeout, which yields null. + var result = db.BzPopMin("my-set", 0.5); + + Assert.Null(result); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] + public void TestBzPopMinMultipleSets() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + db.SortedSetAdd("set-one", "a", 1.5); + db.SortedSetAdd("set-one", "b", 5.1); + db.SortedSetAdd("set-two", "e", 7.76); + + var result = db.BzPopMin(new[] { new RedisKey("set-two"), new RedisKey("set-one") }, 0); + + Assert.NotNull(result); + Assert.Equal("set-two", result!.Item1); + Assert.Equal("e", result.Item2.Value.ToString()); + + result = db.BzPopMin(new[] { new RedisKey("set-two"), new RedisKey("set-one") }, 0); + + Assert.NotNull(result); + Assert.Equal("set-one", result!.Item1); + Assert.Equal("a", result.Item2.Value.ToString()); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] + public void TestBzPopMax() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + var sortedSetKey = "my-set"; + + db.SortedSetAdd(sortedSetKey, "a", 1.5); + db.SortedSetAdd(sortedSetKey, "b", 5.1); + + var result = db.BzPopMax(sortedSetKey, 0); + + Assert.NotNull(result); + Assert.Equal(sortedSetKey, result!.Item1); + Assert.Equal("b", result.Item2.Value.ToString()); + Assert.Equal(5.1, result.Item2.Score); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] + public void TestBzPopMaxNull() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + // Nothing in the set, and a short server timeout, which yields null. + var result = db.BzPopMax("my-set", 0.5); + + Assert.Null(result); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] + public void TestBzPopMaxMultipleSets() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + db.SortedSetAdd("set-one", "a", 1.5); + db.SortedSetAdd("set-one", "b", 5.1); + db.SortedSetAdd("set-two", "e", 7.76); + + var result = db.BzPopMax(new[] { new RedisKey("set-two"), new RedisKey("set-one") }, 0); + + Assert.NotNull(result); + Assert.Equal("set-two", result!.Item1); + Assert.Equal("e", result.Item2.Value.ToString()); + + result = db.BzPopMax(new[] { new RedisKey("set-two"), new RedisKey("set-one") }, 0); + + Assert.NotNull(result); + Assert.Equal("set-one", result!.Item1); + Assert.Equal("b", result.Item2.Value.ToString()); + } } \ No newline at end of file