Skip to content

Commit fe18a3b

Browse files
authored
Add support for BZMPOP, BZPOPMIN and BZPOPMAX (#240)
* Formatting and typos Fix some typos in the markdown files and run `dotnet format` on existing code. * Include documentation in the build Change the settings so that documentation is included in the build. * Add support for BZMPOP, BZPOPMIN and BZPOPMAX Add support for the BZMPOP, BZPOPMIN, BZPOPMAX commands. Issues #232, #233 and #234. These commands are blocking on the server, so they go 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, client-side. 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. Instead of using Order to signal from which end of the sorted set to pop, define a MinMaxModifier enum, which more clearly expresses the intention and maps directly to the Redis command being executed. --------- Co-authored-by: Gabriel Erzse <[email protected]>
1 parent 9a607e0 commit fe18a3b

File tree

11 files changed

+615
-11
lines changed

11 files changed

+615
-11
lines changed

CONTRIBUTING.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Here's how to get started with your code contribution:
3535
3. Write your tests
3636

3737
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.
38-
5. Run dotnet format to make sure your code is formatted
38+
5. Run `dotnet format` to make sure your code is formatted
3939
6. Make sure your tests pass using `dotnet test`
4040
7. Open a pull request
4141

@@ -121,6 +121,7 @@ e.g. :
121121
```bash
122122
dotnet test --environment "REDIS_CLUSTER=127.0.0.1:16379" --environment "NUM_REDIS_CLUSTER_NODES=6"
123123
```
124+
124125
## How to Report a Bug
125126

126127
### Security Vulnerabilities
@@ -145,7 +146,7 @@ issue, so if you're unsure, just email [us](mailto:[email protected]).
145146
When filing an issue, make sure to answer these five questions:
146147

147148
1. What version of NRedisStack are you using?
148-
2. What version of redis are you using?
149+
2. What version of Redis are you using?
149150
3. What did you do?
150151
4. What did you expect to see?
151152
5. What did you see instead?

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,17 @@ This project builds on [StackExchange.Redis](https://github.com/StackExchange/St
3131
The complete documentation for Redis module commands can be found at the [Redis commands website](https://redis.io/commands/).
3232

3333
### Redis OSS commands
34+
3435
You can use Redis OSS commands in the same way as you use them in [StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis).
3536

3637
### Stack commands
38+
3739
Each module has a command class with its own commands.
40+
3841
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).
3942

4043
**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/).
4144

42-
4345
# Usage
4446

4547
## 💻 Installation

src/NRedisStack/CoreCommands/CoreCommandBuilder.cs

+53
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,56 @@ 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(double 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+
}
46+
47+
public static SerializedCommand BzPopMin(RedisKey[] keys, double timeout)
48+
{
49+
if (keys.Length == 0)
50+
{
51+
throw new ArgumentException("At least one key must be provided.");
52+
}
53+
54+
List<object> args = new List<object>();
55+
args.AddRange(keys.Cast<object>());
56+
args.Add(timeout);
57+
58+
return new SerializedCommand(RedisCoreCommands.BZPOPMIN, args);
59+
}
60+
61+
public static SerializedCommand BzPopMax(RedisKey[] keys, double timeout)
62+
{
63+
if (keys.Length == 0)
64+
{
65+
throw new ArgumentException("At least one key must be provided.");
66+
}
67+
68+
List<object> args = new List<object>();
69+
args.AddRange(keys.Cast<object>());
70+
args.Add(timeout);
71+
72+
return new SerializedCommand(RedisCoreCommands.BZPOPMAX, args);
73+
}
2174
}
2275
}

src/NRedisStack/CoreCommands/CoreCommands.cs

