Commit 4a8061f1 authored by Todd Tingen's avatar Todd Tingen Committed by Nick Craver

Initial implementation of Redis Streams. (#860)

The Streams data type is available in release 5.0 RC1 and above.

- Implemented Sync & Async methods for all Stream related commands (minus the blocking options) as of 5.0 RC1.
- Added tests for the synchronous versions of the Streams API but the testing is a work in progress. Need to refactor for reuse within the streams tests and write a more thorough suite of tests.
- Added a NameValueEntry struct which mimicks HashEntry. Using HashEntry for the name/value pairs of stream entries seemed wrong. Perhaps refactor the usage of HashEntry to the more generic NameValueEntry and deprecate HashEntry?
parent 1799c39d
...@@ -782,6 +782,145 @@ public void SortedSetScore() ...@@ -782,6 +782,145 @@ public void SortedSetScore()
mock.Verify(_ => _.SortedSetScore("prefix:key", "member", CommandFlags.HighPriority)); mock.Verify(_ => _.SortedSetScore("prefix:key", "member", CommandFlags.HighPriority));
} }
[Fact]
public void StreamAcknowledge_1()
{
wrapper.StreamAcknowledge("key", "group", "0-0", CommandFlags.HighPriority);
mock.Verify(_ => _.StreamAcknowledge("prefix:key", "group", "0-0", CommandFlags.HighPriority));
}
[Fact]
public void StreamAcknowledge_2()
{
var messageIds = new RedisValue[] { "0-0", "0-1", "0-2" };
wrapper.StreamAcknowledge("key", "group", messageIds, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamAcknowledge("prefix:key", "group", messageIds, CommandFlags.HighPriority));
}
[Fact]
public void StreamAdd_1()
{
wrapper.StreamAdd("key", "field1", "value1", "*", 1000, true, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamAdd("prefix:key", "field1", "value1", "*", 1000, true, CommandFlags.HighPriority));
}
[Fact]
public void StreamAdd_2()
{
var fields = new NameValueEntry[0];
wrapper.StreamAdd("key", fields, "*", 1000, true, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamAdd("prefix:key", fields, "*", 1000, true, CommandFlags.HighPriority));
}
[Fact]
public void StreamClaimMessages()
{
var messageIds = new RedisValue[0];
wrapper.StreamClaim("key", "group", "consumer", 1000, messageIds, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamClaim("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.HighPriority));
}
[Fact]
public void StreamClaimMessagesReturningIds()
{
var messageIds = new RedisValue[0];
wrapper.StreamClaimIdsOnly("key", "group", "consumer", 1000, messageIds, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamClaimIdsOnly("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.HighPriority));
}
[Fact]
public void StreamConsumerInfoGet()
{
wrapper.StreamConsumerInfo("key", "group", CommandFlags.HighPriority);
mock.Verify(_ => _.StreamConsumerInfo("prefix:key", "group", CommandFlags.HighPriority));
}
[Fact]
public void StreamCreateConsumerGroup()
{
wrapper.StreamCreateConsumerGroup("key", "group", "0-0", CommandFlags.HighPriority);
mock.Verify(_ => _.StreamCreateConsumerGroup("prefix:key", "group", "0-0", CommandFlags.HighPriority));
}
[Fact]
public void StreamGroupInfoGet()
{
wrapper.StreamGroupInfo("key", CommandFlags.HighPriority);
mock.Verify(_ => _.StreamGroupInfo("prefix:key", CommandFlags.HighPriority));
}
[Fact]
public void StreamInfoGet()
{
wrapper.StreamInfo("key", CommandFlags.HighPriority);
mock.Verify(_ => _.StreamInfo("prefix:key", CommandFlags.HighPriority));
}
[Fact]
public void StreamLength()
{
wrapper.StreamLength("key", CommandFlags.HighPriority);
mock.Verify(_ => _.StreamLength("prefix:key", CommandFlags.HighPriority));
}
[Fact]
public void StreamMessagesDelete()
{
var messageIds = new RedisValue[0] { };
wrapper.StreamDelete("key", messageIds, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamDelete("prefix:key", messageIds, CommandFlags.HighPriority));
}
[Fact]
public void StreamPendingInfoGet()
{
wrapper.StreamPending("key", "group", CommandFlags.HighPriority);
mock.Verify(_ => _.StreamPending("prefix:key", "group", CommandFlags.HighPriority));
}
[Fact]
public void StreamPendingMessageInfoGet()
{
wrapper.StreamPendingMessages("key", "group", 10, RedisValue.Null, null, null, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamPendingMessages("prefix:key", "group", 10, RedisValue.Null, null, null, CommandFlags.HighPriority));
}
[Fact]
public void StreamRange()
{
wrapper.StreamRange("key", "-", "+", null, Order.Ascending, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamRange("prefix:key", "-", "+",null, Order.Ascending, CommandFlags.HighPriority));
}
[Fact]
public void StreamRead_1()
{
var keysAndIds = new StreamIdPair[0] { };
wrapper.StreamRead(keysAndIds, null, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamRead(keysAndIds, null, CommandFlags.HighPriority));
}
[Fact]
public void StreamRead_2()
{
wrapper.StreamRead("key", "0-0", null, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamRead("prefix:key", "0-0", null, CommandFlags.HighPriority));
}
[Fact]
public void StreamStreamReadGroup()
{
wrapper.StreamReadGroup("key", "group", "consumer", "0-0", 10, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamReadGroup("prefix:key", "group", "consumer", "0-0", 10, CommandFlags.HighPriority));
}
[Fact]
public void StreamTrim()
{
wrapper.StreamTrim("key", 1000, true, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamTrim("prefix:key", 1000, true, CommandFlags.HighPriority));
}
[Fact] [Fact]
public void StringAppend() public void StringAppend()
{ {
......
This diff is collapsed.
...@@ -740,6 +740,145 @@ public void SortedSetScoreAsync() ...@@ -740,6 +740,145 @@ public void SortedSetScoreAsync()
mock.Verify(_ => _.SortedSetScoreAsync("prefix:key", "member", CommandFlags.HighPriority)); mock.Verify(_ => _.SortedSetScoreAsync("prefix:key", "member", CommandFlags.HighPriority));
} }
[Fact]
public void StreamAcknowledgeAsync_1()
{
wrapper.StreamAcknowledgeAsync("key", "group", "0-0", CommandFlags.HighPriority);
mock.Verify(_ => _.StreamAcknowledgeAsync("prefix:key", "group", "0-0", CommandFlags.HighPriority));
}
[Fact]
public void StreamAcknowledgeAsync_2()
{
var messageIds = new RedisValue[] { "0-0", "0-1", "0-2" };
wrapper.StreamAcknowledgeAsync("key", "group", messageIds, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamAcknowledgeAsync("prefix:key", "group", messageIds, CommandFlags.HighPriority));
}
[Fact]
public void StreamAddAsync_1()
{
wrapper.StreamAddAsync("key", "field1", "value1", "*", 1000, true, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamAddAsync("prefix:key", "field1", "value1", "*", 1000, true, CommandFlags.HighPriority));
}
[Fact]
public void StreamAddAsync_2()
{
var fields = new NameValueEntry[0];
wrapper.StreamAddAsync("key", fields, "*", 1000, true, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamAddAsync("prefix:key", fields, "*", 1000, true, CommandFlags.HighPriority));
}
[Fact]
public void StreamClaimMessagesAsync()
{
var messageIds = new RedisValue[0];
wrapper.StreamClaimAsync("key", "group", "consumer", 1000, messageIds, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamClaimAsync("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.HighPriority));
}
[Fact]
public void StreamClaimMessagesReturningIdsAsync()
{
var messageIds = new RedisValue[0];
wrapper.StreamClaimIdsOnlyAsync("key", "group", "consumer", 1000, messageIds, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamClaimIdsOnlyAsync("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.HighPriority));
}
[Fact]
public void StreamConsumerInfoGetAsync()
{
wrapper.StreamConsumerInfoAsync("key", "group", CommandFlags.HighPriority);
mock.Verify(_ => _.StreamConsumerInfoAsync("prefix:key", "group", CommandFlags.HighPriority));
}
[Fact]
public void StreamCreateConsumerGroupAsync()
{
wrapper.StreamCreateConsumerGroupAsync("key", "group", "0-0", CommandFlags.HighPriority);
mock.Verify(_ => _.StreamCreateConsumerGroupAsync("prefix:key", "group", "0-0", CommandFlags.HighPriority));
}
[Fact]
public void StreamGroupInfoGetAsync()
{
wrapper.StreamGroupInfoAsync("key", CommandFlags.HighPriority);
mock.Verify(_ => _.StreamGroupInfoAsync("prefix:key", CommandFlags.HighPriority));
}
[Fact]
public void StreamInfoGetAsync()
{
wrapper.StreamInfoAsync("key", CommandFlags.HighPriority);
mock.Verify(_ => _.StreamInfoAsync("prefix:key", CommandFlags.HighPriority));
}
[Fact]
public void StreamLengthAsync()
{
wrapper.StreamLengthAsync("key", CommandFlags.HighPriority);
mock.Verify(_ => _.StreamLengthAsync("prefix:key", CommandFlags.HighPriority));
}
[Fact]
public void StreamMessagesDeleteAsync()
{
var messageIds = new RedisValue[0] { };
wrapper.StreamDeleteAsync("key", messageIds, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamDeleteAsync("prefix:key", messageIds, CommandFlags.HighPriority));
}
[Fact]
public void StreamPendingInfoGetAsync()
{
wrapper.StreamPendingAsync("key", "group", CommandFlags.HighPriority);
mock.Verify(_ => _.StreamPendingAsync("prefix:key", "group", CommandFlags.HighPriority));
}
[Fact]
public void StreamPendingMessageInfoGetAsync()
{
wrapper.StreamPendingMessagesAsync("key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.HighPriority);
mock.Verify(_ => _.StreamPendingMessagesAsync("prefix:key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.HighPriority));
}
[Fact]
public void StreamRangeAsync()
{
wrapper.StreamRangeAsync("key", "-", "+", null, Order.Ascending, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamRangeAsync("prefix:key", "-", "+", null, Order.Ascending, CommandFlags.HighPriority));
}
[Fact]
public void StreamReadAsync_1()
{
var keysAndIds = new StreamIdPair[0] { };
wrapper.StreamReadAsync(keysAndIds, null, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamReadAsync(keysAndIds, null, CommandFlags.HighPriority));
}
[Fact]
public void StreamReadAsync_2()
{
wrapper.StreamReadAsync("key", "0-0", null, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamReadAsync("prefix:key", "0-0", null, CommandFlags.HighPriority));
}
[Fact]
public void StreamReadGroupAsync()
{
wrapper.StreamReadGroupAsync("key", "group", "consumer", "0-0", 10, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamReadGroupAsync("prefix:key", "group", "consumer", "0-0", 10, CommandFlags.HighPriority));
}
[Fact]
public void StreamTrimAsync()
{
wrapper.StreamTrimAsync("key", 1000, true, CommandFlags.HighPriority);
mock.Verify(_ => _.StreamTrimAsync("prefix:key", 1000, true, CommandFlags.HighPriority));
}
[Fact] [Fact]
public void StringAppendAsync() public void StringAppendAsync()
{ {
......
...@@ -163,6 +163,20 @@ internal enum RedisCommand ...@@ -163,6 +163,20 @@ internal enum RedisCommand
WATCH, WATCH,
XACK,
XADD,
XCLAIM,
XDEL,
XGROUP,
XINFO,
XLEN,
XPENDING,
XRANGE,
XREAD,
XREADGROUP,
XREVRANGE,
XTRIM,
ZADD, ZADD,
ZCARD, ZCARD,
ZCOUNT, ZCOUNT,
......
...@@ -38,6 +38,13 @@ public enum RedisType ...@@ -38,6 +38,13 @@ public enum RedisType
/// <remarks>https://redis.io/commands#hash</remarks> /// <remarks>https://redis.io/commands#hash</remarks>
Hash, Hash,
/// <summary> /// <summary>
/// A Redis Stream is a data structure which models the behavior of an append only log but it has more
/// advanced features for manipulating the data contained within the stream. Each entry in a
/// stream contains a unique message ID and a list of name/value pairs containing the entry's data.
/// </summary>
/// <remarks>https://redis.io/commands#stream</remarks>
Stream,
/// <summary>
/// The data-type was not recognised by the client library /// The data-type was not recognised by the client library
/// </summary> /// </summary>
Unknown, Unknown,
......
...@@ -122,6 +122,18 @@ public static class ExtensionMethods ...@@ -122,6 +122,18 @@ public static class ExtensionMethods
return result; return result;
} }
private static readonly RedisValue[] nixValues = new RedisValue[0];
/// <summary>
/// Create an array of RedisValues from an array of strings.
/// </summary>
/// <param name="values">The string array to convert to RedisValues</param>
public static RedisValue[] ToRedisValueArray(this string[] values)
{
if (values == null) return null;
if (values.Length == 0) return nixValues;
return Array.ConvertAll(values, x => (RedisValue)x);
}
private static readonly string[] nix = new string[0]; private static readonly string[] nix = new string[0];
/// <summary> /// <summary>
/// Create an array of strings from an array of values /// Create an array of strings from an array of values
......
...@@ -587,6 +587,101 @@ public long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue ...@@ -587,6 +587,101 @@ public long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue
return Inner.SortedSetScore(ToInner(key), member, flags); return Inner.SortedSetScore(ToInner(key), member, flags);
} }
public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamAcknowledge(ToInner(key), groupName, messageId, flags);
}
public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamAcknowledge(ToInner(key), groupName, messageIds, flags);
}
public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamAdd(ToInner(key), streamField, streamValue, messageId, maxLength, useApproximateMaxLength, flags);
}
public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamAdd(ToInner(key), streamPairs, messageId, maxLength, useApproximateMaxLength, flags);
}
public RedisStreamEntry[] StreamClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamClaim(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, messageIds, flags);
}
public RedisValue[] StreamClaimIdsOnly(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamClaimIdsOnly(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, messageIds, flags);
}
public bool StreamCreateConsumerGroup(RedisKey key, RedisValue groupName, RedisValue? readFrom = null, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamCreateConsumerGroup(ToInner(key), groupName, readFrom, flags);
}
public StreamInfo StreamInfo(RedisKey key, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamInfo(ToInner(key), flags);
}
public StreamGroupInfo[] StreamGroupInfo(RedisKey key, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamGroupInfo(ToInner(key), flags);
}
public StreamConsumerInfo[] StreamConsumerInfo(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamConsumerInfo(ToInner(key), groupName, flags);
}
public long StreamLength(RedisKey key, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamLength(ToInner(key), flags);
}
public long StreamDelete(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamDelete(ToInner(key), messageIds, flags);
}
public StreamPendingInfo StreamPending(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamPending(ToInner(key), groupName, flags);
}
public StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamPendingMessages(ToInner(key), groupName, count, consumerName, minId, maxId, flags);
}
public RedisStreamEntry[] StreamRange(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamRange(ToInner(key), minId, maxId, count, order, flags);
}
public RedisStreamEntry[] StreamRead(RedisKey key, RedisValue afterId, int? count = null, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamRead(ToInner(key), afterId, count, flags);
}
public RedisStream[] StreamRead(StreamIdPair[] streamIdPairs, int? countPerStream = null, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamRead(streamIdPairs, countPerStream, flags);
}
public RedisStreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? readFromId = null, int? count = null, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamReadGroup(ToInner(key), groupName, consumerName, readFromId, count, flags);
}
public long StreamTrim(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamTrim(ToInner(key), maxLength, useApproximateMaxLength, flags);
}
public long StringAppend(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) public long StringAppend(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None)
{ {
return Inner.StringAppend(ToInner(key), value, flags); return Inner.StringAppend(ToInner(key), value, flags);
......
...@@ -566,6 +566,101 @@ public Task<long> SortedSetRemoveRangeByValueAsync(RedisKey key, RedisValue min, ...@@ -566,6 +566,101 @@ public Task<long> SortedSetRemoveRangeByValueAsync(RedisKey key, RedisValue min,
return Inner.SortedSetScoreAsync(ToInner(key), member, flags); return Inner.SortedSetScoreAsync(ToInner(key), member, flags);
} }
public Task<long> StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamAcknowledgeAsync(ToInner(key), groupName, messageId, flags);
}
public Task<long> StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamAcknowledgeAsync(ToInner(key), groupName, messageIds, flags);
}
public Task<RedisValue> StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamAddAsync(ToInner(key), streamField, streamValue, messageId, maxLength, useApproximateMaxLength, flags);
}
public Task<RedisValue> StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamAddAsync(ToInner(key), streamPairs, messageId, maxLength, useApproximateMaxLength, flags);
}
public Task<RedisStreamEntry[]> StreamClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamClaimAsync(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, messageIds, flags);
}
public Task<RedisValue[]> StreamClaimIdsOnlyAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamClaimIdsOnlyAsync(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, messageIds, flags);
}
public Task<bool> StreamCreateConsumerGroupAsync(RedisKey key, RedisValue groupName, RedisValue? readFrom = null, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamCreateConsumerGroupAsync(ToInner(key), groupName, readFrom, flags);
}
public Task<StreamInfo> StreamInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamInfoAsync(ToInner(key), flags);
}
public Task<StreamGroupInfo[]> StreamGroupInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamGroupInfoAsync(ToInner(key), flags);
}
public Task<StreamConsumerInfo[]> StreamConsumerInfoAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamConsumerInfoAsync(ToInner(key), groupName, flags);
}
public Task<long> StreamLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamLengthAsync(ToInner(key), flags);
}
public Task<long> StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamDeleteAsync(ToInner(key), messageIds, flags);
}
public Task<StreamPendingInfo> StreamPendingAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamPendingAsync(ToInner(key), groupName, flags);
}
public Task<StreamPendingMessageInfo[]> StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamPendingMessagesAsync(ToInner(key), groupName, count, consumerName, minId, maxId, flags);
}
public Task<RedisStreamEntry[]> StreamRangeAsync(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamRangeAsync(ToInner(key), minId, maxId, count, order, flags);
}
public Task<RedisStreamEntry[]> StreamReadAsync(RedisKey key, RedisValue afterId, int? count = null, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamReadAsync(ToInner(key), afterId, count, flags);
}
public Task<RedisStream[]> StreamReadAsync(StreamIdPair[] streamIdPairs, int? countPerStream = null, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamReadAsync(streamIdPairs, countPerStream, flags);
}
public Task<RedisStreamEntry[]> StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? readFromId = null, int? count = null, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamReadGroupAsync(ToInner(key), groupName, consumerName, readFromId, count, flags);
}
public Task<long> StreamTrimAsync(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None)
{
return Inner.StreamTrimAsync(ToInner(key), maxLength, useApproximateMaxLength, flags);
}
public Task<long> StringAppendAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) public Task<long> StringAppendAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None)
{ {
return Inner.StringAppendAsync(ToInner(key), value, flags); return Inner.StringAppendAsync(ToInner(key), value, flags);
......
using System;
using System.Collections.Generic;
namespace StackExchange.Redis
{
/// <summary>
/// Describes a value contained in a stream (a name/value pair).
/// </summary>
public struct NameValueEntry : IEquatable<NameValueEntry>
{
internal readonly RedisValue name, value;
/// <summary>
/// Initializes a <see cref="NameValueEntry"/> value.
/// </summary>
/// <param name="name">The name for this entry.</param>
/// <param name="value">The value for this entry.</param>
public NameValueEntry(RedisValue name, RedisValue value)
{
this.name = name;
this.value = value;
}
/// <summary>
/// The name of the field.
/// </summary>
public RedisValue Name => name;
/// <summary>
/// The value of the field.
/// </summary>
public RedisValue Value => value;
/// <summary>
/// Converts to a key/value pair
/// </summary>
/// <param name="value">The <see cref="NameValueEntry"/> to create a <see cref="KeyValuePair{TKey, TValue}"/> from.</param>
public static implicit operator KeyValuePair<RedisValue, RedisValue>(NameValueEntry value) =>
new KeyValuePair<RedisValue, RedisValue>(value.name, value.value);
/// <summary>
/// Converts from a key/value pair
/// </summary>
/// <param name="value">The <see cref="KeyValuePair{TKey, TValue}"/> to get a <see cref="NameValueEntry"/> from.</param>
public static implicit operator NameValueEntry(KeyValuePair<RedisValue, RedisValue> value) =>
new NameValueEntry(value.Key, value.Value);
/// <summary>
/// See Object.ToString()
/// </summary>
public override string ToString() => name + ": " + value;
/// <summary>
/// See Object.GetHashCode()
/// </summary>
public override int GetHashCode() => name.GetHashCode() ^ value.GetHashCode();
/// <summary>
/// Compares two values for equality.
/// </summary>
/// <param name="obj">The <see cref="NameValueEntry"/> to compare to.</param>
public override bool Equals(object obj) => obj is NameValueEntry heObj && Equals(heObj);
/// <summary>
/// Compares two values for equality.
/// </summary>
/// <param name="other">The <see cref="NameValueEntry"/> to compare to.</param>
public bool Equals(NameValueEntry other) => name == other.name && value == other.value;
/// <summary>
/// Compares two values for equality
/// </summary>
/// <param name="x">The first <see cref="NameValueEntry"/> to compare.</param>
/// <param name="y">The second <see cref="NameValueEntry"/> to compare.</param>
public static bool operator ==(NameValueEntry x, NameValueEntry y) => x.name == y.name && x.value == y.value;
/// <summary>
/// Compares two values for non-equality
/// </summary>
/// <param name="x">The first <see cref="NameValueEntry"/> to compare.</param>
/// <param name="y">The second <see cref="NameValueEntry"/> to compare.</param>
public static bool operator !=(NameValueEntry x, NameValueEntry y) => x.name != y.name || x.value != y.value;
}
}
...@@ -7,6 +7,9 @@ internal struct RawResult ...@@ -7,6 +7,9 @@ internal struct RawResult
{ {
public static readonly RawResult EmptyArray = new RawResult(new RawResult[0]); public static readonly RawResult EmptyArray = new RawResult(new RawResult[0]);
public static readonly RawResult Nil = new RawResult(); public static readonly RawResult Nil = new RawResult();
public static RawResult CreateMultiBulk(params RawResult[] results) => new RawResult(results);
private static readonly byte[] emptyBlob = new byte[0]; private static readonly byte[] emptyBlob = new byte[0];
private readonly int offset, count; private readonly int offset, count;
private readonly Array arr; private readonly Array arr;
......
...@@ -28,7 +28,8 @@ public struct RedisFeatures ...@@ -28,7 +28,8 @@ public struct RedisFeatures
v2_8_18 = new Version(2, 8, 18), v2_8_18 = new Version(2, 8, 18),
v2_9_5 = new Version(2, 9, 5), v2_9_5 = new Version(2, 9, 5),
v3_0_0 = new Version(3, 0, 0), v3_0_0 = new Version(3, 0, 0),
v3_2_0 = new Version(3, 2, 0); v3_2_0 = new Version(3, 2, 0),
v4_9_1 = new Version(4, 9, 1); // 5.0 RC1 is version 4.9.1
private readonly Version version; private readonly Version version;
/// <summary> /// <summary>
...@@ -120,6 +121,11 @@ public RedisFeatures(Version version) ...@@ -120,6 +121,11 @@ public RedisFeatures(Version version)
/// </summary> /// </summary>
public bool SetVaradicAddRemove => Version >= v2_4_0; public bool SetVaradicAddRemove => Version >= v2_4_0;
/// <summary>
/// Are Redis Streams available?
/// </summary>
public bool Streams => Version >= v4_9_1;
/// <summary> /// <summary>
/// Is STRLEN available? /// Is STRLEN available?
/// </summary> /// </summary>
......
namespace StackExchange.Redis
{
/// <summary>
/// Describes a Redis Stream with an associated array of entries.
/// </summary>
public struct RedisStream
{
internal RedisStream(RedisKey key, RedisStreamEntry[] entries)
{
Key = key;
Entries = entries;
}
/// <summary>
/// The key for the stream.
/// </summary>
public RedisKey Key { get; }
/// <summary>
/// An arry of entries contained within the stream.
/// </summary>
public RedisStreamEntry[] Entries { get; }
}
}
namespace StackExchange.Redis
{
/// <summary>
/// Describes an entry contained in a Redis Stream.
/// </summary>
public struct RedisStreamEntry
{
internal RedisStreamEntry(RedisValue id, NameValueEntry[] values)
{
Id = id;
Values = values;
}
/// <summary>
/// A null stream entry.
/// </summary>
public static RedisStreamEntry Null { get; } = new RedisStreamEntry(RedisValue.Null, null);
/// <summary>
/// The ID assigned to the message.
/// </summary>
public RedisValue Id { get; }
/// <summary>
/// The values contained within the message.
/// </summary>
public NameValueEntry[] Values { get; }
/// <summary>
/// Indicates that the Redis Stream Entry is null.
/// </summary>
public bool IsNull => Id == RedisValue.Null && Values == null;
}
}

namespace StackExchange.Redis
{
/// <summary>
/// Constants representing values used in Redis Stream commands.
/// </summary>
internal static class StreamConstants
{
/// <summary>
/// The "~" value used with the MAXLEN option.
/// </summary>
internal static readonly RedisValue ApproximateMaxLen = "~";
/// <summary>
/// The "*" value used with the XADD command.
/// </summary>
internal static readonly RedisValue AutoGeneratedId = "*";
/// <summary>
/// The "$" value used in the XGROUP command. Indicates reading only new messages from the stream.
/// </summary>
internal static readonly RedisValue NewMessages = "$";
/// <summary>
/// The "-" value used in the XRANGE, XREAD, and XREADGROUP commands. Indicates the minimum message ID from the stream.
/// </summary>
internal static readonly RedisValue ReadMinValue = "-";
/// <summary>
/// The "+" value used in the XRANGE, XREAD, and XREADGROUP commands. Indicates the maximum message ID from the stream.
/// </summary>
internal static readonly RedisValue ReadMaxValue = "+";
/// <summary>
/// The ">" value used in the XREADGROUP command. Use this to read messages that have not been delivered to a consumer group.
/// </summary>
internal static readonly RedisValue UndeliveredMessages = ">";
internal static readonly RedisValue Consumers = "CONSUMERS";
internal static readonly RedisValue Count = "COUNT";
internal static readonly RedisValue Create = "CREATE";
internal static readonly RedisValue Group = "GROUP";
internal static readonly RedisValue Groups = "GROUPS";
internal static readonly RedisValue JustId = "JUSTID";
internal static readonly RedisValue MaxLen = "MAXLEN";
internal static readonly RedisValue Stream = "STREAM";
internal static readonly RedisValue Streams = "STREAMS";
}
}

namespace StackExchange.Redis
{
/// <summary>
/// Describes a consumer off a Redis Stream.
/// </summary>
public struct StreamConsumer
{
internal StreamConsumer(RedisValue name, int pendingMessageCount)
{
Name = name;
PendingMessageCount = pendingMessageCount;
}
/// <summary>
/// The name of the consumer.
/// </summary>
public RedisValue Name { get; }
/// <summary>
/// The number of messages that have been delivered by not yet acknowledged by the consumer.
/// </summary>
public int PendingMessageCount { get; }
}
}

namespace StackExchange.Redis
{
/// <summary>
/// Describes a consumer within a consumer group, retrieved using the XINFO CONSUMERS command. <see cref="IDatabase.StreamConsumerInfo"/>
/// </summary>
public struct StreamConsumerInfo
{
internal StreamConsumerInfo(string name, int pendingMessageCount, long idleTimeInMilliseconds)
{
Name = name;
PendingMessageCount = pendingMessageCount;
IdleTimeInMilliseconds = idleTimeInMilliseconds;
}
/// <summary>
/// The name of the consumer.
/// </summary>
public string Name { get; }
/// <summary>
/// The number of pending messages for the consumer. A pending message is one that has been
/// received by the consumer but not yet acknowledged.
/// </summary>
public int PendingMessageCount { get; }
/// <summary>
/// The idle time, if any, for the consumer.
/// </summary>
public long IdleTimeInMilliseconds { get; }
}
}

namespace StackExchange.Redis
{
/// <summary>
/// Describes a consumer group retrieved using the XINFO GROUPS command. <see cref="IDatabase.StreamGroupInfo"/>
/// </summary>
public struct StreamGroupInfo
{
internal StreamGroupInfo(string name, int consumerCount, int pendingMessageCount)
{
Name = name;
ConsumerCount = consumerCount;
PendingMessageCount = pendingMessageCount;
}
/// <summary>
/// The name of the consumer group.
/// </summary>
public string Name { get; }
/// <summary>
/// The number of consumers within the consumer group.
/// </summary>
public int ConsumerCount { get; }
/// <summary>
/// The total number of pending messages for the consumer group. A pending message is one that has been
/// received by a consumer but not yet acknowledged.
/// </summary>
public int PendingMessageCount { get; }
}
}

namespace StackExchange.Redis
{
/// <summary>
/// Describes a pair consisting of the Stream Key and the ID from which to read.
/// </summary>
/// <remarks><see cref="IDatabase.StreamRead(StreamIdPair[], int?, CommandFlags)"/></remarks>
public struct StreamIdPair
{
/// <summary>
/// Initializes a <see cref="StreamIdPair"/> value.
/// </summary>
/// <param name="key">The key for the stream.</param>
/// <param name="id">The ID from which to begin reading the stream.</param>
public StreamIdPair(RedisKey key, RedisValue id)
{
Key = key;
Id = id;
}
/// <summary>
/// The key for the stream.
/// </summary>
public RedisKey Key { get; }
/// <summary>
/// The ID from which to begin reading the stream.
/// </summary>
public RedisValue Id { get; }
/// <summary>
/// See Object.ToString()
/// </summary>
public override string ToString() => $"{Key}: {Id}";
}
}

namespace StackExchange.Redis
{
/// <summary>
/// Describes stream information retrieved using the XINFO STREAM command. <see cref="IDatabase.StreamInfo"/>
/// </summary>
public struct StreamInfo
{
internal StreamInfo(int length,
int radixTreeKeys,
int radixTreeNodes,
int groups,
RedisStreamEntry firstEntry,
RedisStreamEntry lastEntry)
{
Length = length;
RadixTreeKeys = radixTreeKeys;
RadixTreeNodes = radixTreeNodes;
ConsumerGroupCount = groups;
FirstEntry = firstEntry;
LastEntry = lastEntry;
}
/// <summary>
/// The number of entries in the stream.
/// </summary>
public int Length { get; }
/// <summary>
/// The number of radix tree keys in the stream.
/// </summary>
public int RadixTreeKeys { get; }
/// <summary>
/// The number of radix tree nodes in the stream.
/// </summary>
public int RadixTreeNodes { get; }
/// <summary>
/// The number of consumers groups in the stream.
/// </summary>
public int ConsumerGroupCount { get; }
/// <summary>
/// The first entry in the stream.
/// </summary>
public RedisStreamEntry FirstEntry { get; }
/// <summary>
/// The last entry in the stream.
/// </summary>
public RedisStreamEntry LastEntry { get; }
}
}

namespace StackExchange.Redis
{
/// <summary>
/// Describes basic information about pending messages for a consumer group.
/// </summary>
public struct StreamPendingInfo
{
internal StreamPendingInfo(int pendingMessageCount,
RedisValue lowestId,
RedisValue highestId,
StreamConsumer[] consumers)
{
PendingMessageCount = pendingMessageCount;
LowestPendingMessageId = lowestId;
HighestPendingMessageId = highestId;
Consumers = consumers;
}
/// <summary>
/// The number of pending messages. A pending message is a message that has been consumed but not yet acknowledged.
/// </summary>
public int PendingMessageCount { get; }
/// <summary>
/// The lowest message ID in the set of pending messages.
/// </summary>
public RedisValue LowestPendingMessageId { get; }
/// <summary>
/// The highest message ID in the set of pending messages.
/// </summary>
public RedisValue HighestPendingMessageId { get; }
/// <summary>
/// An array of consumers within the consumer group that have pending messages.
/// </summary>
public StreamConsumer[] Consumers { get; }
}
}

namespace StackExchange.Redis
{
/// <summary>
/// Describes properties of a pending message. A pending message is one that has
/// been received by a consumer but has not yet been acknowledged.
/// </summary>
public struct StreamPendingMessageInfo
{
internal StreamPendingMessageInfo(RedisValue messageId,
RedisValue consumerName,
long idleTimeInMs,
int deliveryCount)
{
MessageId = messageId;
ConsumerName = consumerName;
IdleTimeInMilliseconds = idleTimeInMs;
DeliveryCount = deliveryCount;
}
/// <summary>
/// The ID of the pending message.
/// </summary>
public RedisValue MessageId { get; }
/// <summary>
/// The consumer that received the pending message.
/// </summary>
public RedisValue ConsumerName { get; }
/// <summary>
/// The time that has passed since the message was last delivered to a consumer.
/// </summary>
public long IdleTimeInMilliseconds { get; }
/// <summary>
/// The number of times the message has been delivered to a consumer.
/// </summary>
public int DeliveryCount { get; }
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment