Skip to content

Commit fc53b3a

Browse files
committed
Add support for the BLMPOP command
Issue redis#248
1 parent 640158b commit fc53b3a

File tree

6 files changed

+209
-7
lines changed

6 files changed

+209
-7
lines changed

src/NRedisStack/CoreCommands/CoreCommandBuilder.cs

+23
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,29 @@ public static SerializedCommand BzPopMax(RedisKey[] keys, double timeout)
5454
return BlockingCommandWithKeysAndTimeout(RedisCoreCommands.BZPOPMAX, keys, timeout);
5555
}
5656

57+
public static SerializedCommand BlmPop(double timeout, RedisKey[] keys, ListSide listSide, long? count)
58+
{
59+
if (keys.Length == 0)
60+
{
61+
throw new ArgumentException("At least one key must be provided.");
62+
}
63+
64+
List<object> args = new List<object>();
65+
66+
args.Add(timeout);
67+
args.Add(keys.Length);
68+
args.AddRange(keys.Cast<object>());
69+
args.Add(listSide == ListSide.Left ? CoreArgs.LEFT : CoreArgs.RIGHT);
70+
71+
if (count != null)
72+
{
73+
args.Add(CoreArgs.COUNT);
74+
args.Add(count);
75+
}
76+
77+
return new SerializedCommand(RedisCoreCommands.BLMPOP, args);
78+
}
79+
5780
public static SerializedCommand BlPop(RedisKey[] keys, double timeout)
5881
{
5982
return BlockingCommandWithKeysAndTimeout(RedisCoreCommands.BLPOP, keys, timeout);

src/NRedisStack/CoreCommands/CoreCommands.cs

+55
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,61 @@ public static bool ClientSetInfo(this IDatabase db, SetInfoAttr attr, string val
176176
return BzPopMax(db, new[] { key }, timeout);
177177
}
178178

179+
/// <summary>
180+
/// The BLMPOP command.
181+
/// <p/>
182+
/// Removes and returns up to <paramref name="count"/> entries from the first non-empty list in
183+
/// <paramref name="keys"/>. If none of the lists contain elements, the call blocks on the server until elements
184+
/// become available, or the given <paramref name="timeout"/> expires. A <paramref name="timeout"/> of <c>0</c>
185+
/// means to wait indefinitely server-side. Returns <c>null</c> if the server timeout expires.
186+
/// <p/>
187+
/// When using this, pay attention to the timeout configured in the client, on the
188+
/// <see cref="ConnectionMultiplexer"/>, which by default can be too small:
189+
/// <code>
190+
/// ConfigurationOptions configurationOptions = new ConfigurationOptions();
191+
/// configurationOptions.SyncTimeout = 120000; // set a meaningful value here
192+
/// configurationOptions.EndPoints.Add("localhost");
193+
/// ConnectionMultiplexer redis = ConnectionMultiplexer.Connect(configurationOptions);
194+
/// </code>
195+
/// If the connection multiplexer timeout expires in the client, a <c>StackExchange.Redis.RedisTimeoutException</c>
196+
/// is thrown.
197+
/// <p/>
198+
/// This is an extension method added to the <see cref="IDatabase"/> class, for convenience.
199+
/// </summary>
200+
/// <param name="db">The <see cref="IDatabase"/> class where this extension method is applied.</param>
201+
/// <param name="timeout">Server-side timeout for the wait. A value of <c>0</c> means to wait indefinitely.</param>
202+
/// <param name="keys">The keys to check.</param>
203+
/// <param name="listSide">Specify from which end of the list to pop values: left or right.</param>
204+
/// <param name="count">The maximum number of records to pop. If set to <c>null</c> then the server default
205+
/// will be used.</param>
206+
/// <returns>A collection of values, together with the key they were popped from, or <c>null</c> if the
207+
/// server timeout expires.</returns>
208+
/// <remarks><seealso href="https://redis.io/commands/blmpop"/></remarks>
209+
public static Tuple<RedisKey, List<RedisValue>>? BlmPop(this IDatabase db, double timeout, RedisKey[] keys, ListSide listSide, long? count = null)
210+
{
211+
var command = CoreCommandBuilder.BlmPop(timeout, keys, listSide, count);
212+
return db.Execute(command).ToListPopResults();
213+
}
214+
215+
/// <summary>
216+
/// Syntactic sugar for
217+
/// <see cref="BlmPop(StackExchange.Redis.IDatabase,double,StackExchange.Redis.RedisKey[],StackExchange.Redis.ListSide,System.Nullable{long})"/>,
218+
/// where only one key is used.
219+
/// </summary>
220+
/// <param name="db">The <see cref="IDatabase"/> class where this extension method is applied.</param>
221+
/// <param name="timeout">Server-side timeout for the wait. A value of <c>0</c> means to wait indefinitely.</param>
222+
/// <param name="key">The key to check.</param>
223+
/// <param name="listSide">Specify from which end of the list to pop values: left or right.</param>
224+
/// <param name="count">The maximum number of records to pop. If set to <c>null</c> then the server default
225+
/// will be used.</param>
226+
/// <returns>A collection of values, together with the key they were popped from, or <c>null</c> if the
227+
/// server timeout expires.</returns>
228+
/// <remarks><seealso href="https://redis.io/commands/blmpop"/></remarks>
229+
public static Tuple<RedisKey, List<RedisValue>>? BlmPop(this IDatabase db, double timeout, RedisKey key, ListSide listSide, long? count = null)
230+
{
231+
return BlmPop(db, timeout, new[] { key }, listSide, count);
232+
}
233+
179234
/// <summary>
180235
/// The BLPOP command.
181236
/// <p/>

src/NRedisStack/CoreCommands/Literals/CommandArgs.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ namespace NRedisStack.Core.Literals
33
internal static class CoreArgs
44
{
55
public const string COUNT = "COUNT";
6-
public const string lib_name = "LIB-NAME";
7-
public const string lib_ver = "LIB-VER";
6+
public const string LEFT = "LEFT";
87
public const string MAX = "MAX";
98
public const string MIN = "MIN";
9+
public const string RIGHT = "RIGHT";
10+
public const string lib_name = "LIB-NAME";
11+
public const string lib_ver = "LIB-VER";
1012
}
1113
}