+156
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,159 @@ 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"/> expires. 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 in the client, on the
34+
/// <see cref="ConnectionMultiplexer"/>, which by default can be too small:
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 in the client, a <c>StackExchange.Redis.RedisTimeoutException</c>
42+
/// is 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, double timeout, RedisKey[] keys, MinMaxModifier minMaxModifier, long? count = null)
57+
{
58+
var command = CoreCommandBuilder.BzmPop(timeout, keys, minMaxModifier, count);
59+
return db.Execute(command).ToSortedSetPopResults();
60+
}
61+
62+
/// <summary>
63+
/// Syntactic sugar for
64+
/// <see cref="BzmPop(StackExchange.Redis.IDatabase,double,StackExchange.Redis.RedisKey[],NRedisStack.Core.DataTypes.MinMaxModifier,System.Nullable{long})"/>,
65+
/// where only one key is used.
66+
/// </summary>
67+
/// <param name="db">The <see cref="IDatabase"/> class where this extension method is applied.</param>
68+
/// <param name="timeout">Server-side timeout for the wait. A value of <c>0</c> means to wait indefinitely.</param>
69+
/// <param name="key">The key to check.</param>
70+
/// <param name="minMaxModifier">Specify from which end of the sorted set to pop values. If set to <c>MinMaxModifier.Min</c>
71+
/// then the minimum elements will be popped, otherwise the maximum values.</param>
72+
/// <param name="count">The maximum number of records to pop out. If set to <c>null</c> then the server default
73+
/// will be used.</param>
74+
/// <returns>A collection of sorted set entries paired with their scores, together with the key they were popped
75+
/// from, or <c>null</c> if the server timeout expires.</returns>
76+
/// <remarks><seealso href="https://redis.io/commands/bzmpop"/></remarks>
77+
public static Tuple<RedisKey, List<RedisValueWithScore>>? BzmPop(this IDatabase db, double timeout, RedisKey key, MinMaxModifier minMaxModifier, long? count = null)
78+
{
79+
return BzmPop(db, timeout, new[] { key }, minMaxModifier, count);
80+
}
81+
82+
/// <summary>
83+
/// The BZPOPMIN command.
84+
/// <p/>
85+
/// Removes and returns the entry with the smallest score from the first non-empty sorted set in
86+
/// <paramref name="keys"/>. If none of the sets contain elements, the call blocks on the server until elements
87+
/// become available, or the given <paramref name="timeout"/> expires. A <paramref name="timeout"/> of <c>0</c>
88+
/// means to wait indefinitely server-side. Returns <c>null</c> if the server timeout expires.
89+
/// <p/>
90+
/// When using this, pay attention to the timeout configured in the client, on the
91+
/// <see cref="ConnectionMultiplexer"/>, which by default can be too small:
92+
/// <code>
93+
/// ConfigurationOptions configurationOptions = new ConfigurationOptions();
94+
/// configurationOptions.SyncTimeout = 120000; // set a meaningful value here
95+
/// configurationOptions.EndPoints.Add("localhost");
96+
/// ConnectionMultiplexer redis = ConnectionMultiplexer.Connect(configurationOptions);
97+
/// </code>
98+
/// If the connection multiplexer timeout expires in the client, a <c>StackExchange.Redis.RedisTimeoutException</c>
99+
/// is thrown.
100+
/// <p/>
101+
/// This is an extension method added to the <see cref="IDatabase"/> class, for convenience.
102+
/// </summary>
103+
/// <param name="db">The <see cref="IDatabase"/> class where this extension method is applied.</param>
104+
/// <param name="keys">The keys to check.</param>
105+
/// <param name="timeout">Server-side timeout for the wait. A value of <c>0</c> means to wait indefinitely.</param>
106+
/// <returns>A sorted set entry paired with its score, together with the key it was popped from, or <c>null</c>
107+
/// if the server timeout expires.</returns>
108+
/// <remarks><seealso href="https://redis.io/commands/bzpopmin"/></remarks>
109+
public static Tuple<RedisKey, RedisValueWithScore>? BzPopMin(this IDatabase db, RedisKey[] keys, double timeout)
110+
{
111+
var command = CoreCommandBuilder.BzPopMin(keys, timeout);
112+
return db.Execute(command).ToSortedSetPopResult();
113+
}
114+
115+
/// <summary>
116+
/// Syntactic sugar for <see cref="BzPopMin(StackExchange.Redis.IDatabase,StackExchange.Redis.RedisKey[],double)"/>,
117+
/// where only one key is used.
118+
/// </summary>
119+
/// <param name="db">The <see cref="IDatabase"/> class where this extension method is applied.</param>
120+
/// <param name="key">The key to check.</param>
121+
/// <param name="timeout">Server-side timeout for the wait. A value of <c>0</c> means to wait indefinitely.</param>
122+
/// <returns>A sorted set entry paired with its score, together with the key it was popped from, or <c>null</c>
123+
/// if the server timeout expires.</returns>
124+
/// <remarks><seealso href="https://redis.io/commands/bzpopmin"/></remarks>
125+
public static Tuple<RedisKey, RedisValueWithScore>? BzPopMin(this IDatabase db, RedisKey key, double timeout)
126+
{
127+
return BzPopMin(db, new[] { key }, timeout);
128+
}
129+
130+
131+
/// <summary>
132+
/// The BZPOPMAX command.
133+
/// <p/>
134+
/// Removes and returns the entry with the highest score from the first non-empty sorted set in
135+
/// <paramref name="keys"/>. If none of the sets contain elements, the call blocks on the server until elements
136+
/// become available, or the given <paramref name="timeout"/> expires. A <paramref name="timeout"/> of <c>0</c>
137+
/// means to wait indefinitely server-side. Returns <c>null</c> if the server timeout expires.
138+
/// <p/>
139+
/// When using this, pay attention to the timeout configured in the client, on the
140+
/// <see cref="ConnectionMultiplexer"/>, which by default can be too small:
141+
/// <code>
142+
/// ConfigurationOptions configurationOptions = new ConfigurationOptions();
143+
/// configurationOptions.SyncTimeout = 120000; // set a meaningful value here
144+
/// configurationOptions.EndPoints.Add("localhost");
145+
/// ConnectionMultiplexer redis = ConnectionMultiplexer.Connect(configurationOptions);
146+
/// </code>
147+
/// If the connection multiplexer timeout expires in the client, a <c>StackExchange.Redis.RedisTimeoutException</c>
148+
/// is thrown.
149+
/// <p/>
150+
/// This is an extension method added to the <see cref="IDatabase"/> class, for convenience.
151+
/// </summary>
152+
/// <param name="db">The <see cref="IDatabase"/> class where this extension method is applied.</param>
153+
/// <param name="keys">The keys to check.</param>
154+
/// <param name="timeout">Server-side timeout for the wait. A value of <c>0</c> means to wait indefinitely.</param>
155+
/// <returns>A sorted set entry paired with its score, together with the key it was popped from, or <c>null</c>
156+
/// if the server timeout expires.</returns>
157+
/// <remarks><seealso href="https://redis.io/commands/bzpopmax"/></remarks>
158+
public static Tuple<RedisKey, RedisValueWithScore>? BzPopMax(this IDatabase db, RedisKey[] keys, double timeout)
159+
{
160+
var command = CoreCommandBuilder.BzPopMax(keys, timeout);
161+
return db.Execute(command).ToSortedSetPopResult();
162+
}
163+
164+
/// <summary>
165+
/// Syntactic sugar for <see cref="BzPopMax(StackExchange.Redis.IDatabase,StackExchange.Redis.RedisKey[],double)"/>,
166+
/// where only one key is used.
167+
/// </summary>
168+
/// <param name="db">The <see cref="IDatabase"/> class where this extension method is applied.</param>
169+
/// <param name="key">The key to check.</param>
170+
/// <param name="timeout">Server-side timeout for the wait. A value of <c>0</c> means to wait indefinitely.</param>
171+
/// <returns>A sorted set entry paired with its score, together with the key it was popped from, or <c>null</c>
172+
/// if the server timeout expires.</returns>
173+
/// <remarks><seealso href="https://redis.io/commands/bzpopmax"/></remarks>
174+
public static Tuple<RedisKey, RedisValueWithScore>? BzPopMax(this IDatabase db, RedisKey key, double timeout)
175+
{
176+
return BzPopMax(db, new[] { key }, timeout);
177+
}
22178
}
23179
}
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+
}
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+
}
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

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ 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";
9+
public const string BZPOPMAX = "BZPOPMAX";
10+
public const string BZPOPMIN = "BZPOPMIN";
811
public const string CLIENT = "CLIENT";
912
public const string SETINFO = "SETINFO";
1013
}

src/NRedisStack/NRedisStack.csproj

+4-3
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
<PropertyGroup>
44
<Nullable>enable</Nullable>
55
<TargetFrameworks>netstandard2.0;net6.0;net7.0;net8.0</TargetFrameworks>
6+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
67
<LangVersion>latest</LangVersion>
78
<ImplicitUsings>enable</ImplicitUsings>
89
<Authors>Redis Open Source</Authors>
910
<Owners>Redis OSS</Owners>
1011
<Description>.Net Client for Redis Stack</Description>
1112
<PackageReadmeFile>README.md</PackageReadmeFile>
12-
<Version>0.11.0</Version>
13-
<ReleaseVersion>0.11.0</ReleaseVersion>
14-
<PackageVersion>0.11.0</PackageVersion>
13+
<Version>0.11.1</Version>
14+
<ReleaseVersion>0.11.1</ReleaseVersion>
15+
<PackageVersion>0.11.1</PackageVersion>
1516
</PropertyGroup>
1617

1718
<ItemGroup>

0 commit comments

Comments
 (0)