Skip to content

Commit 4aafc50

Browse files
author
Gabriel Erzse
committed
Add support for BZMPOP (#233)
Add support for the BZMPOP command. This command is blocking on the server, so it goes against the current policy of the StackExchange.Redis library. Therefore make it obvious in the code documentation that attention must be given to the timeout in the connection multiplexer. The StackExchange.Redis library already defines a type for the payload returned by BZMPOP (which is the same as for ZMPOP), namely the SortedSetPopResult class. However, the constructor of that class is internal in the library, so we can't create instances of it. Therefore roll our out type for a <value, score> pair, and use Tuple to pair a key with a list of such <value, score> pairs.
1 parent e6da72f commit 4aafc50

File tree

8 files changed

+341
-5
lines changed

8 files changed

+341
-5
lines changed

src/NRedisStack/CoreCommands/CoreCommandBuilder.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using NRedisStack.RedisStackCommands;
22
using NRedisStack.Core.Literals;
33
using NRedisStack.Core;
4+
using NRedisStack.Core.DataTypes;
5+
using StackExchange.Redis;
46

57
namespace NRedisStack
68
{
@@ -18,5 +20,28 @@ public static SerializedCommand ClientSetInfo(SetInfoAttr attr, string value)
1820

1921
return new SerializedCommand(RedisCoreCommands.CLIENT, RedisCoreCommands.SETINFO, attrValue, value);
2022
}
23+
24+
public static SerializedCommand BzmPop(int timeout, RedisKey[] keys, MinMaxModifier minMaxModifier, long? count)
25+
{
26+
if (keys.Length == 0)
27+
{
28+
throw new ArgumentException("At least one key must be provided.");
29+
}
30+
31+
List<object> args = new List<object>();
32+
33+
args.Add(timeout);
34+
args.Add(keys.Length);
35+
args.AddRange(keys.Cast<object>());
36+
args.Add(minMaxModifier == MinMaxModifier.Min ? CoreArgs.MIN : CoreArgs.MAX);
37+
38+
if (count != null)
39+
{
40+
args.Add(CoreArgs.COUNT);
41+
args.Add(count);
42+
}
43+
44+
return new SerializedCommand(RedisCoreCommands.BZMPOP, args);
45+
}
2146
}
2247
}