src/NRedisStack/CoreCommands/Literals/Commands.cs

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace NRedisStack.Core.Literals
55
/// </summary>
66
internal static class RedisCoreCommands
77
{
8+
public const string BLMPOP = "BLMPOP";
89
public const string BLPOP = "BLPOP";
910
public const string BRPOP = "BRPOP";
1011
public const string BZMPOP = "BZMPOP";

src/NRedisStack/ResponseParser.cs

+22
Original file line numberDiff line numberDiff line change
@@ -780,5 +780,27 @@ public static Dictionary<string, RedisResult>[] ToDictionarys(this RedisResult r
780780

781781
return new Tuple<RedisKey, RedisValue>(resultKey, value);
782782
}
783+
784+
public static Tuple<RedisKey, List<RedisValue>>? ToListPopResults(this RedisResult result)
785+
{
786+
if (result.IsNull)
787+
{
788+
return null;
789+
}
790+
791+
var resultArray = (RedisResult[])result!;
792+
var resultKey = resultArray[0].ToRedisKey();
793+
var resultSetItems = resultArray[1].ToArray();
794+
795+
List<RedisValue> values = new List<RedisValue>();
796+
797+
foreach (var resultSetItem in resultSetItems)
798+
{
799+
var value = (RedisValue)resultSetItem!;
800+
values.Add(value);
801+
}
802+
803+
return new Tuple<RedisKey, List<RedisValue>>(resultKey, values);
804+
}
783805
}
784806
}

tests/NRedisStack.Tests/Core Commands/CoreTests.cs

+104-5
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ public void TestBzmPop()
153153
db.SortedSetAdd(sortedSetKey, "d", 9.4);
154154
db.SortedSetAdd(sortedSetKey, "e", 7.76);
155155

156-
// Pop two items with default order, which means it will pop the minimum values.
156+
// Pop two items with Min modifier, which means it will pop the minimum values.
157157
var resultWithDefaultOrder = db.BzmPop(0, sortedSetKey, MinMaxModifier.Min, 2);
158158

159159
Assert.NotNull(resultWithDefaultOrder);
@@ -162,7 +162,7 @@ public void TestBzmPop()
162162
Assert.Equal("a", resultWithDefaultOrder.Item2[0].Value.ToString());
163163
Assert.Equal("c", resultWithDefaultOrder.Item2[1].Value.ToString());
164164

165-
// Pop one more item, with descending order, which means it will pop the maximum value.
165+
// Pop one more item, with Max modifier, which means it will pop the maximum value.
166166
var resultWithDescendingOrder = db.BzmPop(0, sortedSetKey, MinMaxModifier.Max, 1);
167167

168168
Assert.NotNull(resultWithDescendingOrder);
@@ -252,7 +252,6 @@ public void TestBzmPopNoKeysProvided()
252252
var db = redis.GetDatabase(null);
253253
db.Execute("FLUSHALL");
254254

255-
// Server would wait forever, but the multiplexer times out in 1 second.
256255
Assert.Throws<ArgumentException>(() => db.BzmPop(0, Array.Empty<RedisKey>(), MinMaxModifier.Min));
257256
}
258257

@@ -270,15 +269,15 @@ public void TestBzmPopWithOrderEnum()
270269
db.SortedSetAdd(sortedSetKey, "b", 5.1);
271270
db.SortedSetAdd(sortedSetKey, "c", 3.7);
272271

273-
// Pop two items with default order, which means it will pop the minimum values.
272+
// Pop two items with Ascending order, which means it will pop the minimum values.
274273
var resultWithDefaultOrder = db.BzmPop(0, sortedSetKey, Order.Ascending.ToMinMax());
275274

276275
Assert.NotNull(resultWithDefaultOrder);
277276
Assert.Equal(sortedSetKey, resultWithDefaultOrder!.Item1);
278277
Assert.Single(resultWithDefaultOrder.Item2);
279278
Assert.Equal("a", resultWithDefaultOrder.Item2[0].Value.ToString());
280279

