From 2ea5b6b0addf70d4aef75ab01396508b7a6c5078 Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Thu, 15 Feb 2024 13:41:05 +0200 Subject: [PATCH 1/8] Rename existing blocking commands The current naming for the blocking commands does not feel right. For example "BzmPop", it feels like there are no naming rules. So rename existing methods by this rule: Use capital letters at the beginning of human-readable words, e.g. "Pop", and also use capital letters where the Redis command has a single letter, e.g. "B", which stands for "Blocking", "Z" which stands for "Set" and "M" which stands for "Multi". So the new name is "BZMPop". It's still a good time to do such renaming, because these commands have not been released. Also it is a crucial moment to get the naming as right as possible, for the same reason. --- .../CoreCommands/CoreCommandBuilder.cs | 6 +- src/NRedisStack/CoreCommands/CoreCommands.cs | 30 ++++----- .../Core Commands/CoreTests.cs | 66 +++++++++---------- 3 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs b/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs index 6c71a8a2..33c636ac 100644 --- a/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs +++ b/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs @@ -21,7 +21,7 @@ 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) + public static SerializedCommand BZMPop(double timeout, RedisKey[] keys, MinMaxModifier minMaxModifier, long? count) { if (keys.Length == 0) { @@ -44,7 +44,7 @@ public static SerializedCommand BzmPop(double timeout, RedisKey[] keys, MinMaxMo return new SerializedCommand(RedisCoreCommands.BZMPOP, args); } - public static SerializedCommand BzPopMin(RedisKey[] keys, double timeout) + public static SerializedCommand BZPopMin(RedisKey[] keys, double timeout) { if (keys.Length == 0) { @@ -58,7 +58,7 @@ public static SerializedCommand BzPopMin(RedisKey[] keys, double timeout) return new SerializedCommand(RedisCoreCommands.BZPOPMIN, args); } - public static SerializedCommand BzPopMax(RedisKey[] keys, double timeout) + public static SerializedCommand BZPopMax(RedisKey[] keys, double timeout) { if (keys.Length == 0) { diff --git a/src/NRedisStack/CoreCommands/CoreCommands.cs b/src/NRedisStack/CoreCommands/CoreCommands.cs index 6b69074c..5a5dc04a 100644 --- a/src/NRedisStack/CoreCommands/CoreCommands.cs +++ b/src/NRedisStack/CoreCommands/CoreCommands.cs @@ -53,15 +53,15 @@ public static bool ClientSetInfo(this IDatabase db, SetInfoAttr attr, string val /// 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) + public static Tuple>? BZMPop(this IDatabase db, double timeout, RedisKey[] keys, MinMaxModifier minMaxModifier, long? count = null) { - var command = CoreCommandBuilder.BzmPop(timeout, keys, minMaxModifier, count); + 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. @@ -74,9 +74,9 @@ public static bool ClientSetInfo(this IDatabase db, SetInfoAttr attr, string val /// 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) + public static Tuple>? BZMPop(this IDatabase db, double timeout, RedisKey key, MinMaxModifier minMaxModifier, long? count = null) { - return BzmPop(db, timeout, new[] { key }, minMaxModifier, count); + return BZMPop(db, timeout, new[] { key }, minMaxModifier, count); } /// @@ -106,14 +106,14 @@ public static bool ClientSetInfo(this IDatabase db, SetInfoAttr attr, string val /// 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) + public static Tuple? BZPopMin(this IDatabase db, RedisKey[] keys, double timeout) { - var command = CoreCommandBuilder.BzPopMin(keys, timeout); + var command = CoreCommandBuilder.BZPopMin(keys, timeout); return db.Execute(command).ToSortedSetPopResult(); } /// - /// Syntactic sugar for , + /// Syntactic sugar for , /// where only one key is used. /// /// The class where this extension method is applied. @@ -122,9 +122,9 @@ public static bool ClientSetInfo(this IDatabase db, SetInfoAttr attr, string val /// 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) + public static Tuple? BZPopMin(this IDatabase db, RedisKey key, double timeout) { - return BzPopMin(db, new[] { key }, timeout); + return BZPopMin(db, new[] { key }, timeout); } @@ -155,14 +155,14 @@ public static bool ClientSetInfo(this IDatabase db, SetInfoAttr attr, string val /// 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) + public static Tuple? BZPopMax(this IDatabase db, RedisKey[] keys, double timeout) { - var command = CoreCommandBuilder.BzPopMax(keys, timeout); + var command = CoreCommandBuilder.BZPopMax(keys, timeout); return db.Execute(command).ToSortedSetPopResult(); } /// - /// Syntactic sugar for , + /// Syntactic sugar for , /// where only one key is used. /// /// The class where this extension method is applied. @@ -171,9 +171,9 @@ public static bool ClientSetInfo(this IDatabase db, SetInfoAttr attr, string val /// 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) + public static Tuple? BZPopMax(this IDatabase db, RedisKey key, double timeout) { - return BzPopMax(db, new[] { key }, timeout); + return BZPopMax(db, new[] { key }, timeout); } } } diff --git a/tests/NRedisStack.Tests/Core Commands/CoreTests.cs b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs index 275980bf..29edeade 100644 --- a/tests/NRedisStack.Tests/Core Commands/CoreTests.cs +++ b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs @@ -138,7 +138,7 @@ public async Task TestSetInfoNullAsync() } [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] - public void TestBzmPop() + public void TestBZMPop() { var redis = ConnectionMultiplexer.Connect("localhost"); @@ -153,8 +153,8 @@ public void TestBzmPop() 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); + // Pop two items with Min modifier, 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); @@ -162,8 +162,8 @@ public void TestBzmPop() 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); + // Pop one more item, with Max modifier, 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); @@ -172,7 +172,7 @@ public void TestBzmPop() } [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] - public void TestBzmPopNull() + public void TestBZMPopNull() { var redis = ConnectionMultiplexer.Connect("localhost"); @@ -180,13 +180,13 @@ public void TestBzmPopNull() 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); + 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() + public void TestBZMPopMultiplexerTimeout() { var configurationOptions = new ConfigurationOptions(); configurationOptions.SyncTimeout = 1000; @@ -197,11 +197,11 @@ public void TestBzmPopMultiplexerTimeout() db.Execute("FLUSHALL"); // Server would wait forever, but the multiplexer times out in 1 second. - Assert.Throws(() => db.BzmPop(0, "my-set", MinMaxModifier.Min)); + Assert.Throws(() => db.BZMPop(0, "my-set", MinMaxModifier.Min)); } [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] - public void TestBzmPopMultipleSets() + public void TestBZMPopMultipleSets() { var redis = ConnectionMultiplexer.Connect("localhost"); @@ -214,28 +214,28 @@ public void TestBzmPopMultipleSets() db.SortedSetAdd("set-two", "d", 9.4); db.SortedSetAdd("set-two", "e", 7.76); - var result = db.BzmPop(0, "set-two", MinMaxModifier.Max); + 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); + 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); + 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); + result = db.BZMPop(0, "set-one", MinMaxModifier.Min, count: 2); Assert.NotNull(result); Assert.Equal("set-one", result!.Item1); @@ -245,7 +245,7 @@ public void TestBzmPopMultipleSets() } [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] - public void TestBzmPopNoKeysProvided() + public void TestBZMPopNoKeysProvided() { var redis = ConnectionMultiplexer.Connect("localhost"); @@ -253,11 +253,11 @@ public void TestBzmPopNoKeysProvided() db.Execute("FLUSHALL"); // Server would wait forever, but the multiplexer times out in 1 second. - Assert.Throws(() => db.BzmPop(0, Array.Empty(), MinMaxModifier.Min)); + Assert.Throws(() => db.BZMPop(0, Array.Empty(), MinMaxModifier.Min)); } [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] - public void TestBzmPopWithOrderEnum() + public void TestBZMPopWithOrderEnum() { var redis = ConnectionMultiplexer.Connect("localhost"); @@ -271,7 +271,7 @@ public void TestBzmPopWithOrderEnum() 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()); + var resultWithDefaultOrder = db.BZMPop(0, sortedSetKey, Order.Ascending.ToMinMax()); Assert.NotNull(resultWithDefaultOrder); Assert.Equal(sortedSetKey, resultWithDefaultOrder!.Item1); @@ -279,7 +279,7 @@ public void TestBzmPopWithOrderEnum() 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()); + var resultWithDescendingOrder = db.BZMPop(0, sortedSetKey, Order.Descending.ToMinMax()); Assert.NotNull(resultWithDescendingOrder); Assert.Equal(sortedSetKey, resultWithDescendingOrder!.Item1); @@ -288,7 +288,7 @@ public void TestBzmPopWithOrderEnum() } [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] - public void TestBzPopMin() + public void TestBZPopMin() { var redis = ConnectionMultiplexer.Connect("localhost"); @@ -300,7 +300,7 @@ public void TestBzPopMin() db.SortedSetAdd(sortedSetKey, "a", 1.5); db.SortedSetAdd(sortedSetKey, "b", 5.1); - var result = db.BzPopMin(sortedSetKey, 0); + var result = db.BZPopMin(sortedSetKey, 0); Assert.NotNull(result); Assert.Equal(sortedSetKey, result!.Item1); @@ -309,7 +309,7 @@ public void TestBzPopMin() } [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] - public void TestBzPopMinNull() + public void TestBZPopMinNull() { var redis = ConnectionMultiplexer.Connect("localhost"); @@ -317,13 +317,13 @@ public void TestBzPopMinNull() db.Execute("FLUSHALL"); // Nothing in the set, and a short server timeout, which yields null. - var result = db.BzPopMin("my-set", 0.5); + var result = db.BZPopMin("my-set", 0.5); Assert.Null(result); } [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] - public void TestBzPopMinMultipleSets() + public void TestBZPopMinMultipleSets() { var redis = ConnectionMultiplexer.Connect("localhost"); @@ -334,13 +334,13 @@ public void TestBzPopMinMultipleSets() 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); + 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); + result = db.BZPopMin(new[] { new RedisKey("set-two"), new RedisKey("set-one") }, 0); Assert.NotNull(result); Assert.Equal("set-one", result!.Item1); @@ -348,7 +348,7 @@ public void TestBzPopMinMultipleSets() } [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] - public void TestBzPopMax() + public void TestBZPopMax() { var redis = ConnectionMultiplexer.Connect("localhost"); @@ -360,7 +360,7 @@ public void TestBzPopMax() db.SortedSetAdd(sortedSetKey, "a", 1.5); db.SortedSetAdd(sortedSetKey, "b", 5.1); - var result = db.BzPopMax(sortedSetKey, 0); + var result = db.BZPopMax(sortedSetKey, 0); Assert.NotNull(result); Assert.Equal(sortedSetKey, result!.Item1); @@ -369,7 +369,7 @@ public void TestBzPopMax() } [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] - public void TestBzPopMaxNull() + public void TestBZPopMaxNull() { var redis = ConnectionMultiplexer.Connect("localhost"); @@ -377,13 +377,13 @@ public void TestBzPopMaxNull() db.Execute("FLUSHALL"); // Nothing in the set, and a short server timeout, which yields null. - var result = db.BzPopMax("my-set", 0.5); + var result = db.BZPopMax("my-set", 0.5); Assert.Null(result); } [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] - public void TestBzPopMaxMultipleSets() + public void TestBZPopMaxMultipleSets() { var redis = ConnectionMultiplexer.Connect("localhost"); @@ -394,13 +394,13 @@ public void TestBzPopMaxMultipleSets() 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); + 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); + result = db.BZPopMax(new[] { new RedisKey("set-two"), new RedisKey("set-one") }, 0); Assert.NotNull(result); Assert.Equal("set-one", result!.Item1); From ade0ac4fbaf3f1147aa21fe4fe3b3671de06fd10 Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Thu, 15 Feb 2024 12:12:13 +0200 Subject: [PATCH 2/8] Add ToString to RedisValueWithScore --- .../CoreCommands/DataTypes/RedisValueWithScore.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/NRedisStack/CoreCommands/DataTypes/RedisValueWithScore.cs b/src/NRedisStack/CoreCommands/DataTypes/RedisValueWithScore.cs index 6587db45..f3dbb550 100644 --- a/src/NRedisStack/CoreCommands/DataTypes/RedisValueWithScore.cs +++ b/src/NRedisStack/CoreCommands/DataTypes/RedisValueWithScore.cs @@ -28,4 +28,12 @@ public RedisValueWithScore(RedisValue value, double score) /// ZADD my-set 5.1 my-value, the score is 5.1. /// public double Score { get; } -} \ No newline at end of file + + /// + /// ToString override. + /// + public override string ToString() + { + return $"{Value}: {Score}"; + } +} From 695d0c92d09607f07a26c6078ba82ea611338ca7 Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Thu, 15 Feb 2024 10:27:48 +0200 Subject: [PATCH 3/8] Add support for BLPOP and BRPOP commands Issues #249 and #250 --- .../CoreCommands/CoreCommandBuilder.cs | 26 ++-- src/NRedisStack/CoreCommands/CoreCommands.cs | 96 +++++++++++++++ .../CoreCommands/Literals/Commands.cs | 2 + src/NRedisStack/ResponseParser.cs | 14 +++ .../Core Commands/CoreTests.cs | 114 ++++++++++++++++++ 5 files changed, 242 insertions(+), 10 deletions(-) diff --git a/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs b/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs index 33c636ac..f1740240 100644 --- a/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs +++ b/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs @@ -46,19 +46,25 @@ public static SerializedCommand BZMPop(double timeout, RedisKey[] keys, MinMaxMo public static SerializedCommand BZPopMin(RedisKey[] keys, double timeout) { - if (keys.Length == 0) - { - throw new ArgumentException("At least one key must be provided."); - } + return BlockingCommandWithKeysAndTimeout(RedisCoreCommands.BZPOPMIN, keys, timeout); + } - List args = new List(); - args.AddRange(keys.Cast()); - args.Add(timeout); + public static SerializedCommand BZPopMax(RedisKey[] keys, double timeout) + { + return BlockingCommandWithKeysAndTimeout(RedisCoreCommands.BZPOPMAX, keys, timeout); + } + + public static SerializedCommand BLPop(RedisKey[] keys, double timeout) + { + return BlockingCommandWithKeysAndTimeout(RedisCoreCommands.BLPOP, keys, timeout); + } - return new SerializedCommand(RedisCoreCommands.BZPOPMIN, args); + public static SerializedCommand BRPop(RedisKey[] keys, double timeout) + { + return BlockingCommandWithKeysAndTimeout(RedisCoreCommands.BRPOP, keys, timeout); } - public static SerializedCommand BZPopMax(RedisKey[] keys, double timeout) + private static SerializedCommand BlockingCommandWithKeysAndTimeout(String command, RedisKey[] keys, double timeout) { if (keys.Length == 0) { @@ -69,7 +75,7 @@ public static SerializedCommand BZPopMax(RedisKey[] keys, double timeout) args.AddRange(keys.Cast()); args.Add(timeout); - return new SerializedCommand(RedisCoreCommands.BZPOPMAX, args); + return new SerializedCommand(command, args); } } } diff --git a/src/NRedisStack/CoreCommands/CoreCommands.cs b/src/NRedisStack/CoreCommands/CoreCommands.cs index 5a5dc04a..bb2e1a6a 100644 --- a/src/NRedisStack/CoreCommands/CoreCommands.cs +++ b/src/NRedisStack/CoreCommands/CoreCommands.cs @@ -175,5 +175,101 @@ public static bool ClientSetInfo(this IDatabase db, SetInfoAttr attr, string val { return BZPopMax(db, new[] { key }, timeout); } + + /// + /// The BLPOP command. + ///