src/NRedisStack/CoreCommands/CoreCommands.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using NRedisStack.Core;
2+
using NRedisStack.Core.DataTypes;
23
using StackExchange.Redis;
4+
35
namespace NRedisStack
46
{
57

@@ -19,5 +21,61 @@ public static bool ClientSetInfo(this IDatabase db, SetInfoAttr attr, string val
1921
return false;
2022
return db.Execute(CoreCommandBuilder.ClientSetInfo(attr, value)).OKtoBoolean();
2123
}
24+
25+
/// <summary>
26+
/// The BZMPOP command.
27+
/// <p/>
28+
/// Removes and returns up to <paramref name="count"/> entries from the first non-empty sorted set in
29+
/// <paramref name="keys"/>. If none of the sets contain elements, the call blocks on the server until elements
30+
/// become available or the given <paramref name="timeout"/> passes. A <paramref name="timeout"/> of <c>0</c>
31+
/// means to wait indefinitely server-side. Returns <c>null</c> if the server timeout expires.
32+
/// <p/>
33+
/// When using this, pay attention to the timeout configured on the <see cref="ConnectionMultiplexer"/>, which
34+
/// by default can be too small, in which case you want to increase it:
35+
/// <code>
36+
/// ConfigurationOptions configurationOptions = new ConfigurationOptions();
37+
/// configurationOptions.SyncTimeout = 120000; // set a meaningful value here
38+
/// configurationOptions.EndPoints.Add("localhost");
39+
/// ConnectionMultiplexer redis = ConnectionMultiplexer.Connect(configurationOptions);
40+
/// </code>
41+
/// If the connection multiplexer timeout expires, a <c>StackExchange.Redis.RedisTimeoutException</c> will be
42+
/// thrown.
43+
/// <p/>
44+
/// This is an extension method added to the <see cref="IDatabase"/> class, for convenience.
45+
/// </summary>
46+
/// <param name="db">The <see cref="IDatabase"/> class where this extension method is applied.</param>
47+
/// <param name="timeout">Server-side timeout for the wait. A value of <c>0</c> means to wait indefinitely.</param>
48+
/// <param name="keys">The keys to check.</param>
49+
/// <param name="minMaxModifier">Specify from which end of the sorted set to pop values. If set to <c>MinMaxModifier.Min</c>
50+
/// then the minimum elements will be popped, otherwise the maximum values.</param>
51+
/// <param name="count">The maximum number of records to pop out. If set to <c>null</c> then the server default
52+
/// will be used.</param>
53+
/// <returns>A collection of sorted set entries paired with their scores, together with the key they were popped
54+
/// from, or <c>null</c> if the server timeout expires.</returns>
55+
/// <remarks><seealso href="https://redis.io/commands/bzmpop"/></remarks>
56+
public static Tuple<RedisKey, List<RedisValueWithScore>>? BzmPop(this IDatabase db, int timeout, RedisKey[] keys, MinMaxModifier minMaxModifier, long? count = null)
57+
{
58+
var command = CoreCommandBuilder.BzmPop(timeout, keys, minMaxModifier, count);
59+
return db.Execute(command).ToSortedSetPopResult();
60+
}
61+
62+
/// <summary>
63+
/// Syntactic sugar for <see cref="BzmPop(StackExchange.Redis.IDatabase,int,StackExchange.Redis.RedisKey[],StackExchange.Redis.Order,System.Nullable{long})"/>,
64+
/// where only one key is used.
65+
/// </summary>
66+
/// <param name="db">The <see cref="IDatabase"/> class where this extension method is applied.</param>
67+
/// <param name="timeout">Server-side timeout for the wait. A value of <c>0</c> means to wait indefinitely.</param>
68+
/// <param name="key">The key to check.</param>
69+
/// <param name="minMaxModifier">Specify from which end of the sorted set to pop values. If set to <c>MinMaxModifier.Min</c>
70+
/// then the minimum elements will be popped, otherwise the maximum values.</param>
71+
/// <param name="count">The maximum number of records to pop out. If set to <c>null</c> then the server default
72+
/// will be used.</param>
73+
/// <returns>A collection of sorted set entries paired with their scores, together with the key they were popped
74+
/// from, or <c>null</c> if the server timeout expires.</returns>
75+
/// <remarks><seealso href="https://redis.io/commands/bzmpop"/></remarks>
76+
public static Tuple<RedisKey, List<RedisValueWithScore>>? BzmPop(this IDatabase db, int timeout, RedisKey key, MinMaxModifier minMaxModifier, long? count = null)
77+
{
78+
return BzmPop(db, timeout, new[] { key }, minMaxModifier, count);
79+
}
2280
}
2381
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using StackExchange.Redis;
2+
3+
namespace NRedisStack.Core.DataTypes;
4+
5+
/// <summary>
6+
/// Modifier that can be used for sorted set commands, where a MIN/MAX argument is expected by the Redis server.
7+
/// </summary>
8+
public enum MinMaxModifier
9+
{
10+
/// <summary>
11+
/// Maps to the <c>MIN</c> argument on the Redis server.
12+
/// </summary>
13+
Min,
14+
15+
/// <summary>
16+
/// Maps to the <c>MAX</c> argument on the Redis server.
17+
/// </summary>
18+
Max
19+
}
20+
21+
/// <summary>
22+
/// Conversion methods from/to other common data types.
23+
/// </summary>
24+
public static class MinMaxModifierExtensions
25+
{
26+
/// <summary>
27+
/// Convert from <see cref="Order"/> to <see cref="MinMaxModifier"/>.
28+
/// </summary>
29+
public static MinMaxModifier ToMinMax(this Order order) => order switch
30+
{
31+
Order.Ascending => MinMaxModifier.Min,
32+
Order.Descending => MinMaxModifier.Max,
33+
_ => throw new ArgumentOutOfRangeException(nameof(order))
34+
};
35+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using StackExchange.Redis;
2+
3+
namespace NRedisStack.Core.DataTypes;
4+
5+
/// <summary>
6+
/// Holds a <see cref="RedisValue"/> with an associated score.
7+
/// Used when working with sorted sets.
8+
/// </summary>
9+
public struct RedisValueWithScore
10+
{
11+
/// <summary>
12+
/// Pair a <see cref="RedisValue"/> with a numeric score.
13+
/// </summary>
14+
public RedisValueWithScore(RedisValue value, double score)
15+
{
16+
Value = value;
17+
Score = score;
18+
}
19+
20+
/// <summary>
21+
/// The value of an item stored in a sorted set. For example, in the Redis command
22+
/// <c>ZADD my-set 5.1 my-value</c>, the value is <c>my-value</c>.
23+
/// </summary>
24+
public RedisValue Value { get; }
25+
26+
/// <summary>
27+
/// The score of an item stored in a sorted set. For example, in the Redis command
28+
/// <c>ZADD my-set 5.1 my-value</c>, the score is <c>5.1</c>.
29+
/// </summary>
30+
public double Score { get; }
31+
}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
namespace NRedisStack.Core.Literals
22
{
3-
internal class CoreArgs
3+
internal static class CoreArgs
44
{
5+
public const string COUNT = "COUNT";
56
public const string lib_name = "LIB-NAME";
67
public const string lib_ver = "LIB-VER";
8+
public const string MAX = "MAX";
9+
public const string MIN = "MIN";
710
}
811
}

src/NRedisStack/CoreCommands/Literals/Commands.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ namespace NRedisStack.Core.Literals
33
/// <summary>
44
/// Redis Core command literals
55
/// </summary>
6-
internal class RedisCoreCommands
6+
internal static class RedisCoreCommands
77
{
8+
public const string BZMPOP = "BZMPOP";
89
public const string CLIENT = "CLIENT";
910
public const string SETINFO = "SETINFO";
1011
}

src/NRedisStack/ResponseParser.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using NRedisStack.Extensions;
44
using StackExchange.Redis;
55
using NRedisStack.Bloom.DataTypes;
6+
using NRedisStack.Core.DataTypes;
67
using NRedisStack.CuckooFilter.DataTypes;
78
using NRedisStack.CountMinSketch.DataTypes;
89
using NRedisStack.TopK.DataTypes;
@@ -84,6 +85,16 @@ public static TimeStamp ToTimeStamp(this RedisResult result)
8485
return new TimeStamp((long)result);
8586
}
8687