281-
// Pop one more item, with descending order, which means it will pop the maximum value.
280+
// Pop one more item, with Descending order, which means it will pop the maximum value.
282281
var resultWithDescendingOrder = db.BzmPop(0, sortedSetKey, Order.Descending.ToMinMax());
283282

284283
Assert.NotNull(resultWithDescendingOrder);
@@ -406,6 +405,106 @@ public void TestBzPopMaxMultipleSets()
406405
Assert.Equal("set-one", result!.Item1);
407406
Assert.Equal("b", result.Item2.Value.ToString());
408407
}
408+
[SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")]
409+
public void TestBlmPop()
410+
{
411+
var redis = ConnectionMultiplexer.Connect("localhost");
412+
413+
var db = redis.GetDatabase(null);
414+
db.Execute("FLUSHALL");
415+
416+
db.ListRightPush("my-list", "a");
417+
db.ListRightPush("my-list", "b");
418+
db.ListRightPush("my-list", "c");
419+
db.ListRightPush("my-list", "d");
420+
db.ListRightPush("my-list", "e");
421+
422+
// Pop two items from the left side.
423+
var resultWithDefaultOrder = db.BlmPop(0, "my-list", ListSide.Left, 2);
424+
425+
Assert.NotNull(resultWithDefaultOrder);
426+
Assert.Equal("my-list", resultWithDefaultOrder!.Item1);
427+
Assert.Equal(2, resultWithDefaultOrder.Item2.Count);
428+
Assert.Equal("a", resultWithDefaultOrder.Item2[0].ToString());
429+
Assert.Equal("b", resultWithDefaultOrder.Item2[1].ToString());
430+
431+
// Pop one more item, from the right side.
432+
var resultWithDescendingOrder = db.BlmPop(0, "my-list", ListSide.Right, 1);
433+
434+
Assert.NotNull(resultWithDescendingOrder);
435+
Assert.Equal("my-list", resultWithDescendingOrder!.Item1);
436+
Assert.Single(resultWithDescendingOrder.Item2);
437+
Assert.Equal("e", resultWithDescendingOrder.Item2[0].ToString());
438+
}
439+
440+
[SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")]
441+
public void TestBlmPopNull()
442+
{
443+
var redis = ConnectionMultiplexer.Connect("localhost");
444+
445+
var db = redis.GetDatabase(null);
446+
db.Execute("FLUSHALL");
447+
448+
// Nothing in the list, and a short server timeout, which yields null.
449+
var result = db.BlmPop(0.5, "my-list", ListSide.Left);
450+
451+
Assert.Null(result);
452+
}
453+
454+
[SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")]
455+
public void TestBlmPopMultipleLists()
456+
{
457+
var redis = ConnectionMultiplexer.Connect("localhost");
458+
459+
var db = redis.GetDatabase(null);
460+
db.Execute("FLUSHALL");
461+
462+
db.ListRightPush("list-one", "a");
463+
db.ListRightPush("list-one", "b");
464+
db.ListRightPush("list-one", "c");
465+
db.ListRightPush("list-two", "d");
466+
db.ListRightPush("list-two", "e");
467+
468+
var result = db.BlmPop(0, "list-two", ListSide.Right);
469+
470+
Assert.NotNull(result);
471+
Assert.Equal("list-two", result!.Item1);
472+
Assert.Single(result.Item2);
473+
Assert.Equal("e", result.Item2[0].ToString());
474+
475+
result = db.BlmPop(0, new[] { new RedisKey("list-two"), new RedisKey("list-one") }, ListSide.Left);
476+
477+
Assert.NotNull(result);
478+
Assert.Equal("list-two", result!.Item1);
479+
Assert.Single(result.Item2);
480+
Assert.Equal("d", result.Item2[0].ToString());
481+
482+
result = db.BlmPop(0, new[] { new RedisKey("list-two"), new RedisKey("list-one") }, ListSide.Right);
483+
484+
Assert.NotNull(result);
485+
Assert.Equal("list-one", result!.Item1);
486+
Assert.Single(result.Item2);
487+
Assert.Equal("c", result.Item2[0].ToString());
488+
489+
result = db.BlmPop(0, "list-one", ListSide.Left, count: 2);
490+
491+
Assert.NotNull(result);
492+
Assert.Equal("list-one", result!.Item1);
493+
Assert.Equal(2, result.Item2.Count);
494+
Assert.Equal("a", result.Item2[0].ToString());
495+
Assert.Equal("b", result.Item2[1].ToString());
496+
}
497+
498+
[SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")]
499+
public void TestBlmPopNoKeysProvided()
500+
{
501+
var redis = ConnectionMultiplexer.Connect("localhost");
502+
503+
var db = redis.GetDatabase(null);
504+
db.Execute("FLUSHALL");
505+
506+
Assert.Throws<ArgumentException>(() => db.BzmPop(0, Array.Empty<RedisKey>(), MinMaxModifier.Min));
507+
}
409508

410509
[SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")]
411510
public void TestBlPop()

0 commit comments

Comments
 (0)