+ /// Removes and returns an entry from the head (left side) of the first non-empty list in . + /// If none of the lists 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 value, together with the key it was popped from, or null if the server timeout + /// expires. + /// + public static Tuple? BLPop(this IDatabase db, RedisKey[] keys, double timeout) + { + var command = CoreCommandBuilder.BLPop(keys, timeout); + return db.Execute(command).ToListPopResult(); + } + + /// + /// 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 value, together with the key it was popped from, or null if the server timeout + /// expires. + /// + public static Tuple? BLPop(this IDatabase db, RedisKey key, double timeout) + { + return BLPop(db, new[] { key }, timeout); + } + + /// + /// The BRPOP command. + ///

+ /// Removes and returns an entry from the tail (right side) of the first non-empty list in . + /// If none of the lists 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 value, together with the key it was popped from, or null if the server timeout + /// expires. + /// + public static Tuple? BRPop(this IDatabase db, RedisKey[] keys, double timeout) + { + var command = CoreCommandBuilder.BRPop(keys, timeout); + return db.Execute(command).ToListPopResult(); + } + + /// + /// 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 value, together with the key it was popped from, or null if the server timeout + /// expires. + /// + public static Tuple? BRPop(this IDatabase db, RedisKey key, double timeout) + { + return BRPop(db, new[] { key }, timeout); + } } } diff --git a/src/NRedisStack/CoreCommands/Literals/Commands.cs b/src/NRedisStack/CoreCommands/Literals/Commands.cs index e44c82ed..055e4f06 100644 --- a/src/NRedisStack/CoreCommands/Literals/Commands.cs +++ b/src/NRedisStack/CoreCommands/Literals/Commands.cs @@ -5,6 +5,8 @@ namespace NRedisStack.Core.Literals /// internal static class RedisCoreCommands { + public const string BLPOP = "BLPOP"; + public const string BRPOP = "BRPOP"; public const string BZMPOP = "BZMPOP"; public const string BZPOPMAX = "BZPOPMAX"; public const string BZPOPMIN = "BZPOPMIN"; diff --git a/src/NRedisStack/ResponseParser.cs b/src/NRedisStack/ResponseParser.cs index b3a7c315..dfdf463c 100644 --- a/src/NRedisStack/ResponseParser.cs +++ b/src/NRedisStack/ResponseParser.cs @@ -766,5 +766,19 @@ public static Dictionary[] ToDictionarys(this RedisResult r return new Tuple>(resultKey, valuesWithScores); } + + public static Tuple? ToListPopResult(this RedisResult result) + { + if (result.IsNull) + { + return null; + } + + var resultArray = (RedisResult[])result!; + var resultKey = resultArray[0].ToRedisKey(); + var value = resultArray[1].ToRedisValue(); + + return new Tuple(resultKey, value); + } } } \ 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 29edeade..50c9762b 100644 --- a/tests/NRedisStack.Tests/Core Commands/CoreTests.cs +++ b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs @@ -406,4 +406,118 @@ public void TestBZPopMaxMultipleSets() Assert.Equal("set-one", result!.Item1); Assert.Equal("b", result.Item2.Value.ToString()); } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "2.0.0")] + public void TestBLPop() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + db.ListRightPush("my-list", "a"); + db.ListRightPush("my-list", "b"); + + var result = db.BLPop("my-list", 0); + + Assert.NotNull(result); + Assert.Equal("my-list", result!.Item1); + Assert.Equal("a", result.Item2.ToString()); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "2.0.0")] + public void TestBLPopNull() + { + 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.BLPop("my-set", 0.5); + + Assert.Null(result); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "2.0.0")] + public void TestBLPopMultipleLists() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + db.ListRightPush("list-one", "a"); + db.ListRightPush("list-one", "b"); + db.ListRightPush("list-two", "e"); + + var result = db.BLPop(new[] { new RedisKey("list-two"), new RedisKey("list-one") }, 0); + + Assert.NotNull(result); + Assert.Equal("list-two", result!.Item1); + Assert.Equal("e", result.Item2.ToString()); + + result = db.BLPop(new[] { new RedisKey("list-two"), new RedisKey("list-one") }, 0); + + Assert.NotNull(result); + Assert.Equal("list-one", result!.Item1); + Assert.Equal("a", result.Item2.ToString()); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "2.0.0")] + public void TestBRPop() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + db.ListRightPush("my-list", "a"); + db.ListRightPush("my-list", "b"); + + var result = db.BRPop("my-list", 0); + + Assert.NotNull(result); + Assert.Equal("my-list", result!.Item1); + Assert.Equal("b", result.Item2.ToString()); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "2.0.0")] + public void TestBRPopNull() + { + 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.BRPop("my-set", 0.5); + + Assert.Null(result); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "2.0.0")] + public void TestBRPopMultipleLists() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + db.ListRightPush("list-one", "a"); + db.ListRightPush("list-one", "b"); + db.ListRightPush("list-two", "e"); + + var result = db.BRPop(new[] { new RedisKey("list-two"), new RedisKey("list-one") }, 0); + + Assert.NotNull(result); + Assert.Equal("list-two", result!.Item1); + Assert.Equal("e", result.Item2.ToString()); + + result = db.BRPop(new[] { new RedisKey("list-two"), new RedisKey("list-one") }, 0); + + Assert.NotNull(result); + Assert.Equal("list-one", result!.Item1); + Assert.Equal("b", result.Item2.ToString()); + } } \ No newline at end of file From 051214393a0dfc2d8345fa31fdc9a9461d7749da Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Thu, 15 Feb 2024 11:47:55 +0200 Subject: [PATCH 4/8] Add support for the BLMPOP command Issue #248 --- .../CoreCommands/CoreCommandBuilder.cs | 23 ++++ src/NRedisStack/CoreCommands/CoreCommands.cs | 55 +++++++++ .../CoreCommands/Literals/CommandArgs.cs | 6 +- .../CoreCommands/Literals/Commands.cs | 1 + src/NRedisStack/ResponseParser.cs | 22 ++++ .../Core Commands/CoreTests.cs | 106 +++++++++++++++++- 6 files changed, 208 insertions(+), 5 deletions(-) diff --git a/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs b/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs index f1740240..5822e9e0 100644 --- a/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs +++ b/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs @@ -54,6 +54,29 @@ public static SerializedCommand BZPopMax(RedisKey[] keys, double timeout) return BlockingCommandWithKeysAndTimeout(RedisCoreCommands.BZPOPMAX, keys, timeout); } + public static SerializedCommand BLMPop(double timeout, RedisKey[] keys, ListSide listSide, 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(listSide == ListSide.Left ? CoreArgs.LEFT : CoreArgs.RIGHT); + + if (count != null) + { + args.Add(CoreArgs.COUNT); + args.Add(count); + } + + return new SerializedCommand(RedisCoreCommands.BLMPOP, args); + } + public static SerializedCommand BLPop(RedisKey[] keys, double timeout) { return BlockingCommandWithKeysAndTimeout(RedisCoreCommands.BLPOP, keys, timeout); diff --git a/src/NRedisStack/CoreCommands/CoreCommands.cs b/src/NRedisStack/CoreCommands/CoreCommands.cs index bb2e1a6a..c053f1be 100644 --- a/src/NRedisStack/CoreCommands/CoreCommands.cs +++ b/src/NRedisStack/CoreCommands/CoreCommands.cs @@ -176,6 +176,61 @@ public static bool ClientSetInfo(this IDatabase db, SetInfoAttr attr, string val return BZPopMax(db, new[] { key }, timeout); } + /// + /// The BLMPOP command. + ///