88+
public static RedisKey ToRedisKey(this RedisResult result)
89+
{
90+
return new RedisKey(result.ToString());
91+
}
92+
93+
public static RedisValue ToRedisValue(this RedisResult result)
94+
{
95+
return new RedisValue(result.ToString());
96+
}
97+
8798
public static IReadOnlyList<TimeStamp> ToTimeStampArray(this RedisResult result)
8899
{
89100
RedisResult[] redisResults = (RedisResult[])result!;
@@ -715,5 +726,29 @@ public static Dictionary<string, RedisResult>[] ToDictionarys(this RedisResult r
715726
return dicts;
716727

717728
}
729+
730+
public static Tuple<RedisKey, List<RedisValueWithScore>>? ToSortedSetPopResult(this RedisResult result)
731+
{
732+
if (result.IsNull)
733+
{
734+
return null;
735+
}
736+
737+
var resultArray = (RedisResult[])result!;
738+
var resultKey = resultArray[0].ToRedisKey();
739+
var resultSetItems = resultArray[1].ToArray();
740+
741+
List<RedisValueWithScore> valuesWithScores = new List<RedisValueWithScore>();
742+
743+
foreach (var resultSetItem in resultSetItems)
744+
{
745+
var resultSetItemArray = (RedisResult[])resultSetItem!;
746+
var value = resultSetItemArray[0].ToRedisValue();
747+
var score = resultSetItemArray[1].ToDouble();
748+
valuesWithScores.Add(new RedisValueWithScore(value, score));
749+
}
750+
751+
return new Tuple<RedisKey, List<RedisValueWithScore>>(resultKey, valuesWithScores);
752+
}
718753
}
719754
}

0 commit comments

Comments
 (0)