+ /// Removes and returns up to entries from the first non-empty list in + /// . If none of the lists 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 list to pop values: left or right. + /// The maximum number of records to pop. If set to null then the server default + /// will be used. + /// A collection of values, together with the key they were popped from, or null if the + /// server timeout expires. + /// + public static Tuple>? BLMPop(this IDatabase db, double timeout, RedisKey[] keys, ListSide listSide, long? count = null) + { + var command = CoreCommandBuilder.BLMPop(timeout, keys, listSide, count); + return db.Execute(command).ToListPopResults(); + } + + /// + /// 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 list to pop values: left or right. + /// The maximum number of records to pop. If set to null then the server default + /// will be used. + /// A collection of values, together with the key they were popped from, or null if the + /// server timeout expires. + /// + public static Tuple>? BLMPop(this IDatabase db, double timeout, RedisKey key, ListSide listSide, long? count = null) + { + return BLMPop(db, timeout, new[] { key }, listSide, count); + } + /// /// The BLPOP command. ///

diff --git a/src/NRedisStack/CoreCommands/Literals/CommandArgs.cs b/src/NRedisStack/CoreCommands/Literals/CommandArgs.cs index 4e6e4242..9611cefd 100644 --- a/src/NRedisStack/CoreCommands/Literals/CommandArgs.cs +++ b/src/NRedisStack/CoreCommands/Literals/CommandArgs.cs @@ -3,9 +3,11 @@ namespace NRedisStack.Core.Literals 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 LEFT = "LEFT"; public const string MAX = "MAX"; public const string MIN = "MIN"; + public const string RIGHT = "RIGHT"; + public const string lib_name = "LIB-NAME"; + public const string lib_ver = "LIB-VER"; } } diff --git a/src/NRedisStack/CoreCommands/Literals/Commands.cs b/src/NRedisStack/CoreCommands/Literals/Commands.cs index 055e4f06..242cdbf4 100644 --- a/src/NRedisStack/CoreCommands/Literals/Commands.cs +++ b/src/NRedisStack/CoreCommands/Literals/Commands.cs @@ -5,6 +5,7 @@ namespace NRedisStack.Core.Literals ///

internal static class RedisCoreCommands { + public const string BLMPOP = "BLMPOP"; public const string BLPOP = "BLPOP"; public const string BRPOP = "BRPOP"; public const string BZMPOP = "BZMPOP"; diff --git a/src/NRedisStack/ResponseParser.cs b/src/NRedisStack/ResponseParser.cs index dfdf463c..58546d37 100644 --- a/src/NRedisStack/ResponseParser.cs +++ b/src/NRedisStack/ResponseParser.cs @@ -780,5 +780,27 @@ public static Dictionary[] ToDictionarys(this RedisResult r return new Tuple(resultKey, value); } + + public static Tuple>? ToListPopResults(this RedisResult result) + { + if (result.IsNull) + { + return null; + } + + var resultArray = (RedisResult[])result!; + var resultKey = resultArray[0].ToRedisKey(); + var resultSetItems = resultArray[1].ToArray(); + + List values = new List(); + + foreach (var resultSetItem in resultSetItems) + { + var value = (RedisValue)resultSetItem!; + values.Add(value); + } + + return new Tuple>(resultKey, values); + } } } \ 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 50c9762b..68714174 100644 --- a/tests/NRedisStack.Tests/Core Commands/CoreTests.cs +++ b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs @@ -252,7 +252,6 @@ public void TestBZMPopNoKeysProvided() 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)); } @@ -270,7 +269,7 @@ public void TestBZMPopWithOrderEnum() 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. + // Pop two items with Ascending order, which means it will pop the minimum values. var resultWithDefaultOrder = db.BZMPop(0, sortedSetKey, Order.Ascending.ToMinMax()); Assert.NotNull(resultWithDefaultOrder); @@ -278,7 +277,7 @@ public void TestBZMPopWithOrderEnum() 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. + // 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); @@ -407,6 +406,107 @@ public void TestBZPopMaxMultipleSets() Assert.Equal("b", result.Item2.Value.ToString()); } + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] + public void TestBLMPop() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + db.ListRightPush("my-list", "a"); + db.ListRightPush("my-list", "b"); + db.ListRightPush("my-list", "c"); + db.ListRightPush("my-list", "d"); + db.ListRightPush("my-list", "e"); + + // Pop two items from the left side. + var resultWithDefaultOrder = db.BLMPop(0, "my-list", ListSide.Left, 2); + + Assert.NotNull(resultWithDefaultOrder); + Assert.Equal("my-list", resultWithDefaultOrder!.Item1); + Assert.Equal(2, resultWithDefaultOrder.Item2.Count); + Assert.Equal("a", resultWithDefaultOrder.Item2[0].ToString()); + Assert.Equal("b", resultWithDefaultOrder.Item2[1].ToString()); + + // Pop one more item, from the right side. + var resultWithDescendingOrder = db.BLMPop(0, "my-list", ListSide.Right, 1); + + Assert.NotNull(resultWithDescendingOrder); + Assert.Equal("my-list", resultWithDescendingOrder!.Item1); + Assert.Single(resultWithDescendingOrder.Item2); + Assert.Equal("e", resultWithDescendingOrder.Item2[0].ToString()); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] + public void TestBLMPopNull() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + // Nothing in the list, and a short server timeout, which yields null. + var result = db.BLMPop(0.5, "my-list", ListSide.Left); + + Assert.Null(result); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] + public void TestBLMPopMultipleLists() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + db.ListRightPush("list-one", "a"); + db.ListRightPush("list-one", "b"); + db.ListRightPush("list-one", "c"); + db.ListRightPush("list-two", "d"); + db.ListRightPush("list-two", "e"); + + var result = db.BLMPop(0, "list-two", ListSide.Right); + + Assert.NotNull(result); + Assert.Equal("list-two", result!.Item1); + Assert.Single(result.Item2); + Assert.Equal("e", result.Item2[0].ToString()); + + result = db.BLMPop(0, new[] { new RedisKey("list-two"), new RedisKey("list-one") }, ListSide.Left); + + Assert.NotNull(result); + Assert.Equal("list-two", result!.Item1); + Assert.Single(result.Item2); + Assert.Equal("d", result.Item2[0].ToString()); + + result = db.BLMPop(0, new[] { new RedisKey("list-two"), new RedisKey("list-one") }, ListSide.Right); + + Assert.NotNull(result); + Assert.Equal("list-one", result!.Item1); + Assert.Single(result.Item2); + Assert.Equal("c", result.Item2[0].ToString()); + + result = db.BLMPop(0, "list-one", ListSide.Left, count: 2); + + Assert.NotNull(result); + Assert.Equal("list-one", result!.Item1); + Assert.Equal(2, result.Item2.Count); + Assert.Equal("a", result.Item2[0].ToString()); + Assert.Equal("b", result.Item2[1].ToString()); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] + public void TestBLMPopNoKeysProvided() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + Assert.Throws(() => db.BLMPop(0, Array.Empty(), ListSide.Left)); + } + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "2.0.0")] public void TestBLPop() { From c6ccaf08f0ed04e79cc29487aaf649af305b43fc Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Thu, 15 Feb 2024 13:26:07 +0200 Subject: [PATCH 5/8] Add support for the BLMOVE and BRPOPLPUSH commands Issues #235 and #251 --- .../CoreCommands/CoreCommandBuilder.cs | 22 +++++ src/NRedisStack/CoreCommands/CoreCommands.cs | 44 ++++++++++ .../CoreCommands/Literals/Commands.cs | 2 + .../Core Commands/CoreTests.cs | 82 +++++++++++++++++++ 4 files changed, 150 insertions(+) diff --git a/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs b/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs index 5822e9e0..5f8f62e1 100644 --- a/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs +++ b/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs @@ -87,6 +87,28 @@ public static SerializedCommand BRPop(RedisKey[] keys, double timeout) return BlockingCommandWithKeysAndTimeout(RedisCoreCommands.BRPOP, keys, timeout); } + public static SerializedCommand BLMove(RedisKey source, RedisKey destination, ListSide sourceSide, ListSide destinationSide, double timeout) + { + List args = new List(); + args.Add(source); + args.Add(destination); + args.Add(sourceSide == ListSide.Left ? CoreArgs.LEFT : CoreArgs.RIGHT); + args.Add(destinationSide == ListSide.Left ? CoreArgs.LEFT : CoreArgs.RIGHT); + args.Add(timeout); + + return new SerializedCommand(RedisCoreCommands.BLMOVE, args); + } + + public static SerializedCommand BRPopLPush(RedisKey source, RedisKey destination, double timeout) + { + List args = new List(); + args.Add(source); + args.Add(destination); + args.Add(timeout); + + return new SerializedCommand(RedisCoreCommands.BRPOPLPUSH, args); + } + private static SerializedCommand BlockingCommandWithKeysAndTimeout(String command, RedisKey[] keys, double timeout) { if (keys.Length == 0) diff --git a/src/NRedisStack/CoreCommands/CoreCommands.cs b/src/NRedisStack/CoreCommands/CoreCommands.cs index c053f1be..514f3176 100644 --- a/src/NRedisStack/CoreCommands/CoreCommands.cs +++ b/src/NRedisStack/CoreCommands/CoreCommands.cs @@ -326,5 +326,49 @@ public static bool ClientSetInfo(this IDatabase db, SetInfoAttr attr, string val { return BRPop(db, new[] { key }, timeout); } + + /// + /// The BLMOVE command. + ///

+ /// Atomically returns and removes the first or last element of the list stored at + /// (depending on the value of ), and pushes the element as the first or last + /// element of the list stored at (depending on the value of + /// ). + ///

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

+ /// The class where this extension method is applied. + /// The key of the source list. + /// The key of the destination list. + /// What side of the list to remove from. + /// What side of the list to move to. + /// Server-side timeout for the wait. A value of 0 means to wait indefinitely. + /// The element being popped and pushed, or null if the server timeout expires. + /// + public static RedisValue? BLMove(this IDatabase db, RedisKey source, RedisKey destination, ListSide sourceSide, ListSide destinationSide, double timeout) + { + var command = CoreCommandBuilder.BLMove(source, destination, sourceSide, destinationSide, timeout); + return db.Execute(command).ToRedisValue(); + } + + /// + /// The BRPOPLPUSH command. + ///

+ /// 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. + ///

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

+ /// The class where this extension method is applied. + /// The key of the source list. + /// The key of the destination list. + /// Server-side timeout for the wait. A value of 0 means to wait indefinitely. + /// The element being popped and pushed, or null if the server timeout expires. + /// + public static RedisValue? BRPopLPush(this IDatabase db, RedisKey source, RedisKey destination, double timeout) + { + var command = CoreCommandBuilder.BRPopLPush(source, destination, timeout); + return db.Execute(command).ToRedisValue(); + } } } diff --git a/src/NRedisStack/CoreCommands/Literals/Commands.cs b/src/NRedisStack/CoreCommands/Literals/Commands.cs index 242cdbf4..7e969144 100644 --- a/src/NRedisStack/CoreCommands/Literals/Commands.cs +++ b/src/NRedisStack/CoreCommands/Literals/Commands.cs @@ -5,9 +5,11 @@ namespace NRedisStack.Core.Literals /// internal static class RedisCoreCommands { + public const string BLMOVE = "BLMOVE"; public const string BLMPOP = "BLMPOP"; public const string BLPOP = "BLPOP"; public const string BRPOP = "BRPOP"; + public const string BRPOPLPUSH = "BRPOPLPUSH"; public const string BZMPOP = "BZMPOP"; public const string BZPOPMAX = "BZPOPMAX"; public const string BZPOPMIN = "BZPOPMIN"; diff --git a/tests/NRedisStack.Tests/Core Commands/CoreTests.cs b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs index 68714174..00224b3c 100644 --- a/tests/NRedisStack.Tests/Core Commands/CoreTests.cs +++ b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs @@ -620,4 +620,86 @@ public void TestBRPopMultipleLists() Assert.Equal("list-one", result!.Item1); Assert.Equal("b", result.Item2.ToString()); } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "6.2.0")] + public void TestBLMove() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + db.ListRightPush("list-one", "a"); + db.ListRightPush("list-one", "b"); + + db.ListRightPush("list-two", "c"); + db.ListRightPush("list-two", "d"); + + var result = db.BLMove("list-one", "list-two", ListSide.Right, ListSide.Left, 0); + Assert.NotNull(result); + Assert.Equal("b", result!); + + Assert.Equal(1, db.ListLength("list-one")); + Assert.Equal("a", db.ListGetByIndex("list-one", 0)); + Assert.Equal(3, db.ListLength("list-two")); + Assert.Equal("b", db.ListGetByIndex("list-two", 0)); + Assert.Equal("c", db.ListGetByIndex("list-two", 1)); + Assert.Equal("d", db.ListGetByIndex("list-two", 2)); + + result = db.BLMove("list-two", "list-one", ListSide.Left, ListSide.Right, 0); + Assert.NotNull(result); + Assert.Equal("b", result!); + + Assert.Equal(2, db.ListLength("list-one")); + Assert.Equal("a", db.ListGetByIndex("list-one", 0)); + Assert.Equal("b", db.ListGetByIndex("list-one", 1)); + Assert.Equal(2, db.ListLength("list-two")); + Assert.Equal("c", db.ListGetByIndex("list-two", 0)); + Assert.Equal("d", db.ListGetByIndex("list-two", 1)); + + result = db.BLMove("list-one", "list-two", ListSide.Left, ListSide.Left, 0); + Assert.NotNull(result); + Assert.Equal("a", result!); + + Assert.Equal(1, db.ListLength("list-one")); + Assert.Equal("b", db.ListGetByIndex("list-one", 0)); + Assert.Equal(3, db.ListLength("list-two")); + Assert.Equal("a", db.ListGetByIndex("list-two", 0)); + Assert.Equal("c", db.ListGetByIndex("list-two", 1)); + Assert.Equal("d", db.ListGetByIndex("list-two", 2)); + + result = db.BLMove("list-two", "list-one", ListSide.Right, ListSide.Right, 0); + Assert.NotNull(result); + Assert.Equal("d", result!); + + Assert.Equal(2, db.ListLength("list-one")); + Assert.Equal("b", db.ListGetByIndex("list-one", 0)); + Assert.Equal("d", db.ListGetByIndex("list-one", 1)); + Assert.Equal(2, db.ListLength("list-two")); + Assert.Equal("a", db.ListGetByIndex("list-two", 0)); + Assert.Equal("c", db.ListGetByIndex("list-two", 1)); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "2.2.0")] + public void TestBRPopLPush() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + + var db = redis.GetDatabase(null); + db.Execute("FLUSHALL"); + + db.ListRightPush("list-one", "a"); + db.ListRightPush("list-one", "b"); + + db.ListRightPush("list-two", "c"); + db.ListRightPush("list-two", "d"); + + var result = db.BRPopLPush("list-one", "list-two", 0); + Assert.NotNull(result); + Assert.Equal("b", result!); + + Assert.Equal(1, db.ListLength("list-one")); + Assert.Equal(3, db.ListLength("list-two")); + Assert.Equal("b", db.ListLeftPop("list-two")); + } } \ No newline at end of file From 5ac541aef488d216cc938c829dbcea1aad10f968 Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Thu, 15 Feb 2024 14:19:39 +0200 Subject: [PATCH 6/8] Try to fix flaky test --- tests/NRedisStack.Tests/Core Commands/CoreTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/NRedisStack.Tests/Core Commands/CoreTests.cs b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs index 00224b3c..dd2cf662 100644 --- a/tests/NRedisStack.Tests/Core Commands/CoreTests.cs +++ b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs @@ -263,7 +263,7 @@ public void TestBZMPopWithOrderEnum() var db = redis.GetDatabase(null); db.Execute("FLUSHALL"); - var sortedSetKey = "my-set"; + var sortedSetKey = "my-set-" + Guid.NewGuid(); db.SortedSetAdd(sortedSetKey, "a", 1.5); db.SortedSetAdd(sortedSetKey, "b", 5.1); From 889d8a1b246b546c9abcb5bec90888c67573eabc Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Mon, 19 Feb 2024 16:30:17 +0200 Subject: [PATCH 7/8] Use Redis fixture in CoreTests --- .../Core Commands/CoreTests.cs | 96 +++++-------------- tests/NRedisStack.Tests/RedisFixture.cs | 61 +++++++----- 2 files changed, 60 insertions(+), 97 deletions(-) diff --git a/tests/NRedisStack.Tests/Core Commands/CoreTests.cs b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs index dd2cf662..b9476d3b 100644 --- a/tests/NRedisStack.Tests/Core Commands/CoreTests.cs +++ b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs @@ -140,9 +140,7 @@ public async Task TestSetInfoNullAsync() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] public void TestBZMPop() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); var sortedSetKey = "my-set"; @@ -174,9 +172,7 @@ public void TestBZMPop() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] public void TestBZMPopNull() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); // Nothing in the set, and a short server timeout, which yields null. @@ -190,8 +186,8 @@ public void TestBZMPopMultiplexerTimeout() { var configurationOptions = new ConfigurationOptions(); configurationOptions.SyncTimeout = 1000; - configurationOptions.EndPoints.Add("localhost"); - var redis = ConnectionMultiplexer.Connect(configurationOptions); + + using var redis = redisFixture.CustomRedis(configurationOptions, out _); var db = redis.GetDatabase(null); db.Execute("FLUSHALL"); @@ -203,9 +199,7 @@ public void TestBZMPopMultiplexerTimeout() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] public void TestBZMPopMultipleSets() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); db.SortedSetAdd("set-one", "a", 1.5); @@ -247,9 +241,7 @@ public void TestBZMPopMultipleSets() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] public void TestBZMPopNoKeysProvided() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); Assert.Throws(() => db.BZMPop(0, Array.Empty(), MinMaxModifier.Min)); @@ -258,9 +250,7 @@ public void TestBZMPopNoKeysProvided() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] public void TestBZMPopWithOrderEnum() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); var sortedSetKey = "my-set-" + Guid.NewGuid(); @@ -289,9 +279,7 @@ public void TestBZMPopWithOrderEnum() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] public void TestBZPopMin() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); var sortedSetKey = "my-set"; @@ -310,9 +298,7 @@ public void TestBZPopMin() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] public void TestBZPopMinNull() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); // Nothing in the set, and a short server timeout, which yields null. @@ -324,9 +310,7 @@ public void TestBZPopMinNull() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] public void TestBZPopMinMultipleSets() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); db.SortedSetAdd("set-one", "a", 1.5); @@ -349,9 +333,7 @@ public void TestBZPopMinMultipleSets() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] public void TestBZPopMax() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); var sortedSetKey = "my-set"; @@ -370,9 +352,7 @@ public void TestBZPopMax() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] public void TestBZPopMaxNull() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); // Nothing in the set, and a short server timeout, which yields null. @@ -384,9 +364,7 @@ public void TestBZPopMaxNull() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")] public void TestBZPopMaxMultipleSets() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); db.SortedSetAdd("set-one", "a", 1.5); @@ -409,9 +387,7 @@ public void TestBZPopMaxMultipleSets() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] public void TestBLMPop() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); db.ListRightPush("my-list", "a"); @@ -441,9 +417,7 @@ public void TestBLMPop() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] public void TestBLMPopNull() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); // Nothing in the list, and a short server timeout, which yields null. @@ -455,9 +429,7 @@ public void TestBLMPopNull() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] public void TestBLMPopMultipleLists() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); db.ListRightPush("list-one", "a"); @@ -499,9 +471,7 @@ public void TestBLMPopMultipleLists() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")] public void TestBLMPopNoKeysProvided() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); Assert.Throws(() => db.BLMPop(0, Array.Empty(), ListSide.Left)); @@ -510,9 +480,7 @@ public void TestBLMPopNoKeysProvided() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "2.0.0")] public void TestBLPop() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); db.ListRightPush("my-list", "a"); @@ -528,9 +496,7 @@ public void TestBLPop() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "2.0.0")] public void TestBLPopNull() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); // Nothing in the set, and a short server timeout, which yields null. @@ -542,9 +508,7 @@ public void TestBLPopNull() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "2.0.0")] public void TestBLPopMultipleLists() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); db.ListRightPush("list-one", "a"); @@ -567,9 +531,7 @@ public void TestBLPopMultipleLists() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "2.0.0")] public void TestBRPop() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); db.ListRightPush("my-list", "a"); @@ -585,9 +547,7 @@ public void TestBRPop() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "2.0.0")] public void TestBRPopNull() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); // Nothing in the set, and a short server timeout, which yields null. @@ -599,9 +559,7 @@ public void TestBRPopNull() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "2.0.0")] public void TestBRPopMultipleLists() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); db.ListRightPush("list-one", "a"); @@ -624,9 +582,7 @@ public void TestBRPopMultipleLists() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "6.2.0")] public void TestBLMove() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); db.ListRightPush("list-one", "a"); @@ -683,9 +639,7 @@ public void TestBLMove() [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "2.2.0")] public void TestBRPopLPush() { - var redis = ConnectionMultiplexer.Connect("localhost"); - - var db = redis.GetDatabase(null); + var db = redisFixture.Redis.GetDatabase(null); db.Execute("FLUSHALL"); db.ListRightPush("list-one", "a"); diff --git a/tests/NRedisStack.Tests/RedisFixture.cs b/tests/NRedisStack.Tests/RedisFixture.cs index f90389c6..02f92316 100644 --- a/tests/NRedisStack.Tests/RedisFixture.cs +++ b/tests/NRedisStack.Tests/RedisFixture.cs @@ -1,5 +1,3 @@ -using System.Net; -using Org.BouncyCastle.Tls; using StackExchange.Redis; namespace NRedisStack.Tests @@ -7,12 +5,35 @@ namespace NRedisStack.Tests public class RedisFixture : IDisposable { // Set the enviroment variable to specify your own alternet host and port: - string redisStandalone = Environment.GetEnvironmentVariable("REDIS") ?? "localhost:6379"; - string? redisCluster = Environment.GetEnvironmentVariable("REDIS_CLUSTER"); - string? numRedisClusterNodesEnv = Environment.GetEnvironmentVariable("NUM_REDIS_CLUSTER_NODES"); - public bool isOSSCluster = false; + readonly string redisStandalone = Environment.GetEnvironmentVariable("REDIS") ?? "localhost:6379"; + readonly string? redisCluster = Environment.GetEnvironmentVariable("REDIS_CLUSTER"); + readonly string? numRedisClusterNodesEnv = Environment.GetEnvironmentVariable("NUM_REDIS_CLUSTER_NODES"); + + public bool isOSSCluster; public RedisFixture() + { + ConfigurationOptions clusterConfig = new ConfigurationOptions + { + AsyncTimeout = 10000, + SyncTimeout = 10000 + }; + Redis = Connect(clusterConfig, out isOSSCluster); + } + + public void Dispose() + { + Redis.Close(); + } + + public ConnectionMultiplexer Redis { get; } + + public ConnectionMultiplexer CustomRedis(ConfigurationOptions configurationOptions, out bool isOssCluster) + { + return Connect(configurationOptions, out isOssCluster); + } + + private ConnectionMultiplexer Connect(ConfigurationOptions configurationOptions, out bool isOssCluster) { // Redis Cluster if (redisCluster != null && numRedisClusterNodesEnv != null) @@ -22,35 +43,23 @@ public RedisFixture() string host = parts[0]; int startPort = int.Parse(parts[1]); - var endpoints = new EndPointCollection(); + configurationOptions.EndPoints.Clear(); int numRedisClusterNodes = int.Parse(numRedisClusterNodesEnv!); for (int i = 0; i < numRedisClusterNodes; i++) { - endpoints.Add(host, startPort + i); + configurationOptions.EndPoints.Add(host, startPort + i); } - - ConfigurationOptions clusterConfig = new ConfigurationOptions - { - EndPoints = endpoints, - AsyncTimeout = 10000, - SyncTimeout = 10000 - }; - - isOSSCluster = true; - Redis = ConnectionMultiplexer.Connect(clusterConfig); + isOssCluster = true; + return ConnectionMultiplexer.Connect(configurationOptions); } // Redis Standalone - else - Redis = ConnectionMultiplexer.Connect($"{redisStandalone}"); - } + configurationOptions.EndPoints.Clear(); + configurationOptions.EndPoints.Add($"{redisStandalone}"); - public void Dispose() - { - Redis.Close(); + isOssCluster = false; + return ConnectionMultiplexer.Connect(configurationOptions); } - - public ConnectionMultiplexer Redis { get; } } } \ No newline at end of file From d73741b1b610722748fc88548ca6dc5d1111384e Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Mon, 19 Feb 2024 16:41:03 +0200 Subject: [PATCH 8/8] Remove ToString, not really needed --- .../CoreCommands/DataTypes/RedisValueWithScore.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/NRedisStack/CoreCommands/DataTypes/RedisValueWithScore.cs b/src/NRedisStack/CoreCommands/DataTypes/RedisValueWithScore.cs index f3dbb550..9ad3e34d 100644 --- a/src/NRedisStack/CoreCommands/DataTypes/RedisValueWithScore.cs +++ b/src/NRedisStack/CoreCommands/DataTypes/RedisValueWithScore.cs @@ -28,12 +28,4 @@ public RedisValueWithScore(RedisValue value, double score) /// ZADD my-set 5.1 my-value, the score is 5.1. /// public double Score { get; } - - /// - /// ToString override. - /// - public override string ToString() - { - return $"{Value}: {Score}"; - } }