diff --git a/eng/StackExchange.Redis.Build/FastHashGenerator.cs b/eng/StackExchange.Redis.Build/FastHashGenerator.cs index cdbc94ebe..1e563c25d 100644 --- a/eng/StackExchange.Redis.Build/FastHashGenerator.cs +++ b/eng/StackExchange.Redis.Build/FastHashGenerator.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using RESPite; namespace StackExchange.Redis.Build; @@ -78,7 +79,15 @@ private static string GetName(INamedTypeSymbol type) string name = named.Name, value = ""; foreach (var attrib in named.GetAttributes()) { - if (attrib.AttributeClass?.Name == "FastHashAttribute") + if (attrib.AttributeClass is { + Name: "FastHashAttribute", + ContainingType: null, + ContainingNamespace: + { + Name: "RESPite", + ContainingNamespace.IsGlobalNamespace: true, + } + }) { if (attrib.ConstructorArguments.Length == 1) { @@ -178,25 +187,28 @@ private void Generate( // perform string escaping on the generated value (this includes the quotes, note) var csValue = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(literal.Value)).ToFullString(); - var hash = FastHash.Hash64(buffer.AsSpan(0, len)); + var hashCS = FastHash.HashCS(buffer.AsSpan(0, len)); + var hashCI = FastHash.HashCI(buffer.AsSpan(0, len)); NewLine().Append("static partial class ").Append(literal.Name); NewLine().Append("{"); indent++; NewLine().Append("public const int Length = ").Append(len).Append(';'); - NewLine().Append("public const long Hash = ").Append(hash).Append(';'); + NewLine().Append("public const long HashCS = ").Append(hashCS).Append(';'); + NewLine().Append("public const long HashCI = ").Append(hashCI).Append(';'); NewLine().Append("public static ReadOnlySpan U8 => ").Append(csValue).Append("u8;"); NewLine().Append("public const string Text = ").Append(csValue).Append(';'); - if (len <= 8) + if (len <= FastHash.MaxBytesHashIsEqualityCS) { - // the hash enforces all the values - NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.Payload.Length == Length;"); - NewLine().Append("public static bool Is(long hash, ReadOnlySpan value) => hash == Hash & value.Length == Length;"); + // the case-sensitive hash enforces all the values + NewLine().Append("public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS & value.Length == Length;"); + NewLine().Append("public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && (global::RESPite.FastHash.HashCS(value) == HashCS || global::RESPite.FastHash.EqualsCI(value, U8));"); } else { - NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8);"); - NewLine().Append("public static bool Is(long hash, ReadOnlySpan value) => hash == Hash && value.SequenceEqual(U8);"); + NewLine().Append("public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8);"); + NewLine().Append("public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8);"); } + indent--; NewLine().Append("}"); } diff --git a/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj index f875133ba..8de694082 100644 --- a/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj +++ b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj @@ -12,8 +12,11 @@ - - FastHash.cs + + Shared/FastHash.cs + + + Shared/Experiments.cs diff --git a/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt b/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt index 6a0bff19d..a52c05995 100644 --- a/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt @@ -20,6 +20,18 @@ [SER004]RESPite.Buffers.CycleBuffer.UncommittedAvailable.get -> int [SER004]RESPite.Buffers.CycleBuffer.Write(in System.Buffers.ReadOnlySequence value) -> void [SER004]RESPite.Buffers.CycleBuffer.Write(System.ReadOnlySpan value) -> void +[SER004]RESPite.FastHash +[SER004]RESPite.FastHash.FastHash() -> void +[SER004]RESPite.FastHash.FastHash(System.ReadOnlyMemory value) -> void +[SER004]RESPite.FastHash.FastHash(System.ReadOnlySpan value) -> void +[SER004]RESPite.FastHash.IsCI(long hash, System.ReadOnlySpan value) -> bool +[SER004]RESPite.FastHash.IsCI(System.ReadOnlySpan value) -> bool +[SER004]RESPite.FastHash.IsCS(long hash, System.ReadOnlySpan value) -> bool +[SER004]RESPite.FastHash.IsCS(System.ReadOnlySpan value) -> bool +[SER004]RESPite.FastHash.Length.get -> int +[SER004]RESPite.FastHashAttribute +[SER004]RESPite.FastHashAttribute.FastHashAttribute(string! token = "") -> void +[SER004]RESPite.FastHashAttribute.Token.get -> string! [SER004]RESPite.Messages.RespReader.AggregateEnumerator.FillAll(scoped System.Span target, RESPite.Messages.RespReader.Projection! projection) -> void [SER004]RESPite.Messages.RespReader.AggregateEnumerator.FillAll(scoped System.Span target, ref TState state, RESPite.Messages.RespReader.Projection! first, RESPite.Messages.RespReader.Projection! second, System.Func! combine) -> void [SER004]RESPite.Messages.RespReader.AggregateEnumerator.FillAll(scoped System.Span target, ref TState state, RESPite.Messages.RespReader.Projection! projection) -> void @@ -157,6 +169,11 @@ [SER004]RESPite.Messages.RespScanState.TryRead(System.ReadOnlySpan value, out int bytesRead) -> bool [SER004]RESPite.RespException [SER004]RESPite.RespException.RespException(string! message) -> void +[SER004]static RESPite.FastHash.EqualsCI(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool +[SER004]static RESPite.FastHash.EqualsCS(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool +[SER004]static RESPite.FastHash.HashCI(scoped System.ReadOnlySpan value) -> long +[SER004]static RESPite.FastHash.HashCS(scoped System.ReadOnlySpan value) -> long +[SER004]static RESPite.FastHash.HashCS(System.Buffers.ReadOnlySequence value) -> long [SER004]static RESPite.Messages.RespFrameScanner.Default.get -> RESPite.Messages.RespFrameScanner! [SER004]static RESPite.Messages.RespFrameScanner.Subscription.get -> RESPite.Messages.RespFrameScanner! [SER004]virtual RESPite.Messages.RespAttributeReader.Read(ref RESPite.Messages.RespReader reader, ref T value) -> void diff --git a/src/RESPite/RESPite.csproj b/src/RESPite/RESPite.csproj index 7e93abb6f..68d87fe42 100644 --- a/src/RESPite/RESPite.csproj +++ b/src/RESPite/RESPite.csproj @@ -45,6 +45,7 @@ + diff --git a/src/StackExchange.Redis/FastHash.cs b/src/RESPite/Shared/FastHash.cs similarity index 50% rename from src/StackExchange.Redis/FastHash.cs rename to src/RESPite/Shared/FastHash.cs index 49eb01b31..5c4f5dd70 100644 --- a/src/StackExchange.Redis/FastHash.cs +++ b/src/RESPite/Shared/FastHash.cs @@ -2,10 +2,11 @@ using System.Buffers; using System.Buffers.Binary; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace StackExchange.Redis; +namespace RESPite; /// /// This type is intended to provide fast hashing functions for small strings, for example well-known @@ -15,54 +16,126 @@ namespace StackExchange.Redis; /// See HastHashGenerator.md for more information and intended usage. [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] [Conditional("DEBUG")] // evaporate in release -internal sealed class FastHashAttribute(string token = "") : Attribute +[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] +public sealed class FastHashAttribute(string token = "") : Attribute { public string Token => token; } -internal static class FastHash +[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] +public readonly struct FastHash { - /* not sure we need this, but: retain for reference + private readonly long _hashCI; + private readonly long _hashCS; + private readonly ReadOnlyMemory _value; + public int Length => _value.Length; + + public FastHash(ReadOnlySpan value) : this((ReadOnlyMemory)value.ToArray()) { } + public FastHash(ReadOnlyMemory value) + { + _value = value; + var span = value.Span; + _hashCI = HashCI(span); + _hashCS = HashCS(span); + } - // Perform case-insensitive hash by masking (X and x differ by only 1 bit); this halves - // our entropy, but is still useful when case doesn't matter. private const long CaseMask = ~0x2020202020202020; - public static long Hash64CI(this ReadOnlySequence value) - => value.Hash64() & CaseMask; - public static long Hash64CI(this scoped ReadOnlySpan value) - => value.Hash64() & CaseMask; -*/ + public bool IsCS(ReadOnlySpan value) => IsCS(HashCS(value), value); + + public bool IsCS(long hash, ReadOnlySpan value) + { + var len = _value.Length; + if (hash != _hashCS | (value.Length != len)) return false; + return len <= MaxBytesHashIsEqualityCS || EqualsCS(_value.Span, value); + } + + public bool IsCI(ReadOnlySpan value) => IsCI(HashCI(value), value); + public bool IsCI(long hash, ReadOnlySpan value) + { + var len = _value.Length; + if (hash != _hashCI | (value.Length != len)) return false; + if (len <= MaxBytesHashIsEqualityCS && HashCS(value) == _hashCS) return true; + return EqualsCI(_value.Span, value); + } - public static long Hash64(this ReadOnlySequence value) + public static long HashCS(ReadOnlySequence value) { #if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER var first = value.FirstSpan; #else var first = value.First.Span; #endif - return first.Length >= sizeof(long) || value.IsSingleSegment - ? first.Hash64() : SlowHash64(value); + return first.Length >= MaxBytesHashed || value.IsSingleSegment + ? HashCS(first) : SlowHashCS(value); - static long SlowHash64(ReadOnlySequence value) + static long SlowHashCS(ReadOnlySequence value) { - Span buffer = stackalloc byte[sizeof(long)]; - if (value.Length < sizeof(long)) + Span buffer = stackalloc byte[MaxBytesHashed]; + var len = value.Length; + if (len <= MaxBytesHashed) { value.CopyTo(buffer); - buffer.Slice((int)value.Length).Clear(); + buffer = buffer.Slice(0, (int)len); } else { - value.Slice(0, sizeof(long)).CopyTo(buffer); + value.Slice(0, MaxBytesHashed).CopyTo(buffer); } - return BitConverter.IsLittleEndian - ? Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(buffer)) - : BinaryPrimitives.ReadInt64LittleEndian(buffer); + return HashCS(buffer); } } - public static long Hash64(this scoped ReadOnlySpan value) + internal const int MaxBytesHashIsEqualityCS = sizeof(long), MaxBytesHashed = sizeof(long); + + public static bool EqualsCS(ReadOnlySpan first, ReadOnlySpan second) + { + var len = first.Length; + if (len != second.Length) return false; + // for very short values, the CS hash performs CS equality + return len <= MaxBytesHashIsEqualityCS ? HashCS(first) == HashCS(second) : first.SequenceEqual(second); + } + + public static unsafe bool EqualsCI(ReadOnlySpan first, ReadOnlySpan second) + { + var len = first.Length; + if (len != second.Length) return false; + // for very short values, the CS hash performs CS equality; check that first + if (len <= MaxBytesHashIsEqualityCS && HashCS(first) == HashCS(second)) return true; + + // OK, don't be clever (SIMD, etc); the purpose of FashHash is to compare RESP key tokens, which are + // typically relatively short, think 3-20 bytes. That wouldn't even touch a SIMD vector, so: + // just loop (the exact thing we'd need to do *anyway* in a SIMD implementation, to mop up the non-SIMD + // trailing bytes). + fixed (byte* firstPtr = &MemoryMarshal.GetReference(first)) + { + fixed (byte* secondPtr = &MemoryMarshal.GetReference(second)) + { + const int CS_MASK = ~0x20; + for (int i = 0; i < len; i++) + { + byte x = firstPtr[i]; + var xCI = x & CS_MASK; + if (xCI >= 'A' & xCI <= 'Z') + { + // alpha mismatch + if (xCI != (secondPtr[i] & CS_MASK)) return false; + } + else if (x != secondPtr[i]) + { + // non-alpha mismatch + return false; + } + } + return true; + } + } + } + + public static long HashCI(scoped ReadOnlySpan value) + => HashCS(value) & CaseMask; + + public static long HashCS(scoped ReadOnlySpan value) { if (BitConverter.IsLittleEndian) { diff --git a/src/StackExchange.Redis/APITypes/LCSMatchResult.cs b/src/StackExchange.Redis/APITypes/LCSMatchResult.cs index fdeea89ff..3aca6357b 100644 --- a/src/StackExchange.Redis/APITypes/LCSMatchResult.cs +++ b/src/StackExchange.Redis/APITypes/LCSMatchResult.cs @@ -1,11 +1,14 @@ using System; +using System.ComponentModel; +// ReSharper disable once CheckNamespace namespace StackExchange.Redis; /// /// The result of a LongestCommonSubsequence command with IDX feature. /// Returns a list of the positions of each sub-match. /// +// ReSharper disable once InconsistentNaming public readonly struct LCSMatchResult { internal static LCSMatchResult Null { get; } = new LCSMatchResult(Array.Empty(), 0); @@ -36,20 +39,92 @@ internal LCSMatchResult(LCSMatch[] matches, long matchLength) LongestMatchLength = matchLength; } + /// + /// Represents a position range in a string. + /// + // ReSharper disable once InconsistentNaming + public readonly struct LCSPosition : IEquatable + { + /// + /// The start index of the position. + /// + public long Start { get; } + + /// + /// The end index of the position. + /// + public long End { get; } + + /// + /// Returns a new Position. + /// + /// The start index. + /// The end index. + public LCSPosition(long start, long end) + { + Start = start; + End = end; + } + + /// + public override string ToString() => $"[{Start}..{End}]"; + + /// + public override int GetHashCode() + { + unchecked + { + return ((int)Start * 31) + (int)End; + } + } + + /// + public override bool Equals(object? obj) => obj is LCSPosition other && Equals(in other); + + /// + /// Compares this position to another for equality. + /// + [CLSCompliant(false)] + public bool Equals(in LCSPosition other) => Start == other.Start && End == other.End; + + /// + /// Compares this position to another for equality. + /// + bool IEquatable.Equals(LCSPosition other) => Equals(in other); + } + /// /// Represents a sub-match of the longest match. i.e first indexes the matched substring in each string. /// - public readonly struct LCSMatch + // ReSharper disable once InconsistentNaming + public readonly struct LCSMatch : IEquatable { + private readonly LCSPosition _first; + private readonly LCSPosition _second; + + /// + /// The position of the matched substring in the first string. + /// + public LCSPosition First => _first; + + /// + /// The position of the matched substring in the second string. + /// + public LCSPosition Second => _second; + /// /// The first index of the matched substring in the first string. /// - public long FirstStringIndex { get; } + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public long FirstStringIndex => _first.Start; /// /// The first index of the matched substring in the second string. /// - public long SecondStringIndex { get; } + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public long SecondStringIndex => _second.Start; /// /// The length of the match. @@ -59,14 +134,44 @@ public readonly struct LCSMatch /// /// Returns a new Match. /// - /// The first index of the matched substring in the first string. - /// The first index of the matched substring in the second string. + /// The position of the matched substring in the first string. + /// The position of the matched substring in the second string. /// The length of the match. - internal LCSMatch(long firstStringIndex, long secondStringIndex, long length) + internal LCSMatch(in LCSPosition first, in LCSPosition second, long length) { - FirstStringIndex = firstStringIndex; - SecondStringIndex = secondStringIndex; + _first = first; + _second = second; Length = length; } + + /// + public override string ToString() => $"First: {_first}, Second: {_second}, Length: {Length}"; + + /// + public override int GetHashCode() + { + unchecked + { + int hash = 17; + hash = (hash * 31) + _first.GetHashCode(); + hash = (hash * 31) + _second.GetHashCode(); + hash = (hash * 31) + Length.GetHashCode(); + return hash; + } + } + + /// + public override bool Equals(object? obj) => obj is LCSMatch other && Equals(in other); + + /// + /// Compares this match to another for equality. + /// + [CLSCompliant(false)] + public bool Equals(in LCSMatch other) => _first.Equals(in other._first) && _second.Equals(in other._second) && Length == other.Length; + + /// + /// Compares this match to another for equality. + /// + bool IEquatable.Equals(LCSMatch other) => Equals(in other); } } diff --git a/src/StackExchange.Redis/ClientInfo.cs b/src/StackExchange.Redis/ClientInfo.cs index 15c3f641a..511aee723 100644 --- a/src/StackExchange.Redis/ClientInfo.cs +++ b/src/StackExchange.Redis/ClientInfo.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net; +using RESPite.Messages; namespace StackExchange.Redis { @@ -289,18 +290,16 @@ private static void AddFlag(ref ClientFlags value, string raw, ClientFlags toAdd private sealed class ClientInfoProcessor : ResultProcessor { - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { - switch (result.Resp2TypeBulkString) + if (reader.Prefix is RespPrefix.BulkString or RespPrefix.VerbatimString) { - case ResultType.BulkString: - var raw = result.GetString(); - if (TryParse(raw, out var clients)) - { - SetResult(message, clients); - return true; - } - break; + var raw = reader.ReadString(); + if (TryParse(raw, out var clients)) + { + SetResult(message, clients); + return true; + } } return false; } diff --git a/src/StackExchange.Redis/Condition.cs b/src/StackExchange.Redis/Condition.cs index 1b5bcced4..fa7e76299 100644 --- a/src/StackExchange.Redis/Condition.cs +++ b/src/StackExchange.Redis/Condition.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using RESPite.Messages; namespace StackExchange.Redis { @@ -372,7 +373,8 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value) internal abstract IEnumerable CreateMessages(int db, IResultBox? resultBox); internal abstract int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy); - internal abstract bool TryValidate(in RawResult result, out bool value); + + internal abstract bool TryValidate(ref RespReader reader, out bool value); internal sealed class ConditionProcessor : ResultProcessor { @@ -387,13 +389,12 @@ public static Message CreateMessage(Condition condition, int db, CommandFlags fl public static Message CreateMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) => new ConditionMessage(condition, db, flags, command, key, value, value1, value2, value3, value4); - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0071:Simplify interpolation", Justification = "Allocations (string.Concat vs. string.Format)")] - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { - connection?.BridgeCouldBeNull?.Multiplexer?.OnTransactionLog($"condition '{message.CommandAndKey}' got '{result.ToString()}'"); + connection?.BridgeCouldBeNull?.Multiplexer?.OnTransactionLog($"condition '{message.CommandAndKey}' got '{reader.GetOverview()}'"); var msg = message as ConditionMessage; var condition = msg?.Condition; - if (condition != null && condition.TryValidate(result, out bool final)) + if (condition != null && condition.TryValidate(ref reader, out bool final)) { SetResult(message, final); return true; @@ -510,19 +511,20 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key); - internal override bool TryValidate(in RawResult result, out bool value) + internal override bool TryValidate(ref RespReader reader, out bool value) { switch (type) { case RedisType.SortedSet: - var parsedValue = result.AsRedisValue(); - value = parsedValue.IsNull != expectedResult; - ConnectionMultiplexer.TraceWithoutContext("exists: " + parsedValue + "; expected: " + expectedResult + "; voting: " + value); + // ZSCORE returns bulk string (score) or null + var parsedValue = reader.IsNull; + value = parsedValue != expectedResult; + ConnectionMultiplexer.TraceWithoutContext("exists: " + !parsedValue + "; expected: " + expectedResult + "; voting: " + value); return true; default: - bool parsed; - if (ResultProcessor.DemandZeroOrOneProcessor.TryGet(result, out parsed)) + // EXISTS, HEXISTS, SISMEMBER return integer 0 or 1 + if (ResultProcessor.DemandZeroOrOneProcessor.TryGet(ref reader, out bool parsed)) { value = parsed == expectedResult; ConnectionMultiplexer.TraceWithoutContext("exists: " + parsed + "; expected: " + expectedResult + "; voting: " + value); @@ -586,12 +588,30 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key); - internal override bool TryValidate(in RawResult result, out bool value) + internal override bool TryValidate(ref RespReader reader, out bool value) { - value = result.ItemsCount == 1 && result[0].AsRedisValue().StartsWith(prefix); - - if (!expectedResult) value = !value; - return true; + // ZRANGEBYLEX returns an array with 0 or 1 elements + if (reader.IsAggregate) + { + var count = reader.AggregateLength(); + if (count == 1) + { + // Check if the first element starts with prefix + if (reader.TryMoveNext() && reader.IsScalar) + { + value = reader.ReadRedisValue().StartsWith(prefix); + if (!expectedResult) value = !value; + return true; + } + } + else if (count == 0) + { + value = !expectedResult; // No match found + return true; + } + } + value = false; + return false; } } @@ -640,13 +660,21 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key); - internal override bool TryValidate(in RawResult result, out bool value) + internal override bool TryValidate(ref RespReader reader, out bool value) { + // All commands (ZSCORE, GET, HGET) return scalar values + if (!reader.IsScalar) + { + value = false; + return false; + } + switch (type) { case RedisType.SortedSet: + // ZSCORE returns bulk string (score as double) or null var parsedValue = RedisValue.Null; - if (!result.IsNull && result.TryGetDouble(out var val)) + if (!reader.IsNull && reader.TryReadDouble(out var val)) { parsedValue = val; } @@ -657,19 +685,12 @@ internal override bool TryValidate(in RawResult result, out bool value) return true; default: - switch (result.Resp2TypeBulkString) - { - case ResultType.BulkString: - case ResultType.SimpleString: - case ResultType.Integer: - var parsed = result.AsRedisValue(); - value = (parsed == expectedValue) == expectedEqual; - ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + (string?)expectedValue + - "; wanted: " + (expectedEqual ? "==" : "!=") + "; voting: " + value); - return true; - } - value = false; - return false; + // GET or HGET returns bulk string, simple string, or integer + var parsed = reader.ReadRedisValue(); + value = (parsed == expectedValue) == expectedEqual; + ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + (string?)expectedValue + + "; wanted: " + (expectedEqual ? "==" : "!=") + "; voting: " + value); + return true; } } } @@ -711,26 +732,24 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key); - internal override bool TryValidate(in RawResult result, out bool value) + internal override bool TryValidate(ref RespReader reader, out bool value) { - switch (result.Resp2TypeBulkString) + // LINDEX returns bulk string, simple string, or integer + if (reader.IsScalar) { - case ResultType.BulkString: - case ResultType.SimpleString: - case ResultType.Integer: - var parsed = result.AsRedisValue(); - if (expectedValue.HasValue) - { - value = (parsed == expectedValue.Value) == expectedResult; - ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + (string?)expectedValue.Value + - "; wanted: " + (expectedResult ? "==" : "!=") + "; voting: " + value); - } - else - { - value = parsed.IsNull != expectedResult; - ConnectionMultiplexer.TraceWithoutContext("exists: " + parsed + "; expected: " + expectedResult + "; voting: " + value); - } - return true; + var parsed = reader.ReadRedisValue(); + if (expectedValue.HasValue) + { + value = (parsed == expectedValue.Value) == expectedResult; + ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + (string?)expectedValue.Value + + "; wanted: " + (expectedResult ? "==" : "!=") + "; voting: " + value); + } + else + { + value = parsed.IsNull != expectedResult; + ConnectionMultiplexer.TraceWithoutContext("exists: " + parsed + "; expected: " + expectedResult + "; voting: " + value); + } + return true; } value = false; return false; @@ -784,18 +803,15 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key); - internal override bool TryValidate(in RawResult result, out bool value) + internal override bool TryValidate(ref RespReader reader, out bool value) { - switch (result.Resp2TypeBulkString) + // Length commands (HLEN, SCARD, LLEN, ZCARD, XLEN, STRLEN) return integer + if (reader.IsScalar && reader.TryReadInt64(out var parsed)) { - case ResultType.BulkString: - case ResultType.SimpleString: - case ResultType.Integer: - var parsed = result.AsRedisValue(); - value = parsed.IsInteger && (expectedLength.CompareTo((long)parsed) == compareToResult); - ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + expectedLength + - "; wanted: " + GetComparisonString() + "; voting: " + value); - return true; + value = expectedLength.CompareTo(parsed) == compareToResult; + ConnectionMultiplexer.TraceWithoutContext("actual: " + parsed + "; expected: " + expectedLength + + "; wanted: " + GetComparisonString() + "; voting: " + value); + return true; } value = false; return false; @@ -841,18 +857,16 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key); - internal override bool TryValidate(in RawResult result, out bool value) + internal override bool TryValidate(ref RespReader reader, out bool value) { - switch (result.Resp2TypeBulkString) + // Length commands (HLEN, SCARD, LLEN, ZCARD, XLEN, STRLEN) return integer + if (reader.IsScalar) { - case ResultType.BulkString: - case ResultType.SimpleString: - case ResultType.Integer: - var parsed = result.AsRedisValue(); - value = parsed.IsInteger && (expectedLength.CompareTo((long)parsed) == compareToResult); - ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + expectedLength + - "; wanted: " + GetComparisonString() + "; voting: " + value); - return true; + var parsed = reader.ReadRedisValue(); + value = parsed.IsInteger && (expectedLength.CompareTo((long)parsed) == compareToResult); + ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + expectedLength + + "; wanted: " + GetComparisonString() + "; voting: " + value); + return true; } value = false; return false; @@ -898,17 +912,16 @@ internal override IEnumerable CreateMessages(int db, IResultBox? result internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key); - internal override bool TryValidate(in RawResult result, out bool value) + internal override bool TryValidate(ref RespReader reader, out bool value) { - switch (result.Resp2TypeBulkString) + // ZCOUNT returns integer + if (reader.IsScalar) { - case ResultType.Integer: - var parsedValue = result.AsRedisValue(); - value = (parsedValue == expectedValue) == expectedEqual; - ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsedValue + "; expected: " + (string?)expectedValue + "; wanted: " + (expectedEqual ? "==" : "!=") + "; voting: " + value); - return true; + var parsedValue = reader.ReadRedisValue(); + value = (parsedValue == expectedValue) == expectedEqual; + ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsedValue + "; expected: " + (string?)expectedValue + "; wanted: " + (expectedEqual ? "==" : "!=") + "; voting: " + value); + return true; } - value = false; return false; } diff --git a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs index 2cdf1f418..b2337e87d 100644 --- a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs +++ b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using RESPite; using RESPite.Buffers; using RESPite.Messages; using static StackExchange.Redis.PhysicalConnection; @@ -83,12 +84,12 @@ private static bool IsArrayOutOfBand(in RespReader source) ? tmp : StackCopyLengthChecked(in reader, stackalloc byte[MAX_TYPE_LEN]); - var hash = span.Hash64(); + var hash = FastHash.HashCS(span); switch (hash) { - case PushMessage.Hash when PushMessage.Is(hash, span) & len >= 3: - case PushPMessage.Hash when PushPMessage.Is(hash, span) & len >= 4: - case PushSMessage.Hash when PushSMessage.Is(hash, span) & len >= 3: + case PushMessage.HashCS when PushMessage.IsCS(hash, span) & len >= 3: + case PushPMessage.HashCS when PushPMessage.IsCS(hash, span) & len >= 4: + case PushSMessage.HashCS when PushSMessage.IsCS(hash, span) & len >= 3: return true; } } diff --git a/src/StackExchange.Redis/HotKeys.ResultProcessor.cs b/src/StackExchange.Redis/HotKeys.ResultProcessor.cs index d819e6dee..44409744a 100644 --- a/src/StackExchange.Redis/HotKeys.ResultProcessor.cs +++ b/src/StackExchange.Redis/HotKeys.ResultProcessor.cs @@ -1,4 +1,8 @@ -namespace StackExchange.Redis; +using System; +using RESPite; +using RESPite.Messages; + +namespace StackExchange.Redis; public sealed partial class HotKeysResult { @@ -6,21 +10,22 @@ public sealed partial class HotKeysResult private sealed class HotKeysResultProcessor : ResultProcessor { - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { - if (result.IsNull) + if (reader.IsNull) { SetResult(message, null); return true; } // an array with a single element that *is* an array/map that is the results - if (result is { Resp2TypeArray: ResultType.Array, ItemsCount: 1 }) + if (reader.IsAggregate && reader.AggregateLengthIs(1)) { - ref readonly RawResult inner = ref result[0]; - if (inner is { Resp2TypeArray: ResultType.Array, IsNull: false }) + var iter = reader.AggregateChildren(); + iter.DemandNext(); + if (iter.Value.IsAggregate && !iter.Value.IsNull) { - var hotKeys = new HotKeysResult(in inner); + var hotKeys = new HotKeysResult(ref iter.Value); SetResult(message, hotKeys); return true; } @@ -30,158 +35,149 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - private HotKeysResult(in RawResult result) + private HotKeysResult(ref RespReader reader) { var metrics = HotKeysMetrics.None; // we infer this from the keys present - var iter = result.GetItems().GetEnumerator(); - while (iter.MoveNext()) + int count = reader.AggregateLength(); + if ((count & 1) != 0) return; // must be even (key-value pairs) + + Span keyBuffer = stackalloc byte[CommandBytes.MaxLength]; + + while (reader.TryMoveNext() && reader.IsScalar) { - ref readonly RawResult key = ref iter.Current; - if (!iter.MoveNext()) break; // lies about the length! - ref readonly RawResult value = ref iter.Current; - var hash = key.Payload.Hash64(); + var keyBytes = reader.TryGetSpan(out var tmp) ? tmp : reader.Buffer(keyBuffer); + if (keyBytes.Length > CommandBytes.MaxLength) + { + // Skip this key-value pair + if (!reader.TryMoveNext()) break; + continue; + } + + var hash = FastHash.HashCS(keyBytes); + + // Move to value + if (!reader.TryMoveNext()) break; + long i64; switch (hash) { - case tracking_active.Hash when tracking_active.Is(hash, key): - TrackingActive = value.GetBoolean(); + case tracking_active.HashCS when tracking_active.IsCS(hash, keyBytes): + TrackingActive = reader.ReadBoolean(); break; - case sample_ratio.Hash when sample_ratio.Is(hash, key) && value.TryGetInt64(out i64): + case sample_ratio.HashCS when sample_ratio.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): SampleRatio = i64; break; - case selected_slots.Hash when selected_slots.Is(hash, key) & value.Resp2TypeArray is ResultType.Array: - var len = value.ItemsCount; - if (len == 0) - { - _selectedSlots = []; - continue; - } - - var items = value.GetItems().GetEnumerator(); - var slots = len == 1 ? null : new SlotRange[len]; - for (int i = 0; i < len && items.MoveNext(); i++) - { - ref readonly RawResult pair = ref items.Current; - if (pair.Resp2TypeArray is ResultType.Array) + case selected_slots.HashCS when selected_slots.IsCS(hash, keyBytes) && reader.IsAggregate: + var slotRanges = reader.ReadPastArray( + static (ref RespReader slotReader) => { + if (!slotReader.IsAggregate) return default; + + int pairLen = slotReader.AggregateLength(); long from = -1, to = -1; - switch (pair.ItemsCount) - { - case 1 when pair[0].TryGetInt64(out from): - to = from; // single slot - break; - case 2 when pair[0].TryGetInt64(out from) && pair[1].TryGetInt64(out to): - break; - } - if (from < SlotRange.MinSlot) - { - // skip invalid ranges - } - else if (len == 1 & from == SlotRange.MinSlot & to == SlotRange.MaxSlot) + var pairIter = slotReader.AggregateChildren(); + if (pairLen >= 1 && pairIter.MoveNext() && pairIter.Value.TryReadInt64(out from)) { - // this is the "normal" case when no slot filter was applied - slots = SlotRange.SharedAllSlots; // avoid the alloc + to = from; // single slot + if (pairLen >= 2 && pairIter.MoveNext() && pairIter.Value.TryReadInt64(out to)) + { + // to is now set + } } - else - { - slots ??= new SlotRange[len]; - slots[i] = new((int)from, (int)to); - } - } + + return from >= SlotRange.MinSlot ? new SlotRange((int)from, (int)to) : default; + }, + scalar: false); + + if (slotRanges is { Length: 1 } && slotRanges[0].From == SlotRange.MinSlot && slotRanges[0].To == SlotRange.MaxSlot) + { + // this is the "normal" case when no slot filter was applied + _selectedSlots = SlotRange.SharedAllSlots; // avoid the alloc + } + else + { + _selectedSlots = slotRanges ?? []; } - _selectedSlots = slots; break; - case all_commands_all_slots_us.Hash when all_commands_all_slots_us.Is(hash, key) && value.TryGetInt64(out i64): + case all_commands_all_slots_us.HashCS when all_commands_all_slots_us.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): AllCommandsAllSlotsMicroseconds = i64; break; - case all_commands_selected_slots_us.Hash when all_commands_selected_slots_us.Is(hash, key) && value.TryGetInt64(out i64): + case all_commands_selected_slots_us.HashCS when all_commands_selected_slots_us.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): AllCommandSelectedSlotsMicroseconds = i64; break; - case sampled_command_selected_slots_us.Hash when sampled_command_selected_slots_us.Is(hash, key) && value.TryGetInt64(out i64): - case sampled_commands_selected_slots_us.Hash when sampled_commands_selected_slots_us.Is(hash, key) && value.TryGetInt64(out i64): + case sampled_command_selected_slots_us.HashCS when sampled_command_selected_slots_us.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): + case sampled_commands_selected_slots_us.HashCS when sampled_commands_selected_slots_us.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): SampledCommandsSelectedSlotsMicroseconds = i64; break; - case net_bytes_all_commands_all_slots.Hash when net_bytes_all_commands_all_slots.Is(hash, key) && value.TryGetInt64(out i64): + case net_bytes_all_commands_all_slots.HashCS when net_bytes_all_commands_all_slots.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): AllCommandsAllSlotsNetworkBytes = i64; break; - case net_bytes_all_commands_selected_slots.Hash when net_bytes_all_commands_selected_slots.Is(hash, key) && value.TryGetInt64(out i64): + case net_bytes_all_commands_selected_slots.HashCS when net_bytes_all_commands_selected_slots.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): NetworkBytesAllCommandsSelectedSlotsRaw = i64; break; - case net_bytes_sampled_commands_selected_slots.Hash when net_bytes_sampled_commands_selected_slots.Is(hash, key) && value.TryGetInt64(out i64): + case net_bytes_sampled_commands_selected_slots.HashCS when net_bytes_sampled_commands_selected_slots.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): NetworkBytesSampledCommandsSelectedSlotsRaw = i64; break; - case collection_start_time_unix_ms.Hash when collection_start_time_unix_ms.Is(hash, key) && value.TryGetInt64(out i64): + case collection_start_time_unix_ms.HashCS when collection_start_time_unix_ms.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): CollectionStartTimeUnixMilliseconds = i64; break; - case collection_duration_ms.Hash when collection_duration_ms.Is(hash, key) && value.TryGetInt64(out i64): + case collection_duration_ms.HashCS when collection_duration_ms.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): CollectionDurationMicroseconds = i64 * 1000; // ms vs us is in question: support both, and abstract it from the caller break; - case collection_duration_us.Hash when collection_duration_us.Is(hash, key) && value.TryGetInt64(out i64): + case collection_duration_us.HashCS when collection_duration_us.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): CollectionDurationMicroseconds = i64; break; - case total_cpu_time_sys_ms.Hash when total_cpu_time_sys_ms.Is(hash, key) && value.TryGetInt64(out i64): + case total_cpu_time_sys_ms.HashCS when total_cpu_time_sys_ms.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): metrics |= HotKeysMetrics.Cpu; TotalCpuTimeSystemMicroseconds = i64 * 1000; // ms vs us is in question: support both, and abstract it from the caller break; - case total_cpu_time_sys_us.Hash when total_cpu_time_sys_us.Is(hash, key) && value.TryGetInt64(out i64): + case total_cpu_time_sys_us.HashCS when total_cpu_time_sys_us.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): metrics |= HotKeysMetrics.Cpu; TotalCpuTimeSystemMicroseconds = i64; break; - case total_cpu_time_user_ms.Hash when total_cpu_time_user_ms.Is(hash, key) && value.TryGetInt64(out i64): + case total_cpu_time_user_ms.HashCS when total_cpu_time_user_ms.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): metrics |= HotKeysMetrics.Cpu; TotalCpuTimeUserMicroseconds = i64 * 1000; // ms vs us is in question: support both, and abstract it from the caller break; - case total_cpu_time_user_us.Hash when total_cpu_time_user_us.Is(hash, key) && value.TryGetInt64(out i64): + case total_cpu_time_user_us.HashCS when total_cpu_time_user_us.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): metrics |= HotKeysMetrics.Cpu; TotalCpuTimeUserMicroseconds = i64; break; - case total_net_bytes.Hash when total_net_bytes.Is(hash, key) && value.TryGetInt64(out i64): + case total_net_bytes.HashCS when total_net_bytes.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): metrics |= HotKeysMetrics.Network; TotalNetworkBytesRaw = i64; break; - case by_cpu_time_us.Hash when by_cpu_time_us.Is(hash, key) & value.Resp2TypeArray is ResultType.Array: + case by_cpu_time_us.HashCS when by_cpu_time_us.IsCS(hash, keyBytes) && reader.IsAggregate: metrics |= HotKeysMetrics.Cpu; - len = value.ItemsCount / 2; - if (len == 0) + int cpuLen = reader.AggregateLength() / 2; + var cpuTime = new MetricKeyCpu[cpuLen]; + var cpuIter = reader.AggregateChildren(); + int cpuIdx = 0; + while (cpuIter.MoveNext() && cpuIdx < cpuLen) { - _cpuByKey = []; - continue; - } - - var cpuTime = new MetricKeyCpu[len]; - items = value.GetItems().GetEnumerator(); - for (int i = 0; i < len && items.MoveNext(); i++) - { - var metricKey = items.Current.AsRedisKey(); - if (items.MoveNext() && items.Current.TryGetInt64(out var metricValue)) + var metricKey = cpuIter.Value.ReadRedisKey(); + if (cpuIter.MoveNext() && cpuIter.Value.TryReadInt64(out var metricValue)) { - cpuTime[i] = new(metricKey, metricValue); + cpuTime[cpuIdx++] = new(metricKey, metricValue); } } - _cpuByKey = cpuTime; break; - case by_net_bytes.Hash when by_net_bytes.Is(hash, key) & value.Resp2TypeArray is ResultType.Array: + case by_net_bytes.HashCS when by_net_bytes.IsCS(hash, keyBytes) && reader.IsAggregate: metrics |= HotKeysMetrics.Network; - len = value.ItemsCount / 2; - if (len == 0) + int netLen = reader.AggregateLength() / 2; + var netBytes = new MetricKeyBytes[netLen]; + var netIter = reader.AggregateChildren(); + int netIdx = 0; + while (netIter.MoveNext() && netIdx < netLen) { - _networkBytesByKey = []; - continue; - } - - var netBytes = new MetricKeyBytes[len]; - items = value.GetItems().GetEnumerator(); - for (int i = 0; i < len && items.MoveNext(); i++) - { - var metricKey = items.Current.AsRedisKey(); - if (items.MoveNext() && items.Current.TryGetInt64(out var metricValue)) + var metricKey = netIter.Value.ReadRedisKey(); + if (netIter.MoveNext() && netIter.Value.TryReadInt64(out var metricValue)) { - netBytes[i] = new(metricKey, metricValue); + netBytes[netIdx++] = new(metricKey, metricValue); } } - _networkBytesByKey = netBytes; break; } // switch diff --git a/src/StackExchange.Redis/KeyNotification.cs b/src/StackExchange.Redis/KeyNotification.cs index 3427c4dce..bfc7f5b58 100644 --- a/src/StackExchange.Redis/KeyNotification.cs +++ b/src/StackExchange.Redis/KeyNotification.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; +using RESPite; using static StackExchange.Redis.KeyNotificationChannels; namespace StackExchange.Redis; @@ -37,11 +38,11 @@ public static bool TryParse(scoped in RedisChannel channel, scoped in RedisValue { // check that the prefix is valid, i.e. "__keyspace@" or "__keyevent@" var prefix = span.Slice(0, KeySpacePrefix.Length); - var hash = prefix.Hash64(); + var hash = FastHash.HashCS(prefix); switch (hash) { - case KeySpacePrefix.Hash when KeySpacePrefix.Is(hash, prefix): - case KeyEventPrefix.Hash when KeyEventPrefix.Is(hash, prefix): + case KeySpacePrefix.HashCS when KeySpacePrefix.IsCS(hash, prefix): + case KeyEventPrefix.HashCS when KeyEventPrefix.IsCS(hash, prefix): // check that there is *something* non-empty after the prefix, with __: as the suffix (we don't verify *what*) if (span.Slice(KeySpacePrefix.Length).IndexOf("__:"u8) > 0) { @@ -442,7 +443,7 @@ public bool IsKeySpace get { var span = _channel.Span; - return span.Length >= KeySpacePrefix.Length + MinSuffixBytes && KeySpacePrefix.Is(span.Hash64(), span.Slice(0, KeySpacePrefix.Length)); + return span.Length >= KeySpacePrefix.Length + MinSuffixBytes && KeySpacePrefix.IsCS(FastHash.HashCS(span), span.Slice(0, KeySpacePrefix.Length)); } } @@ -454,7 +455,7 @@ public bool IsKeyEvent get { var span = _channel.Span; - return span.Length >= KeyEventPrefix.Length + MinSuffixBytes && KeyEventPrefix.Is(span.Hash64(), span.Slice(0, KeyEventPrefix.Length)); + return span.Length >= KeyEventPrefix.Length + MinSuffixBytes && KeyEventPrefix.IsCS(FastHash.HashCS(span), span.Slice(0, KeyEventPrefix.Length)); } } diff --git a/src/StackExchange.Redis/KeyNotificationTypeFastHash.cs b/src/StackExchange.Redis/KeyNotificationTypeFastHash.cs index bcf08bad2..57e0d1964 100644 --- a/src/StackExchange.Redis/KeyNotificationTypeFastHash.cs +++ b/src/StackExchange.Redis/KeyNotificationTypeFastHash.cs @@ -1,4 +1,5 @@ using System; +using RESPite; namespace StackExchange.Redis; @@ -12,63 +13,63 @@ internal static partial class KeyNotificationTypeFastHash public static KeyNotificationType Parse(ReadOnlySpan value) { - var hash = value.Hash64(); + var hash = FastHash.HashCS(value); return hash switch { - append.Hash when append.Is(hash, value) => KeyNotificationType.Append, - copy.Hash when copy.Is(hash, value) => KeyNotificationType.Copy, - del.Hash when del.Is(hash, value) => KeyNotificationType.Del, - expire.Hash when expire.Is(hash, value) => KeyNotificationType.Expire, - hdel.Hash when hdel.Is(hash, value) => KeyNotificationType.HDel, - hexpired.Hash when hexpired.Is(hash, value) => KeyNotificationType.HExpired, - hincrbyfloat.Hash when hincrbyfloat.Is(hash, value) => KeyNotificationType.HIncrByFloat, - hincrby.Hash when hincrby.Is(hash, value) => KeyNotificationType.HIncrBy, - hpersist.Hash when hpersist.Is(hash, value) => KeyNotificationType.HPersist, - hset.Hash when hset.Is(hash, value) => KeyNotificationType.HSet, - incrbyfloat.Hash when incrbyfloat.Is(hash, value) => KeyNotificationType.IncrByFloat, - incrby.Hash when incrby.Is(hash, value) => KeyNotificationType.IncrBy, - linsert.Hash when linsert.Is(hash, value) => KeyNotificationType.LInsert, - lpop.Hash when lpop.Is(hash, value) => KeyNotificationType.LPop, - lpush.Hash when lpush.Is(hash, value) => KeyNotificationType.LPush, - lrem.Hash when lrem.Is(hash, value) => KeyNotificationType.LRem, - lset.Hash when lset.Is(hash, value) => KeyNotificationType.LSet, - ltrim.Hash when ltrim.Is(hash, value) => KeyNotificationType.LTrim, - move_from.Hash when move_from.Is(hash, value) => KeyNotificationType.MoveFrom, - move_to.Hash when move_to.Is(hash, value) => KeyNotificationType.MoveTo, - persist.Hash when persist.Is(hash, value) => KeyNotificationType.Persist, - rename_from.Hash when rename_from.Is(hash, value) => KeyNotificationType.RenameFrom, - rename_to.Hash when rename_to.Is(hash, value) => KeyNotificationType.RenameTo, - restore.Hash when restore.Is(hash, value) => KeyNotificationType.Restore, - rpop.Hash when rpop.Is(hash, value) => KeyNotificationType.RPop, - rpush.Hash when rpush.Is(hash, value) => KeyNotificationType.RPush, - sadd.Hash when sadd.Is(hash, value) => KeyNotificationType.SAdd, - set.Hash when set.Is(hash, value) => KeyNotificationType.Set, - setrange.Hash when setrange.Is(hash, value) => KeyNotificationType.SetRange, - sortstore.Hash when sortstore.Is(hash, value) => KeyNotificationType.SortStore, - srem.Hash when srem.Is(hash, value) => KeyNotificationType.SRem, - spop.Hash when spop.Is(hash, value) => KeyNotificationType.SPop, - xadd.Hash when xadd.Is(hash, value) => KeyNotificationType.XAdd, - xdel.Hash when xdel.Is(hash, value) => KeyNotificationType.XDel, - xgroup_createconsumer.Hash when xgroup_createconsumer.Is(hash, value) => KeyNotificationType.XGroupCreateConsumer, - xgroup_create.Hash when xgroup_create.Is(hash, value) => KeyNotificationType.XGroupCreate, - xgroup_delconsumer.Hash when xgroup_delconsumer.Is(hash, value) => KeyNotificationType.XGroupDelConsumer, - xgroup_destroy.Hash when xgroup_destroy.Is(hash, value) => KeyNotificationType.XGroupDestroy, - xgroup_setid.Hash when xgroup_setid.Is(hash, value) => KeyNotificationType.XGroupSetId, - xsetid.Hash when xsetid.Is(hash, value) => KeyNotificationType.XSetId, - xtrim.Hash when xtrim.Is(hash, value) => KeyNotificationType.XTrim, - zadd.Hash when zadd.Is(hash, value) => KeyNotificationType.ZAdd, - zdiffstore.Hash when zdiffstore.Is(hash, value) => KeyNotificationType.ZDiffStore, - zinterstore.Hash when zinterstore.Is(hash, value) => KeyNotificationType.ZInterStore, - zunionstore.Hash when zunionstore.Is(hash, value) => KeyNotificationType.ZUnionStore, - zincr.Hash when zincr.Is(hash, value) => KeyNotificationType.ZIncr, - zrembyrank.Hash when zrembyrank.Is(hash, value) => KeyNotificationType.ZRemByRank, - zrembyscore.Hash when zrembyscore.Is(hash, value) => KeyNotificationType.ZRemByScore, - zrem.Hash when zrem.Is(hash, value) => KeyNotificationType.ZRem, - expired.Hash when expired.Is(hash, value) => KeyNotificationType.Expired, - evicted.Hash when evicted.Is(hash, value) => KeyNotificationType.Evicted, - _new.Hash when _new.Is(hash, value) => KeyNotificationType.New, - overwritten.Hash when overwritten.Is(hash, value) => KeyNotificationType.Overwritten, - type_changed.Hash when type_changed.Is(hash, value) => KeyNotificationType.TypeChanged, + append.HashCS when append.IsCS(hash, value) => KeyNotificationType.Append, + copy.HashCS when copy.IsCS(hash, value) => KeyNotificationType.Copy, + del.HashCS when del.IsCS(hash, value) => KeyNotificationType.Del, + expire.HashCS when expire.IsCS(hash, value) => KeyNotificationType.Expire, + hdel.HashCS when hdel.IsCS(hash, value) => KeyNotificationType.HDel, + hexpired.HashCS when hexpired.IsCS(hash, value) => KeyNotificationType.HExpired, + hincrbyfloat.HashCS when hincrbyfloat.IsCS(hash, value) => KeyNotificationType.HIncrByFloat, + hincrby.HashCS when hincrby.IsCS(hash, value) => KeyNotificationType.HIncrBy, + hpersist.HashCS when hpersist.IsCS(hash, value) => KeyNotificationType.HPersist, + hset.HashCS when hset.IsCS(hash, value) => KeyNotificationType.HSet, + incrbyfloat.HashCS when incrbyfloat.IsCS(hash, value) => KeyNotificationType.IncrByFloat, + incrby.HashCS when incrby.IsCS(hash, value) => KeyNotificationType.IncrBy, + linsert.HashCS when linsert.IsCS(hash, value) => KeyNotificationType.LInsert, + lpop.HashCS when lpop.IsCS(hash, value) => KeyNotificationType.LPop, + lpush.HashCS when lpush.IsCS(hash, value) => KeyNotificationType.LPush, + lrem.HashCS when lrem.IsCS(hash, value) => KeyNotificationType.LRem, + lset.HashCS when lset.IsCS(hash, value) => KeyNotificationType.LSet, + ltrim.HashCS when ltrim.IsCS(hash, value) => KeyNotificationType.LTrim, + move_from.HashCS when move_from.IsCS(hash, value) => KeyNotificationType.MoveFrom, + move_to.HashCS when move_to.IsCS(hash, value) => KeyNotificationType.MoveTo, + persist.HashCS when persist.IsCS(hash, value) => KeyNotificationType.Persist, + rename_from.HashCS when rename_from.IsCS(hash, value) => KeyNotificationType.RenameFrom, + rename_to.HashCS when rename_to.IsCS(hash, value) => KeyNotificationType.RenameTo, + restore.HashCS when restore.IsCS(hash, value) => KeyNotificationType.Restore, + rpop.HashCS when rpop.IsCS(hash, value) => KeyNotificationType.RPop, + rpush.HashCS when rpush.IsCS(hash, value) => KeyNotificationType.RPush, + sadd.HashCS when sadd.IsCS(hash, value) => KeyNotificationType.SAdd, + set.HashCS when set.IsCS(hash, value) => KeyNotificationType.Set, + setrange.HashCS when setrange.IsCS(hash, value) => KeyNotificationType.SetRange, + sortstore.HashCS when sortstore.IsCS(hash, value) => KeyNotificationType.SortStore, + srem.HashCS when srem.IsCS(hash, value) => KeyNotificationType.SRem, + spop.HashCS when spop.IsCS(hash, value) => KeyNotificationType.SPop, + xadd.HashCS when xadd.IsCS(hash, value) => KeyNotificationType.XAdd, + xdel.HashCS when xdel.IsCS(hash, value) => KeyNotificationType.XDel, + xgroup_createconsumer.HashCS when xgroup_createconsumer.IsCS(hash, value) => KeyNotificationType.XGroupCreateConsumer, + xgroup_create.HashCS when xgroup_create.IsCS(hash, value) => KeyNotificationType.XGroupCreate, + xgroup_delconsumer.HashCS when xgroup_delconsumer.IsCS(hash, value) => KeyNotificationType.XGroupDelConsumer, + xgroup_destroy.HashCS when xgroup_destroy.IsCS(hash, value) => KeyNotificationType.XGroupDestroy, + xgroup_setid.HashCS when xgroup_setid.IsCS(hash, value) => KeyNotificationType.XGroupSetId, + xsetid.HashCS when xsetid.IsCS(hash, value) => KeyNotificationType.XSetId, + xtrim.HashCS when xtrim.IsCS(hash, value) => KeyNotificationType.XTrim, + zadd.HashCS when zadd.IsCS(hash, value) => KeyNotificationType.ZAdd, + zdiffstore.HashCS when zdiffstore.IsCS(hash, value) => KeyNotificationType.ZDiffStore, + zinterstore.HashCS when zinterstore.IsCS(hash, value) => KeyNotificationType.ZInterStore, + zunionstore.HashCS when zunionstore.IsCS(hash, value) => KeyNotificationType.ZUnionStore, + zincr.HashCS when zincr.IsCS(hash, value) => KeyNotificationType.ZIncr, + zrembyrank.HashCS when zrembyrank.IsCS(hash, value) => KeyNotificationType.ZRemByRank, + zrembyscore.HashCS when zrembyscore.IsCS(hash, value) => KeyNotificationType.ZRemByScore, + zrem.HashCS when zrem.IsCS(hash, value) => KeyNotificationType.ZRem, + expired.HashCS when expired.IsCS(hash, value) => KeyNotificationType.Expired, + evicted.HashCS when evicted.IsCS(hash, value) => KeyNotificationType.Evicted, + _new.HashCS when _new.IsCS(hash, value) => KeyNotificationType.New, + overwritten.HashCS when overwritten.IsCS(hash, value) => KeyNotificationType.Overwritten, + type_changed.HashCS when type_changed.IsCS(hash, value) => KeyNotificationType.TypeChanged, _ => KeyNotificationType.Unknown, }; } diff --git a/src/StackExchange.Redis/PhysicalConnection.Read.cs b/src/StackExchange.Redis/PhysicalConnection.Read.cs index 299c40930..25bbdd296 100644 --- a/src/StackExchange.Redis/PhysicalConnection.Read.cs +++ b/src/StackExchange.Redis/PhysicalConnection.Read.cs @@ -9,6 +9,7 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using RESPite; using RESPite.Buffers; using RESPite.Internal; using RESPite.Messages; @@ -293,13 +294,13 @@ static bool IsArrayPong(ReadOnlySpan payload) { if (payload.Length >= sizeof(ulong)) { - var hash = payload.Hash64(); + var hash = FastHash.HashCS(payload); switch (hash) { - case ArrayPong_LC_Bulk.Hash when payload.StartsWith(ArrayPong_LC_Bulk.U8): - case ArrayPong_UC_Bulk.Hash when payload.StartsWith(ArrayPong_UC_Bulk.U8): - case ArrayPong_LC_Simple.Hash when payload.StartsWith(ArrayPong_LC_Simple.U8): - case ArrayPong_UC_Simple.Hash when payload.StartsWith(ArrayPong_UC_Simple.U8): + case ArrayPong_LC_Bulk.HashCS when payload.StartsWith(ArrayPong_LC_Bulk.U8): + case ArrayPong_UC_Bulk.HashCS when payload.StartsWith(ArrayPong_UC_Bulk.U8): + case ArrayPong_LC_Simple.HashCS when payload.StartsWith(ArrayPong_LC_Simple.U8): + case ArrayPong_UC_Simple.HashCS when payload.StartsWith(ArrayPong_UC_Simple.U8): var reader = new RespReader(payload); return reader.SafeTryMoveNext() // have root && reader.Prefix == RespPrefix.Array // root is array @@ -350,41 +351,41 @@ private bool OnOutOfBand(ReadOnlySpan payload, ref byte[]? lease) var span = reader.TryGetSpan(out var tmp) ? tmp : StackCopyLengthChecked(in reader, stackalloc byte[MAX_TYPE_LEN]); - var hash = span.Hash64(); + var hash = FastHash.HashCS(span); RedisChannel.RedisChannelOptions channelOptions = RedisChannel.RedisChannelOptions.None; PushKind kind; switch (hash) { - case PushMessage.Hash when PushMessage.Is(hash, span) & len >= 3: + case PushMessage.HashCS when PushMessage.IsCS(hash, span) & len >= 3: kind = PushKind.Message; break; - case PushPMessage.Hash when PushPMessage.Is(hash, span) & len >= 4: + case PushPMessage.HashCS when PushPMessage.IsCS(hash, span) & len >= 4: channelOptions = RedisChannel.RedisChannelOptions.Pattern; kind = PushKind.PMessage; break; - case PushSMessage.Hash when PushSMessage.Is(hash, span) & len >= 3: + case PushSMessage.HashCS when PushSMessage.IsCS(hash, span) & len >= 3: channelOptions = RedisChannel.RedisChannelOptions.Sharded; kind = PushKind.SMessage; break; - case PushSubscribe.Hash when PushSubscribe.Is(hash, span): + case PushSubscribe.HashCS when PushSubscribe.IsCS(hash, span): kind = PushKind.Subscribe; break; - case PushPSubscribe.Hash when PushPSubscribe.Is(hash, span): + case PushPSubscribe.HashCS when PushPSubscribe.IsCS(hash, span): channelOptions = RedisChannel.RedisChannelOptions.Pattern; kind = PushKind.PSubscribe; break; - case PushSSubscribe.Hash when PushSSubscribe.Is(hash, span): + case PushSSubscribe.HashCS when PushSSubscribe.IsCS(hash, span): channelOptions = RedisChannel.RedisChannelOptions.Sharded; kind = PushKind.SSubscribe; break; - case PushUnsubscribe.Hash when PushUnsubscribe.Is(hash, span): + case PushUnsubscribe.HashCS when PushUnsubscribe.IsCS(hash, span): kind = PushKind.Unsubscribe; break; - case PushPUnsubscribe.Hash when PushPUnsubscribe.Is(hash, span): + case PushPUnsubscribe.HashCS when PushPUnsubscribe.IsCS(hash, span): channelOptions = RedisChannel.RedisChannelOptions.Pattern; kind = PushKind.PUnsubscribe; break; - case PushSUnsubscribe.Hash when PushSUnsubscribe.Is(hash, span): + case PushSUnsubscribe.HashCS when PushSUnsubscribe.IsCS(hash, span): channelOptions = RedisChannel.RedisChannelOptions.Sharded; kind = PushKind.SUnsubscribe; break; diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 2fdbc5df6..797632e41 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,2 +1,17 @@ #nullable enable +override StackExchange.Redis.LCSMatchResult.LCSMatch.Equals(object? obj) -> bool +override StackExchange.Redis.LCSMatchResult.LCSMatch.GetHashCode() -> int +override StackExchange.Redis.LCSMatchResult.LCSMatch.ToString() -> string! +override StackExchange.Redis.LCSMatchResult.LCSPosition.Equals(object? obj) -> bool +override StackExchange.Redis.LCSMatchResult.LCSPosition.GetHashCode() -> int +override StackExchange.Redis.LCSMatchResult.LCSPosition.ToString() -> string! +StackExchange.Redis.LCSMatchResult.LCSMatch.Equals(in StackExchange.Redis.LCSMatchResult.LCSMatch other) -> bool +StackExchange.Redis.LCSMatchResult.LCSMatch.First.get -> StackExchange.Redis.LCSMatchResult.LCSPosition +StackExchange.Redis.LCSMatchResult.LCSMatch.Second.get -> StackExchange.Redis.LCSMatchResult.LCSPosition +StackExchange.Redis.LCSMatchResult.LCSPosition +StackExchange.Redis.LCSMatchResult.LCSPosition.End.get -> long +StackExchange.Redis.LCSMatchResult.LCSPosition.Equals(in StackExchange.Redis.LCSMatchResult.LCSPosition other) -> bool +StackExchange.Redis.LCSMatchResult.LCSPosition.LCSPosition() -> void +StackExchange.Redis.LCSMatchResult.LCSPosition.LCSPosition(long start, long end) -> void +StackExchange.Redis.LCSMatchResult.LCSPosition.Start.get -> long StackExchange.Redis.RedisType.VectorSet = 8 -> StackExchange.Redis.RedisType diff --git a/src/StackExchange.Redis/ResultProcessor.Lease.cs b/src/StackExchange.Redis/ResultProcessor.Lease.cs index dfee6e474..104b5df65 100644 --- a/src/StackExchange.Redis/ResultProcessor.Lease.cs +++ b/src/StackExchange.Redis/ResultProcessor.Lease.cs @@ -59,120 +59,87 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes protected abstract T TryParse(ref RespReader reader); } - private abstract class InterleavedLeaseProcessor : ResultProcessor?> - { - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) - { - if (result.Resp2TypeArray != ResultType.Array) - { - return false; // not an array - } - - // deal with null - if (result.IsNull) - { - SetResult(message, Lease.Empty); - return true; - } - - // lease and fill - var items = result.GetItems(); - var length = checked((int)items.Length) / 2; - var lease = Lease.Create(length, clear: false); // note this handles zero nicely - var target = lease.Span; - - var iter = items.GetEnumerator(); - for (int i = 0; i < target.Length; i++) - { - bool ok = iter.MoveNext(); - if (ok) - { - ref readonly RawResult first = ref iter.Current; - ok = iter.MoveNext() && TryParse(in first, in iter.Current, out target[i]); - } - if (!ok) - { - lease.Dispose(); - return false; - } - } - SetResult(message, lease); - return true; - } - - protected abstract bool TryParse(in RawResult first, in RawResult second, out T parsed); - } - // takes a nested vector of the form [[A],[B,C],[D]] and exposes it as [A,B,C,D]; this is // especially useful for VLINKS private abstract class FlattenedLeaseProcessor : ResultProcessor?> { - protected virtual long GetArrayLength(in RawResult array) => array.GetItems().Length; - - protected virtual bool TryReadOne(ref Sequence.Enumerator reader, out T value) - { - if (reader.MoveNext()) - { - return TryReadOne(in reader.Current, out value); - } - value = default!; - return false; - } + protected virtual long GetArrayLength(in RespReader reader) => reader.AggregateLength(); - protected virtual bool TryReadOne(in RawResult result, out T value) + protected virtual bool TryReadOne(ref RespReader reader, out T value) { value = default!; return false; } - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { - if (result.Resp2TypeArray != ResultType.Array) + if (!reader.IsAggregate) { return false; // not an array } - if (result.IsNull) + + // deal with null + if (reader.IsNull) { SetResult(message, Lease.Empty); return true; } - var items = result.GetItems(); - long length = 0; - foreach (ref RawResult item in items) + + // First pass: count total elements across all nested arrays + long totalLength = 0; + var iter = reader.AggregateChildren(); + while (iter.MoveNext()) { - if (item.Resp2TypeArray == ResultType.Array && !item.IsNull) + if (iter.Value.IsAggregate && !iter.Value.IsNull) { - length += GetArrayLength(in item); + totalLength += GetArrayLength(in iter.Value); } } - if (length == 0) + if (totalLength == 0) { SetResult(message, Lease.Empty); return true; } - var lease = Lease.Create(checked((int)length), clear: false); + + // Second pass: fill the lease + var lease = Lease.Create(checked((int)totalLength), clear: false); int index = 0; var target = lease.Span; - foreach (ref RawResult item in items) + + try { - if (item.Resp2TypeArray == ResultType.Array && !item.IsNull) + iter = reader.AggregateChildren(); + while (iter.MoveNext()) { - var iter = item.GetItems().GetEnumerator(); - while (index < target.Length && TryReadOne(ref iter, out target[index])) + if (iter.Value.IsAggregate && !iter.Value.IsNull) { - index++; + var childReader = iter.Value; + while (childReader.TryMoveNext() && index < target.Length) + { + if (!TryReadOne(ref childReader, out target[index])) + { + lease.Dispose(); + return false; + } + index++; + } } } - } - if (index == length) + if (index == totalLength) + { + SetResult(message, lease); + return true; + } + lease.Dispose(); // failed to fill? + return false; + } + catch { - SetResult(message, lease); - return true; + lease.Dispose(); + throw; } - lease.Dispose(); // failed to fill? - return false; } } diff --git a/src/StackExchange.Redis/ResultProcessor.Literals.cs b/src/StackExchange.Redis/ResultProcessor.Literals.cs index 1d7c8901f..00c375a35 100644 --- a/src/StackExchange.Redis/ResultProcessor.Literals.cs +++ b/src/StackExchange.Redis/ResultProcessor.Literals.cs @@ -1,4 +1,6 @@ -namespace StackExchange.Redis; +using RESPite; + +namespace StackExchange.Redis; internal partial class ResultProcessor { @@ -38,6 +40,24 @@ [FastHash] internal static partial class master { } [FastHash] internal static partial class slave { } [FastHash] internal static partial class replica { } [FastHash] internal static partial class sentinel { } + [FastHash] internal static partial class primary { } + [FastHash] internal static partial class standalone { } + [FastHash] internal static partial class cluster { } + + // Config keys + [FastHash] internal static partial class timeout { } + [FastHash] internal static partial class databases { } + [FastHash("slave-read-only")] internal static partial class slave_read_only { } + [FastHash("replica-read-only")] internal static partial class replica_read_only { } + [FastHash] internal static partial class yes { } + [FastHash] internal static partial class no { } + + // HELLO keys + [FastHash] internal static partial class version { } + [FastHash] internal static partial class proto { } + [FastHash] internal static partial class id { } + [FastHash] internal static partial class mode { } + [FastHash] internal static partial class role { } // Replication states [FastHash] internal static partial class connect { } @@ -46,6 +66,48 @@ [FastHash] internal static partial class sync { } [FastHash] internal static partial class connected { } [FastHash] internal static partial class none { } [FastHash] internal static partial class handshake { } + + // Result processor literals + [FastHash] + internal static partial class OK + { + public static readonly FastHash Hash = new(U8); + } + + [FastHash] + internal static partial class PONG + { + public static readonly FastHash Hash = new(U8); + } + + [FastHash("Background saving started")] + internal static partial class background_saving_started + { + public static readonly FastHash Hash = new(U8); + } + + [FastHash("Background append only file rewriting started")] + internal static partial class background_aof_rewriting_started + { + public static readonly FastHash Hash = new(U8); + } + + // LCS processor literals + [FastHash] internal static partial class matches { } + [FastHash] internal static partial class len { } + + // Sentinel processor literals + [FastHash] internal static partial class ip { } + [FastHash] internal static partial class port { } + + // Stream info processor literals + [FastHash] internal static partial class name { } + [FastHash] internal static partial class pending { } + [FastHash] internal static partial class idle { } + [FastHash] internal static partial class consumers { } + [FastHash("last-delivered-id")] internal static partial class last_delivered_id { } + [FastHash("entries-read")] internal static partial class entries_read { } + [FastHash] internal static partial class lag { } // ReSharper restore InconsistentNaming #pragma warning restore CS8981, SA1300, SA1134 // forgive naming etc } diff --git a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs index d4a5ac733..3b7d779e9 100644 --- a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs +++ b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs @@ -2,6 +2,7 @@ using System; using Pipelines.Sockets.Unofficial.Arenas; +using RESPite; using RESPite.Messages; namespace StackExchange.Redis; @@ -18,30 +19,39 @@ internal abstract partial class ResultProcessor private sealed class VectorSetLinksWithScoresProcessor : FlattenedLeaseProcessor { - protected override long GetArrayLength(in RawResult array) => array.GetItems().Length / 2; + protected override long GetArrayLength(in RespReader reader) => reader.AggregateLength() / 2; - protected override bool TryReadOne(ref Sequence.Enumerator reader, out VectorSetLink value) + protected override bool TryReadOne(ref RespReader reader, out VectorSetLink value) { - if (reader.MoveNext()) + if (!reader.IsScalar) { - ref readonly RawResult first = ref reader.Current; - if (reader.MoveNext() && reader.Current.TryGetDouble(out var score)) - { - value = new VectorSetLink(first.AsRedisValue(), score); - return true; - } + value = default; + return false; + } + + var member = reader.ReadRedisValue(); + if (!reader.TryMoveNext() || !reader.IsScalar || !reader.TryReadDouble(out var score)) + { + value = default; + return false; } - value = default; - return false; + value = new VectorSetLink(member, score); + return true; } } private sealed class VectorSetLinksProcessor : FlattenedLeaseProcessor { - protected override bool TryReadOne(in RawResult result, out RedisValue value) + protected override bool TryReadOne(ref RespReader reader, out RedisValue value) { - value = result.AsRedisValue(); + if (!reader.IsScalar) + { + value = default; + return false; + } + + value = reader.ReadRedisValue(); return true; } } @@ -87,36 +97,36 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes continue; } - var hash = testBytes.Hash64(); // this still contains the key, even though we've advanced + var hash = FastHash.HashCS(testBytes); // this still contains the key, even though we've advanced switch (hash) { - case size.Hash when size.Is(hash, testBytes) && reader.TryReadInt64(out var i64): + case size.HashCS when size.IsCS(hash, testBytes) && reader.TryReadInt64(out var i64): resultSize = i64; break; - case vset_uid.Hash when vset_uid.Is(hash, testBytes) && reader.TryReadInt64(out var i64): + case vset_uid.HashCS when vset_uid.IsCS(hash, testBytes) && reader.TryReadInt64(out var i64): vsetUid = i64; break; - case max_level.Hash when max_level.Is(hash, testBytes) && reader.TryReadInt64(out var i64): + case max_level.HashCS when max_level.IsCS(hash, testBytes) && reader.TryReadInt64(out var i64): maxLevel = checked((int)i64); break; - case vector_dim.Hash when vector_dim.Is(hash, testBytes) && reader.TryReadInt64(out var i64): + case vector_dim.HashCS when vector_dim.IsCS(hash, testBytes) && reader.TryReadInt64(out var i64): vectorDim = checked((int)i64); break; - case quant_type.Hash when quant_type.Is(hash, testBytes): + case quant_type.HashCS when quant_type.IsCS(hash, testBytes): len = reader.ScalarLength(); testBytes = (len > stackBuffer.Length | reader.IsNull) ? default : reader.TryGetSpan(out tmp) ? tmp : reader.Buffer(stackBuffer); - hash = testBytes.Hash64(); + hash = FastHash.HashCS(testBytes); switch (hash) { - case bin.Hash when bin.Is(hash, testBytes): + case bin.HashCS when bin.IsCS(hash, testBytes): quantType = VectorSetQuantization.Binary; break; - case f32.Hash when f32.Is(hash, testBytes): + case f32.HashCS when f32.IsCS(hash, testBytes): quantType = VectorSetQuantization.None; break; - case int8.Hash when int8.Is(hash, testBytes): + case int8.HashCS when int8.IsCS(hash, testBytes): quantType = VectorSetQuantization.Int8; break; default: @@ -125,7 +135,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes break; } break; - case hnsw_max_node_uid.Hash when hnsw_max_node_uid.Is(hash, testBytes) && reader.TryReadInt64(out var i64): + case hnsw_max_node_uid.HashCS when hnsw_max_node_uid.IsCS(hash, testBytes) && reader.TryReadInt64(out var i64): hnswMaxNodeUid = i64; break; } diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 826c1aa69..8e09b61f0 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -11,6 +11,7 @@ using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Pipelines.Sockets.Unofficial.Arenas; +using RESPite; using RESPite.Messages; namespace StackExchange.Redis @@ -19,15 +20,15 @@ internal abstract partial class ResultProcessor { public static readonly ResultProcessor Boolean = new BooleanProcessor(), - DemandOK = new ExpectBasicStringProcessor(CommonReplies.OK), - DemandPONG = new ExpectBasicStringProcessor(CommonReplies.PONG), + DemandOK = new ExpectBasicStringProcessor(Literals.OK.Hash), + DemandPONG = new ExpectBasicStringProcessor(Literals.PONG.Hash), DemandZeroOrOne = new DemandZeroOrOneProcessor(), AutoConfigure = new AutoConfigureProcessor(), TrackSubscriptions = new TrackSubscriptionsProcessor(null), Tracer = new TracerProcessor(false), EstablishConnection = new TracerProcessor(true), - BackgroundSaveStarted = new ExpectBasicStringProcessor(CommonReplies.backgroundSavingStarted_trimmed, startsWith: true), - BackgroundSaveAOFStarted = new ExpectBasicStringProcessor(CommonReplies.backgroundSavingAOFStarted_trimmed, startsWith: true); + BackgroundSaveStarted = new ExpectBasicStringProcessor(Literals.background_saving_started.Hash, startsWith: true), + BackgroundSaveAOFStarted = new ExpectBasicStringProcessor(Literals.background_aof_rewriting_started.Hash, startsWith: true); public static readonly ResultProcessor ByteArray = new ByteArrayProcessor(); @@ -353,7 +354,7 @@ protected virtual bool SetResultCore(PhysicalConnection connection, Message mess return SetResultCore(connection, message, rawResult); } - private static RawResult AsRaw(ref RespReader reader, bool resp3) + internal static RawResult AsRaw(ref RespReader reader, bool resp3) { var flags = RawResult.ResultFlags.HasValue; if (!reader.IsNull) flags |= RawResult.ResultFlags.NonNull; @@ -517,32 +518,41 @@ public sealed class TrackSubscriptionsProcessor : ResultProcessor private ConnectionMultiplexer.Subscription? Subscription { get; } public TrackSubscriptionsProcessor(ConnectionMultiplexer.Subscription? sub) => Subscription = sub; - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { - if (result.Resp2TypeArray == ResultType.Array) + if (reader.IsAggregate) { - var items = result.GetItems(); - if (items.Length >= 3 && items[2].TryGetInt64(out long count)) + int length = reader.AggregateLength(); + if (length >= 3) { - connection.SubscriptionCount = count; - SetResult(message, true); + var iter = reader.AggregateChildren(); + // Skip first two elements + iter.DemandNext(); // [0] + iter.DemandNext(); // [1] + iter.DemandNext(); // [2] - the count - var ep = connection.BridgeCouldBeNull?.ServerEndPoint; - if (ep is not null) + if (iter.Value.TryReadInt64(out long count)) { - switch (message.Command) + connection.SubscriptionCount = count; + SetResult(message, true); + + var ep = connection.BridgeCouldBeNull?.ServerEndPoint; + if (ep is not null) { - case RedisCommand.SUBSCRIBE: - case RedisCommand.SSUBSCRIBE: - case RedisCommand.PSUBSCRIBE: - Subscription?.AddEndpoint(ep); - break; - default: - Subscription?.TryRemoveEndpoint(ep); - break; + switch (message.Command) + { + case RedisCommand.SUBSCRIBE: + case RedisCommand.SSUBSCRIBE: + case RedisCommand.PSUBSCRIBE: + Subscription?.AddEndpoint(ep); + break; + default: + Subscription?.TryRemoveEndpoint(ep); + break; + } } + return true; } - return true; } } SetResult(message, false); @@ -575,9 +585,30 @@ public static bool TryGet(in RawResult result, out bool value) return false; } - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + public static bool TryGet(ref RespReader reader, out bool value) { - if (TryGet(result, out bool value)) + if (reader.IsScalar && reader.ScalarLengthIs(1)) + { + var span = reader.TryGetSpan(out var tmp) ? tmp : reader.Buffer(stackalloc byte[1]); + var byteValue = span[0]; + if (byteValue == (byte)'1') + { + value = true; + return true; + } + else if (byteValue == (byte)'0') + { + value = false; + return true; + } + } + value = false; + return false; + } + + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) + { + if (TryGet(ref reader, out bool value)) { SetResult(message, value); return true; @@ -967,40 +998,38 @@ public override bool SetResult(PhysicalConnection connection, Message message, r return base.SetResult(connection, message, ref copy); } - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { var server = connection.BridgeCouldBeNull?.ServerEndPoint; if (server == null) return false; - switch (result.Resp2TypeBulkString) + // Handle CLIENT command (returns integer client ID) + if (message?.Command == RedisCommand.CLIENT && reader.Prefix == RespPrefix.Integer) { - case ResultType.Integer: - if (message?.Command == RedisCommand.CLIENT) - { - if (result.TryGetInt64(out long clientId)) - { - connection.ConnectionId = clientId; - Log?.LogInformationAutoConfiguredClientConnectionId(new(server), clientId); + if (reader.TryReadInt64(out long clientId)) + { + connection.ConnectionId = clientId; + Log?.LogInformationAutoConfiguredClientConnectionId(new(server), clientId); + SetResult(message, true); + return true; + } + return false; + } - SetResult(message, true); - return true; - } - } - break; - case ResultType.BulkString: - if (message?.Command == RedisCommand.INFO) - { - string? info = result.GetString(); - if (string.IsNullOrWhiteSpace(info)) - { - SetResult(message, true); - return true; - } - string? primaryHost = null, primaryPort = null; - bool roleSeen = false; - using (var reader = new StringReader(info)) - { - while (reader.ReadLine() is string line) + // Handle INFO command (returns bulk string) + if (message?.Command == RedisCommand.INFO && reader.IsScalar) + { + string? info = reader.ReadString(); + if (string.IsNullOrWhiteSpace(info)) + { + SetResult(message, true); + return true; + } + string? primaryHost = null, primaryPort = null; + bool roleSeen = false; + using (var stringReader = new StringReader(info)) + { + while (stringReader.ReadLine() is string line) { if (string.IsNullOrWhiteSpace(line) || line.StartsWith("# ")) { @@ -1046,66 +1075,77 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes server.RunId = val; } } - if (roleSeen && Format.TryParseEndPoint(primaryHost!, primaryPort, out var sep)) - { - // These are in the same section, if present - server.PrimaryEndPoint = sep; - } - } - } - else if (message?.Command == RedisCommand.SENTINEL) + if (roleSeen && Format.TryParseEndPoint(primaryHost!, primaryPort, out var sep)) { - server.ServerType = ServerType.Sentinel; - Log?.LogInformationAutoConfiguredSentinelServerType(new(server)); + // These are in the same section, if present + server.PrimaryEndPoint = sep; } - SetResult(message, true); - return true; - case ResultType.Array: - if (message?.Command == RedisCommand.CONFIG) + } + SetResult(message, true); + return true; + } + + // Handle SENTINEL command (returns bulk string) + if (message?.Command == RedisCommand.SENTINEL && reader.IsScalar) + { + server.ServerType = ServerType.Sentinel; + Log?.LogInformationAutoConfiguredSentinelServerType(new(server)); + SetResult(message, true); + return true; + } + + // Handle CONFIG command (returns array of key-value pairs) + if (message?.Command == RedisCommand.CONFIG && reader.IsAggregate) + { + var iter = reader.AggregateChildren(); + while (iter.MoveNext()) + { + var key = iter.Value; + if (!iter.MoveNext()) break; + var val = iter.Value; + + if (key.TryGetSpan(out var keySpan)) { - var iter = result.GetItems().GetEnumerator(); - while (iter.MoveNext()) + var keyHash = FastHash.HashCS(keySpan); + if (Literals.timeout.IsCS(keyHash, keySpan) && val.TryReadInt64(out long i64)) { - ref RawResult key = ref iter.Current; - if (!iter.MoveNext()) break; - ref RawResult val = ref iter.Current; - - if (key.IsEqual(CommonReplies.timeout) && val.TryGetInt64(out long i64)) + // note the configuration is in seconds + int timeoutSeconds = checked((int)i64), targetSeconds; + if (timeoutSeconds > 0) { - // note the configuration is in seconds - int timeoutSeconds = checked((int)i64), targetSeconds; - if (timeoutSeconds > 0) + if (timeoutSeconds >= 60) { - if (timeoutSeconds >= 60) - { - targetSeconds = timeoutSeconds - 20; // time to spare... - } - else - { - targetSeconds = (timeoutSeconds * 3) / 4; - } - Log?.LogInformationAutoConfiguredConfigTimeout(new(server), targetSeconds); - server.WriteEverySeconds = targetSeconds; + targetSeconds = timeoutSeconds - 20; // time to spare... } - } - else if (key.IsEqual(CommonReplies.databases) && val.TryGetInt64(out i64)) - { - int dbCount = checked((int)i64); - Log?.LogInformationAutoConfiguredConfigDatabases(new(server), dbCount); - server.Databases = dbCount; - if (dbCount > 1) + else { - connection.MultiDatabasesOverride = true; + targetSeconds = (timeoutSeconds * 3) / 4; } + Log?.LogInformationAutoConfiguredConfigTimeout(new(server), targetSeconds); + server.WriteEverySeconds = targetSeconds; + } + } + else if (Literals.databases.IsCS(keyHash, keySpan) && val.TryReadInt64(out i64)) + { + int dbCount = checked((int)i64); + Log?.LogInformationAutoConfiguredConfigDatabases(new(server), dbCount); + server.Databases = dbCount; + if (dbCount > 1) + { + connection.MultiDatabasesOverride = true; } - else if (key.IsEqual(CommonReplies.slave_read_only) || key.IsEqual(CommonReplies.replica_read_only)) + } + else if (Literals.slave_read_only.IsCS(keyHash, keySpan) || Literals.replica_read_only.IsCS(keyHash, keySpan)) + { + if (val.TryGetSpan(out var valSpan)) { - if (val.IsEqual(CommonReplies.yes)) + var valHash = FastHash.HashCS(valSpan); + if (Literals.yes.IsCS(valHash, valSpan)) { server.ReplicaReadOnly = true; Log?.LogInformationAutoConfiguredConfigReadOnlyReplica(new(server), true); } - else if (val.IsEqual(CommonReplies.no)) + else if (Literals.no.IsCS(valHash, valSpan)) { server.ReplicaReadOnly = false; Log?.LogInformationAutoConfiguredConfigReadOnlyReplica(new(server), false); @@ -1113,50 +1153,55 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } } - else if (message?.Command == RedisCommand.HELLO) + } + SetResult(message, true); + return true; + } + + // Handle HELLO command (returns array/map of key-value pairs) + if (message?.Command == RedisCommand.HELLO && reader.IsAggregate) + { + var iter = reader.AggregateChildren(); + while (iter.MoveNext()) + { + var key = iter.Value; + if (!iter.MoveNext()) break; + var val = iter.Value; + + if (key.TryGetSpan(out var keySpan)) { - var iter = result.GetItems().GetEnumerator(); - while (iter.MoveNext()) + var keyHash = FastHash.HashCS(keySpan); + if (Literals.version.IsCS(keyHash, keySpan) && Format.TryParseVersion(val.ReadString(), out var version)) { - ref RawResult key = ref iter.Current; - if (!iter.MoveNext()) break; - ref RawResult val = ref iter.Current; - - if (key.IsEqual(CommonReplies.version) && Format.TryParseVersion(val.GetString(), out var version)) - { - server.Version = version; - Log?.LogInformationAutoConfiguredHelloServerVersion(new(server), version); - } - else if (key.IsEqual(CommonReplies.proto) && val.TryGetInt64(out var i64)) - { - connection.SetProtocol(i64 >= 3 ? RedisProtocol.Resp3 : RedisProtocol.Resp2); - Log?.LogInformationAutoConfiguredHelloProtocol(new(server), connection.Protocol ?? RedisProtocol.Resp2); - } - else if (key.IsEqual(CommonReplies.id) && val.TryGetInt64(out i64)) - { - connection.ConnectionId = i64; - Log?.LogInformationAutoConfiguredHelloConnectionId(new(server), i64); - } - else if (key.IsEqual(CommonReplies.mode) && TryParseServerType(val.GetString(), out var serverType)) - { - server.ServerType = serverType; - Log?.LogInformationAutoConfiguredHelloServerType(new(server), serverType); - } - else if (key.IsEqual(CommonReplies.role) && TryParseRole(val.GetString(), out bool isReplica)) - { - server.IsReplica = isReplica; - Log?.LogInformationAutoConfiguredHelloRole(new(server), isReplica ? "replica" : "primary"); - } + server.Version = version; + Log?.LogInformationAutoConfiguredHelloServerVersion(new(server), version); + } + else if (Literals.proto.IsCS(keyHash, keySpan) && val.TryReadInt64(out var i64)) + { + connection.SetProtocol(i64 >= 3 ? RedisProtocol.Resp3 : RedisProtocol.Resp2); + Log?.LogInformationAutoConfiguredHelloProtocol(new(server), connection.Protocol ?? RedisProtocol.Resp2); + } + else if (Literals.id.IsCS(keyHash, keySpan) && val.TryReadInt64(out i64)) + { + connection.ConnectionId = i64; + Log?.LogInformationAutoConfiguredHelloConnectionId(new(server), i64); + } + else if (Literals.mode.IsCS(keyHash, keySpan) && TryParseServerType(val.ReadString(), out var serverType)) + { + server.ServerType = serverType; + Log?.LogInformationAutoConfiguredHelloServerType(new(server), serverType); + } + else if (Literals.role.IsCS(keyHash, keySpan) && TryParseRole(val.ReadString(), out bool isReplica)) + { + server.IsReplica = isReplica; + Log?.LogInformationAutoConfiguredHelloRole(new(server), isReplica ? "replica" : "primary"); } } - else if (message?.Command == RedisCommand.SENTINEL) - { - server.ServerType = ServerType.Sentinel; - Log?.LogInformationAutoConfiguredSentinelServerType(new(server)); - } - SetResult(message, true); - return true; + } + SetResult(message, true); + return true; } + return false; } @@ -1261,12 +1306,13 @@ internal static ClusterConfiguration Parse(PhysicalConnection connection, string return config; } - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { - switch (result.Resp2TypeBulkString) + if (reader.IsScalar) { - case ResultType.BulkString: - string nodes = result.GetString()!; + string? nodes = reader.ReadString(); + if (nodes is not null) + { var bridge = connection.BridgeCouldBeNull; var config = Parse(connection, nodes); @@ -1277,6 +1323,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } SetResult(message, config); return true; + } } return false; } @@ -1411,21 +1458,38 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes private sealed class ExpectBasicStringProcessor : ResultProcessor { - private readonly CommandBytes _expected; + private readonly FastHash _expected; private readonly bool _startsWith; - public ExpectBasicStringProcessor(CommandBytes expected, bool startsWith = false) + + public ExpectBasicStringProcessor(in FastHash expected, bool startsWith = false) { _expected = expected; _startsWith = startsWith; } - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { - if (_startsWith ? result.StartsWith(_expected) : result.IsEqual(_expected)) + if (!reader.IsScalar) return false; + + var expectedLength = _expected.Length; + // For exact match, length must be exact + if (_startsWith) + { + if (reader.ScalarLength() < expectedLength) return false; + } + else + { + if (!reader.ScalarLengthIs(expectedLength)) return false; + } + + var bytes = reader.TryGetSpan(out var tmp) ? tmp : reader.Buffer(stackalloc byte[expectedLength]); + if (_startsWith) bytes = bytes.Slice(0, expectedLength); + if (_expected.IsCS(bytes)) { SetResult(message, true); return true; } + if (message.Command == RedisCommand.AUTH) connection?.BridgeCouldBeNull?.Multiplexer?.SetAuthSuspect(new RedisException("Unknown AUTH exception")); return false; } @@ -1433,17 +1497,16 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes private sealed class InfoProcessor : ResultProcessor>[]> { - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { - if (result.Resp2TypeBulkString == ResultType.BulkString) + if (reader.IsScalar) { string category = Normalize(null); var list = new List>>(); - var raw = result.GetString(); - if (raw is not null) + if (!reader.IsNull) { - using var reader = new StringReader(raw); - while (reader.ReadLine() is string line) + using var stringReader = new StringReader(reader.ReadString()!); + while (stringReader.ReadLine() is string line) { if (string.IsNullOrWhiteSpace(line)) continue; if (line.StartsWith("# ")) @@ -1795,16 +1858,16 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes static RedisType FastParse(ReadOnlySpan span) { if (span.IsEmpty) return Redis.RedisType.None; // includes null - var hash = span.Hash64(); + var hash = FastHash.HashCS(span); return hash switch { - redistype_string.Hash when redistype_string.Is(hash, span) => Redis.RedisType.String, - redistype_list.Hash when redistype_list.Is(hash, span) => Redis.RedisType.List, - redistype_set.Hash when redistype_set.Is(hash, span) => Redis.RedisType.Set, - redistype_zset.Hash when redistype_zset.Is(hash, span) => Redis.RedisType.SortedSet, - redistype_hash.Hash when redistype_hash.Is(hash, span) => Redis.RedisType.Hash, - redistype_stream.Hash when redistype_stream.Is(hash, span) => Redis.RedisType.Stream, - redistype_vectorset.Hash when redistype_vectorset.Is(hash, span) => Redis.RedisType.VectorSet, + redistype_string.HashCS when redistype_string.IsCS(hash, span) => Redis.RedisType.String, + redistype_list.HashCS when redistype_list.IsCS(hash, span) => Redis.RedisType.List, + redistype_set.HashCS when redistype_set.IsCS(hash, span) => Redis.RedisType.Set, + redistype_zset.HashCS when redistype_zset.IsCS(hash, span) => Redis.RedisType.SortedSet, + redistype_hash.HashCS when redistype_hash.IsCS(hash, span) => Redis.RedisType.Hash, + redistype_stream.HashCS when redistype_stream.IsCS(hash, span) => Redis.RedisType.Stream, + redistype_vectorset.HashCS when redistype_vectorset.IsCS(hash, span) => Redis.RedisType.VectorSet, _ => Redis.RedisType.Unknown, }; } @@ -2045,35 +2108,86 @@ The geohash integer. /// private sealed class LongestCommonSubsequenceProcessor : ResultProcessor { - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { - switch (result.Resp2TypeArray) + if (reader.IsAggregate) { - case ResultType.Array: - SetResult(message, Parse(result)); + // Top-level array: ["matches", matches_array, "len", length_value] + // Use nominal access instead of positional + LCSMatchResult.LCSMatch[]? matchesArray = null; + long longestMatchLength = 0; + + Span keyBuffer = stackalloc byte[16]; // Buffer for key names + var iter = reader.AggregateChildren(); + while (iter.MoveNext() && iter.Value.IsScalar) + { + // Capture the scalar key + var keyBytes = iter.Value.TryGetSpan(out var tmp) ? tmp : iter.Value.Buffer(keyBuffer); + var hash = FastHash.HashCS(keyBytes); + + if (!iter.MoveNext()) break; // out of data + + // Use FastHash pattern to identify "matches" vs "len" + switch (hash) + { + case Literals.matches.HashCS when Literals.matches.IsCS(hash, keyBytes): + // Read the matches array + if (iter.Value.IsAggregate) + { + bool failed = false; + matchesArray = iter.Value.ReadPastArray(ref failed, static (ref failed, ref reader) => + { + // Don't even bother if we've already failed + if (!failed && reader.IsAggregate) + { + var matchChildren = reader.AggregateChildren(); + if (matchChildren.MoveNext() && TryReadPosition(ref matchChildren.Value, out var firstPos) + && matchChildren.MoveNext() && TryReadPosition(ref matchChildren.Value, out var secondPos) + && matchChildren.MoveNext() && matchChildren.Value.IsScalar && matchChildren.Value.TryReadInt64(out var length)) + { + return new LCSMatchResult.LCSMatch(firstPos, secondPos, length); + } + } + failed = true; + return default; + }); + + // Check if anything went wrong + if (failed) matchesArray = null; + } + break; + + case Literals.len.HashCS when Literals.len.IsCS(hash, keyBytes): + // Read the length value + if (iter.Value.IsScalar) + { + longestMatchLength = iter.Value.TryReadInt64(out var totalLen) ? totalLen : 0; + } + break; + } + } + + if (matchesArray is not null) + { + SetResult(message, new LCSMatchResult(matchesArray, longestMatchLength)); return true; + } } return false; } - private static LCSMatchResult Parse(in RawResult result) + private static bool TryReadPosition(ref RespReader reader, out LCSMatchResult.LCSPosition position) { - var topItems = result.GetItems(); - var matches = new LCSMatchResult.LCSMatch[topItems[1].GetItems().Length]; - int i = 0; - var matchesRawArray = topItems[1]; // skip the first element (title "matches") - foreach (var match in matchesRawArray.GetItems()) - { - var matchItems = match.GetItems(); + // Expecting a 2-element array: [start, end] + position = default; + if (!reader.IsAggregate) return false; - matches[i++] = new LCSMatchResult.LCSMatch( - firstStringIndex: (long)matchItems[0].GetItems()[0].AsRedisValue(), - secondStringIndex: (long)matchItems[1].GetItems()[0].AsRedisValue(), - length: (long)matchItems[2].AsRedisValue()); - } - var len = (long)topItems[3].AsRedisValue(); + if (!(reader.TryMoveNext() && reader.IsScalar && reader.TryReadInt64(out var start))) return false; - return new LCSMatchResult(matches, len); + if (!(reader.TryMoveNext() && reader.IsScalar && reader.TryReadInt64(out var end))) return false; + + position = new LCSMatchResult.LCSPosition(start, end); + return true; } } @@ -2092,18 +2206,13 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes private sealed class RedisValueFromArrayProcessor : ResultProcessor { - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { - switch (result.Resp2TypeBulkString) + if (reader.IsAggregate && reader.AggregateLengthIs(1)) { - case ResultType.Array: - var items = result.GetItems(); - if (items.Length == 1) - { // treat an array of 1 like a single reply - SetResult(message, items[0].AsRedisValue()); - return true; - } - break; + reader.MoveNext(); + SetResult(message, reader.ReadRedisValue()); + return true; } return false; } @@ -2124,13 +2233,13 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes ? span : reader.Buffer(stackalloc byte[16]); // word-aligned, enough for longest role type - var hash = FastHash.Hash64(roleBytes); + var hash = FastHash.HashCS(roleBytes); var role = hash switch { - Literals.master.Hash when Literals.master.Is(hash, roleBytes) => ParsePrimary(ref reader), - Literals.slave.Hash when Literals.slave.Is(hash, roleBytes) => ParseReplica(ref reader, Literals.slave.Text), - Literals.replica.Hash when Literals.replica.Is(hash, roleBytes) => ParseReplica(ref reader, Literals.replica.Text), - Literals.sentinel.Hash when Literals.sentinel.Is(hash, roleBytes) => ParseSentinel(ref reader), + Literals.master.HashCS when Literals.master.IsCS(hash, roleBytes) => ParsePrimary(ref reader), + Literals.slave.HashCS when Literals.slave.IsCS(hash, roleBytes) => ParseReplica(ref reader, Literals.slave.Text), + Literals.replica.HashCS when Literals.replica.IsCS(hash, roleBytes) => ParseReplica(ref reader, Literals.replica.Text), + Literals.sentinel.HashCS when Literals.sentinel.IsCS(hash, roleBytes) => ParseSentinel(ref reader), _ => new Role.Unknown(reader.ReadString()!), }; @@ -2233,15 +2342,15 @@ private static bool TryParsePrimaryReplica(ref RespReader reader, out Role.Maste ? span : reader.Buffer(stackalloc byte[16]); // word-aligned, enough for longest state - var hash = FastHash.Hash64(stateBytes); + var hash = FastHash.HashCS(stateBytes); var replicationState = hash switch { - Literals.connect.Hash when Literals.connect.Is(hash, stateBytes) => Literals.connect.Text, - Literals.connecting.Hash when Literals.connecting.Is(hash, stateBytes) => Literals.connecting.Text, - Literals.sync.Hash when Literals.sync.Is(hash, stateBytes) => Literals.sync.Text, - Literals.connected.Hash when Literals.connected.Is(hash, stateBytes) => Literals.connected.Text, - Literals.none.Hash when Literals.none.Is(hash, stateBytes) => Literals.none.Text, - Literals.handshake.Hash when Literals.handshake.Is(hash, stateBytes) => Literals.handshake.Text, + Literals.connect.HashCS when Literals.connect.IsCS(hash, stateBytes) => Literals.connect.Text, + Literals.connecting.HashCS when Literals.connecting.IsCS(hash, stateBytes) => Literals.connecting.Text, + Literals.sync.HashCS when Literals.sync.IsCS(hash, stateBytes) => Literals.sync.Text, + Literals.connected.HashCS when Literals.connected.IsCS(hash, stateBytes) => Literals.connected.Text, + Literals.none.HashCS when Literals.none.IsCS(hash, stateBytes) => Literals.none.Text, + Literals.handshake.HashCS when Literals.handshake.IsCS(hash, stateBytes) => Literals.handshake.Text, _ => reader.ReadString()!, }; @@ -2307,20 +2416,21 @@ public SingleStreamProcessor(bool skipStreamName = false) /// /// Handles . /// - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { - if (result.IsNull) + if (reader.IsNull) { // Server returns 'nil' if no entries are returned for the given stream. SetResult(message, []); return true; } - if (result.Resp2TypeArray != ResultType.Array) + if (!reader.IsAggregate) { return false; } + var protocol = connection.Protocol.GetValueOrDefault(); StreamEntry[] entries; if (skipStreamName) @@ -2364,12 +2474,27 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes 6) "46" */ - ref readonly RawResult readResult = ref (result.Resp3Type == ResultType.Map ? ref result[1] : ref result[0][1]); - entries = ParseRedisStreamEntries(readResult); + if (protocol == RedisProtocol.Resp3) + { + // RESP3: map - skip the key, read the value + reader.MoveNext(); // skip key + reader.MoveNext(); // move to value + entries = ParseRedisStreamEntries(ref reader, protocol); + } + else + { + // RESP2: array - first element is array with [name, entries] + var iter = reader.AggregateChildren(); + iter.DemandNext(); // first stream + var streamIter = iter.Value.AggregateChildren(); + streamIter.DemandNext(); // skip stream name + streamIter.DemandNext(); // entries array + entries = ParseRedisStreamEntries(ref streamIter.Value, protocol); + } } else { - entries = ParseRedisStreamEntries(result); + entries = ParseRedisStreamEntries(ref reader, protocol); } SetResult(message, entries); @@ -2377,6 +2502,12 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + private readonly struct MultiStreamState(MultiStreamProcessor processor, RedisProtocol protocol) + { + public MultiStreamProcessor Processor { get; } = processor; + public RedisProtocol Protocol { get; } = protocol; + } + /// /// Handles . /// @@ -2438,36 +2569,37 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } else { - int count = reader.AggregateLength(); - if (count == 0) - { - SetResult(message, []); - return true; - } - - streams = new RedisStream[count]; - var iter = reader.AggregateChildren(); - for (int i = 0; i < count; i++) - { - iter.DemandNext(); - var itemReader = iter.Value; - - if (!itemReader.IsAggregate) + var state = new MultiStreamState(this, protocol); + streams = reader.ReadPastArray( + ref state, + static (ref state, ref itemReader) => { - return false; - } + if (!itemReader.IsAggregate) + { + throw new InvalidOperationException("Expected aggregate for stream"); + } - var details = itemReader.AggregateChildren(); + // [0] = Name of the Stream + if (!itemReader.TryMoveNext()) + { + throw new InvalidOperationException("Expected stream name"); + } + var key = itemReader.ReadRedisKey(); - // details[0] = Name of the Stream - details.DemandNext(); - var key = details.Value.ReadRedisKey(); + // [1] = Multibulk Array of Stream Entries + if (!itemReader.TryMoveNext()) + { + throw new InvalidOperationException("Expected stream entries"); + } + var entries = state.Processor.ParseRedisStreamEntries(ref itemReader, state.Protocol); - // details[1] = Multibulk Array of Stream Entries - details.DemandNext(); - var entries = ParseRedisStreamEntries(ref details.Value, protocol); + return new RedisStream(key: key, entries: entries); + }, + scalar: false)!; // null-checked below - streams[i] = new RedisStream(key: key, entries: entries); + if (streams == null) + { + return false; } } @@ -2502,21 +2634,42 @@ protected override RedisStream Parse(in RawResult first, in RawResult second, ob /// internal sealed class StreamAutoClaimProcessor : StreamProcessorBase { - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { // See https://redis.io/commands/xautoclaim for command documentation. // Note that the result should never be null, so intentionally treating it as a failure to parse here - if (result.Resp2TypeArray == ResultType.Array && !result.IsNull) + if (reader.IsAggregate && !reader.IsNull) { - var items = result.GetItems(); + int length = reader.AggregateLength(); + if (!(length == 2 || length == 3)) + { + return false; + } + + var iter = reader.AggregateChildren(); + var protocol = connection.Protocol.GetValueOrDefault(); // [0] The next start ID. - var nextStartId = items[0].AsRedisValue(); + iter.DemandNext(); + var nextStartId = iter.Value.ReadRedisValue(); + // [1] The array of StreamEntry's. - var entries = ParseRedisStreamEntries(items[1]); + iter.DemandNext(); + var entries = ParseRedisStreamEntries(ref iter.Value, protocol); + // [2] The array of message IDs deleted from the stream that were in the PEL. // This is not available in 6.2 so we need to be defensive when reading this part of the response. - var deletedIds = (items.Length == 3 ? items[2].GetItemsAsValues() : null) ?? []; + RedisValue[] deletedIds = []; + if (length == 3) + { + iter.DemandNext(); + if (iter.Value.IsAggregate && !iter.Value.IsNull) + { + deletedIds = iter.Value.ReadPastArray( + static (ref RespReader r) => r.ReadRedisValue(), + scalar: true)!; + } + } SetResult(message, new StreamAutoClaimResult(nextStartId, entries, deletedIds)); return true; @@ -2531,21 +2684,47 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes /// internal sealed class StreamAutoClaimIdsOnlyProcessor : ResultProcessor { - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { // See https://redis.io/commands/xautoclaim for command documentation. // Note that the result should never be null, so intentionally treating it as a failure to parse here - if (result.Resp2TypeArray == ResultType.Array && !result.IsNull) + if (reader.IsAggregate && !reader.IsNull) { - var items = result.GetItems(); + int length = reader.AggregateLength(); + if (!(length == 2 || length == 3)) + { + return false; + } + + var iter = reader.AggregateChildren(); // [0] The next start ID. - var nextStartId = items[0].AsRedisValue(); + iter.DemandNext(); + var nextStartId = iter.Value.ReadRedisValue(); + // [1] The array of claimed message IDs. - var claimedIds = items[1].GetItemsAsValues() ?? []; + iter.DemandNext(); + RedisValue[] claimedIds = []; + if (iter.Value.IsAggregate && !iter.Value.IsNull) + { + claimedIds = iter.Value.ReadPastArray( + static (ref RespReader r) => r.ReadRedisValue(), + scalar: true)!; + } + // [2] The array of message IDs deleted from the stream that were in the PEL. // This is not available in 6.2 so we need to be defensive when reading this part of the response. - var deletedIds = (items.Length == 3 ? items[2].GetItemsAsValues() : null) ?? []; + RedisValue[] deletedIds = []; + if (length == 3) + { + iter.DemandNext(); + if (iter.Value.IsAggregate && !iter.Value.IsNull) + { + deletedIds = iter.Value.ReadPastArray( + static (ref RespReader r) => r.ReadRedisValue(), + scalar: true)!; + } + } SetResult(message, new StreamAutoClaimIdsOnlyResult(nextStartId, claimedIds, deletedIds)); return true; @@ -2557,7 +2736,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes internal sealed class StreamConsumerInfoProcessor : InterleavedStreamInfoProcessorBase { - protected override StreamConsumerInfo ParseItem(in RawResult result) + protected override StreamConsumerInfo ParseItem(ref RespReader reader) { // Note: the base class passes a single consumer from the response into this method. @@ -2575,14 +2754,44 @@ protected override StreamConsumerInfo ParseItem(in RawResult result) // 4) (integer)1 // 5) idle // 6) (integer)83841983 - var arr = result.GetItems(); + if (!reader.IsAggregate) + { + return default; + } + string? name = default; int pendingMessageCount = default; long idleTimeInMilliseconds = default; - KeyValuePairParser.TryRead(arr, KeyValuePairParser.Name, ref name); - KeyValuePairParser.TryRead(arr, KeyValuePairParser.Pending, ref pendingMessageCount); - KeyValuePairParser.TryRead(arr, KeyValuePairParser.Idle, ref idleTimeInMilliseconds); + Span keyBuffer = stackalloc byte[CommandBytes.MaxLength]; + while (reader.TryMoveNext() && reader.IsScalar) + { + var keyBytes = reader.TryGetSpan(out var tmp) ? tmp : reader.Buffer(keyBuffer); + if (keyBytes.Length > CommandBytes.MaxLength) + { + if (!reader.TryMoveNext()) break; + continue; + } + + var hash = FastHash.HashCS(keyBytes); + if (!reader.TryMoveNext()) break; + + switch (hash) + { + case Literals.name.HashCS when Literals.name.IsCS(hash, keyBytes): + name = reader.ReadString(); + break; + case Literals.pending.HashCS when Literals.pending.IsCS(hash, keyBytes): + if (reader.TryReadInt64(out var pending)) + { + pendingMessageCount = checked((int)pending); + } + break; + case Literals.idle.HashCS when Literals.idle.IsCS(hash, keyBytes): + reader.TryReadInt64(out idleTimeInMilliseconds); + break; + } + } return new StreamConsumerInfo(name!, pendingMessageCount, idleTimeInMilliseconds); } @@ -2656,7 +2865,7 @@ internal static bool TryRead(Sequence pairs, in CommandBytes key, [No internal sealed class StreamGroupInfoProcessor : InterleavedStreamInfoProcessorBase { - protected override StreamGroupInfo ParseItem(in RawResult result) + protected override StreamGroupInfo ParseItem(ref RespReader reader) { // Note: the base class passes a single item from the response into this method. @@ -2686,18 +2895,60 @@ protected override StreamGroupInfo ParseItem(in RawResult result) // 10) (integer)1 // 11) "lag" // 12) (integer)1 - var arr = result.GetItems(); + if (!reader.IsAggregate) + { + return default; + } + string? name = default, lastDeliveredId = default; int consumerCount = default, pendingMessageCount = default; long entriesRead = default; long? lag = default; - KeyValuePairParser.TryRead(arr, KeyValuePairParser.Name, ref name); - KeyValuePairParser.TryRead(arr, KeyValuePairParser.Consumers, ref consumerCount); - KeyValuePairParser.TryRead(arr, KeyValuePairParser.Pending, ref pendingMessageCount); - KeyValuePairParser.TryRead(arr, KeyValuePairParser.LastDeliveredId, ref lastDeliveredId); - KeyValuePairParser.TryRead(arr, KeyValuePairParser.EntriesRead, ref entriesRead); - KeyValuePairParser.TryRead(arr, KeyValuePairParser.Lag, ref lag); + Span keyBuffer = stackalloc byte[CommandBytes.MaxLength]; + while (reader.TryMoveNext() && reader.IsScalar) + { + var keyBytes = reader.TryGetSpan(out var tmp) ? tmp : reader.Buffer(keyBuffer); + if (keyBytes.Length > CommandBytes.MaxLength) + { + if (!reader.TryMoveNext()) break; + continue; + } + + var hash = FastHash.HashCS(keyBytes); + if (!reader.TryMoveNext()) break; + + switch (hash) + { + case Literals.name.HashCS when Literals.name.IsCS(hash, keyBytes): + name = reader.ReadString(); + break; + case Literals.consumers.HashCS when Literals.consumers.IsCS(hash, keyBytes): + if (reader.TryReadInt64(out var consumers)) + { + consumerCount = checked((int)consumers); + } + break; + case Literals.pending.HashCS when Literals.pending.IsCS(hash, keyBytes): + if (reader.TryReadInt64(out var pending)) + { + pendingMessageCount = checked((int)pending); + } + break; + case Literals.last_delivered_id.HashCS when Literals.last_delivered_id.IsCS(hash, keyBytes): + lastDeliveredId = reader.ReadString(); + break; + case Literals.entries_read.HashCS when Literals.entries_read.IsCS(hash, keyBytes): + reader.TryReadInt64(out entriesRead); + break; + case Literals.lag.HashCS when Literals.lag.IsCS(hash, keyBytes): + if (reader.TryReadInt64(out var lagValue)) + { + lag = lagValue; + } + break; + } + } return new StreamGroupInfo(name!, consumerCount, pendingMessageCount, lastDeliveredId, entriesRead, lag); } @@ -2705,19 +2956,22 @@ protected override StreamGroupInfo ParseItem(in RawResult result) internal abstract class InterleavedStreamInfoProcessorBase : ResultProcessor { - protected abstract T ParseItem(in RawResult result); + protected abstract T ParseItem(ref RespReader reader); - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { - if (result.Resp2TypeArray != ResultType.Array) + if (!reader.IsAggregate) { return false; } - var arr = result.GetItems(); - var parsedItems = arr.ToArray((in item, in obj) => obj.ParseItem(item), this); + var self = this; + var parsedItems = reader.ReadPastArray( + ref self, + static (ref self, ref r) => self.ParseItem(ref r), + scalar: false); - SetResult(message, parsedItems); + SetResult(message, parsedItems!); return true; } } @@ -2744,15 +2998,15 @@ internal sealed class StreamInfoProcessor : StreamProcessorBase // 12) 1) 1526569544280-0 // 2) 1) "message" // 2) "banana" - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { - if (result.Resp2TypeArray != ResultType.Array) + if (!reader.IsAggregate) { return false; } - var arr = result.GetItems(); - var max = arr.Length / 2; + int count = reader.AggregateLength(); + if ((count & 1) != 0) return false; // must be even (key-value pairs) long length = -1, radixTreeKeys = -1, radixTreeNodes = -1, groups = -1, entriesAdded = -1, idmpDuration = -1, idmpMaxsize = -1, @@ -2761,63 +3015,76 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes maxDeletedEntryId = Redis.RedisValue.Null, recordedFirstEntryId = Redis.RedisValue.Null; StreamEntry firstEntry = StreamEntry.Null, lastEntry = StreamEntry.Null; - var iter = arr.GetEnumerator(); - for (int i = 0; i < max; i++) + + var protocol = connection.Protocol.GetValueOrDefault(); + Span keyBuffer = stackalloc byte[CommandBytes.MaxLength]; + + while (reader.TryMoveNext() && reader.IsScalar) { - ref RawResult key = ref iter.GetNext(), value = ref iter.GetNext(); - if (key.Payload.Length > CommandBytes.MaxLength) continue; - var hash = key.Payload.Hash64(); + var keyBytes = reader.TryGetSpan(out var tmp) ? tmp : reader.Buffer(keyBuffer); + if (keyBytes.Length > CommandBytes.MaxLength) + { + // Skip this key-value pair + if (!reader.TryMoveNext()) break; + continue; + } + + var hash = FastHash.HashCS(keyBytes); + + // Move to value + if (!reader.TryMoveNext()) break; + switch (hash) { - case Literals.length.Hash when Literals.length.Is(hash, key): - if (!value.TryGetInt64(out length)) return false; + case Literals.length.HashCS when Literals.length.IsCS(hash, keyBytes): + if (!reader.TryReadInt64(out length)) return false; break; - case Literals.radix_tree_keys.Hash when Literals.radix_tree_keys.Is(hash, key): - if (!value.TryGetInt64(out radixTreeKeys)) return false; + case Literals.radix_tree_keys.HashCS when Literals.radix_tree_keys.IsCS(hash, keyBytes): + if (!reader.TryReadInt64(out radixTreeKeys)) return false; break; - case Literals.radix_tree_nodes.Hash when Literals.radix_tree_nodes.Is(hash, key): - if (!value.TryGetInt64(out radixTreeNodes)) return false; + case Literals.radix_tree_nodes.HashCS when Literals.radix_tree_nodes.IsCS(hash, keyBytes): + if (!reader.TryReadInt64(out radixTreeNodes)) return false; break; - case Literals.groups.Hash when Literals.groups.Is(hash, key): - if (!value.TryGetInt64(out groups)) return false; + case Literals.groups.HashCS when Literals.groups.IsCS(hash, keyBytes): + if (!reader.TryReadInt64(out groups)) return false; break; - case Literals.last_generated_id.Hash when Literals.last_generated_id.Is(hash, key): - lastGeneratedId = value.AsRedisValue(); + case Literals.last_generated_id.HashCS when Literals.last_generated_id.IsCS(hash, keyBytes): + lastGeneratedId = reader.ReadRedisValue(); break; - case Literals.first_entry.Hash when Literals.first_entry.Is(hash, key): - firstEntry = ParseRedisStreamEntry(value); + case Literals.first_entry.HashCS when Literals.first_entry.IsCS(hash, keyBytes): + firstEntry = ParseRedisStreamEntry(ref reader, protocol); break; - case Literals.last_entry.Hash when Literals.last_entry.Is(hash, key): - lastEntry = ParseRedisStreamEntry(value); + case Literals.last_entry.HashCS when Literals.last_entry.IsCS(hash, keyBytes): + lastEntry = ParseRedisStreamEntry(ref reader, protocol); break; // 7.0 - case Literals.max_deleted_entry_id.Hash when Literals.max_deleted_entry_id.Is(hash, key): - maxDeletedEntryId = value.AsRedisValue(); + case Literals.max_deleted_entry_id.HashCS when Literals.max_deleted_entry_id.IsCS(hash, keyBytes): + maxDeletedEntryId = reader.ReadRedisValue(); break; - case Literals.recorded_first_entry_id.Hash when Literals.recorded_first_entry_id.Is(hash, key): - recordedFirstEntryId = value.AsRedisValue(); + case Literals.recorded_first_entry_id.HashCS when Literals.recorded_first_entry_id.IsCS(hash, keyBytes): + recordedFirstEntryId = reader.ReadRedisValue(); break; - case Literals.entries_added.Hash when Literals.entries_added.Is(hash, key): - if (!value.TryGetInt64(out entriesAdded)) return false; + case Literals.entries_added.HashCS when Literals.entries_added.IsCS(hash, keyBytes): + if (!reader.TryReadInt64(out entriesAdded)) return false; break; // 8.6 - case Literals.idmp_duration.Hash when Literals.idmp_duration.Is(hash, key): - if (!value.TryGetInt64(out idmpDuration)) return false; + case Literals.idmp_duration.HashCS when Literals.idmp_duration.IsCS(hash, keyBytes): + if (!reader.TryReadInt64(out idmpDuration)) return false; break; - case Literals.idmp_maxsize.Hash when Literals.idmp_maxsize.Is(hash, key): - if (!value.TryGetInt64(out idmpMaxsize)) return false; + case Literals.idmp_maxsize.HashCS when Literals.idmp_maxsize.IsCS(hash, keyBytes): + if (!reader.TryReadInt64(out idmpMaxsize)) return false; break; - case Literals.pids_tracked.Hash when Literals.pids_tracked.Is(hash, key): - if (!value.TryGetInt64(out pidsTracked)) return false; + case Literals.pids_tracked.HashCS when Literals.pids_tracked.IsCS(hash, keyBytes): + if (!reader.TryReadInt64(out pidsTracked)) return false; break; - case Literals.iids_tracked.Hash when Literals.iids_tracked.Is(hash, key): - if (!value.TryGetInt64(out iidsTracked)) return false; + case Literals.iids_tracked.HashCS when Literals.iids_tracked.IsCS(hash, keyBytes): + if (!reader.TryReadInt64(out iidsTracked)) return false; break; - case Literals.iids_added.Hash when Literals.iids_added.Is(hash, key): - if (!value.TryGetInt64(out iidsAdded)) return false; + case Literals.iids_added.HashCS when Literals.iids_added.IsCS(hash, keyBytes): + if (!reader.TryReadInt64(out iidsAdded)) return false; break; - case Literals.iids_duplicates.Hash when Literals.iids_duplicates.Is(hash, key): - if (!value.TryGetInt64(out iidsDuplicates)) return false; + case Literals.iids_duplicates.HashCS when Literals.iids_duplicates.IsCS(hash, keyBytes): + if (!reader.TryReadInt64(out iidsDuplicates)) return false; break; } } @@ -2847,7 +3114,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes internal sealed class StreamPendingInfoProcessor : ResultProcessor { - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { // Example: // > XPENDING mystream mygroup @@ -2858,38 +3125,66 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes // 2) "2" // 5) 1) 1) "Joe" // 2) "8" - if (result.Resp2TypeArray != ResultType.Array) + if (!(reader.IsAggregate && reader.AggregateLengthIs(4))) { return false; } - var arr = result.GetItems(); + var iter = reader.AggregateChildren(); - if (arr.Length != 4) + // Element 0: pending message count + iter.DemandNext(); + if (!iter.Value.TryReadInt64(out var pendingMessageCount)) { return false; } + // Element 1: lowest ID + iter.DemandNext(); + var lowestId = iter.Value.ReadRedisValue(); + + // Element 2: highest ID + iter.DemandNext(); + var highestId = iter.Value.ReadRedisValue(); + + // Element 3: consumers array (may be null) + iter.DemandNext(); StreamConsumer[]? consumers = null; // If there are no consumers as of yet for the given group, the last // item in the response array will be null. - ref RawResult third = ref arr[3]; - if (!third.IsNull) + if (iter.Value.IsAggregate && !iter.Value.IsNull) { - consumers = third.ToArray((in item) => - { - var details = item.GetItems(); - return new StreamConsumer( - name: details[0].AsRedisValue(), - pendingMessageCount: (int)details[1].AsRedisValue()); - }); + consumers = iter.Value.ReadPastArray( + static (ref RespReader consumerReader) => + { + if (!(consumerReader.IsAggregate && consumerReader.AggregateLengthIs(2))) + { + throw new InvalidOperationException("Expected array of 2 elements for consumer"); + } + + var consumerIter = consumerReader.AggregateChildren(); + + consumerIter.DemandNext(); + var name = consumerIter.Value.ReadRedisValue(); + + consumerIter.DemandNext(); + if (!consumerIter.Value.TryReadInt64(out var count)) + { + throw new InvalidOperationException("Expected integer for pending message count"); + } + + return new StreamConsumer( + name: name, + pendingMessageCount: checked((int)count)); + }, + scalar: false); } var pendingInfo = new StreamPendingInfo( - pendingMessageCount: (int)arr[0].AsRedisValue(), - lowestId: arr[1].AsRedisValue(), - highestId: arr[2].AsRedisValue(), + pendingMessageCount: checked((int)pendingMessageCount), + lowestId: lowestId, + highestId: highestId, consumers: consumers ?? []); SetResult(message, pendingInfo); @@ -2899,25 +3194,52 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes internal sealed class StreamPendingMessagesProcessor : ResultProcessor { - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { - if (result.Resp2TypeArray != ResultType.Array) + if (!reader.IsAggregate) { return false; } - var messageInfoArray = result.GetItems().ToArray((in item) => - { - var details = item.GetItems().GetEnumerator(); + var messageInfoArray = reader.ReadPastArray( + static (ref RespReader itemReader) => + { + if (!(itemReader.IsAggregate && itemReader.AggregateLengthIs(4))) + { + throw new InvalidOperationException("Expected array of 4 elements for pending message"); + } + + if (!itemReader.TryMoveNext()) + { + throw new InvalidOperationException("Expected message ID"); + } + var messageId = itemReader.ReadRedisValue(); + + if (!itemReader.TryMoveNext()) + { + throw new InvalidOperationException("Expected consumer name"); + } + var consumerName = itemReader.ReadRedisValue(); + + if (!itemReader.TryMoveNext() || !itemReader.TryReadInt64(out var idleTimeInMs)) + { + throw new InvalidOperationException("Expected integer for idle time"); + } - return new StreamPendingMessageInfo( - messageId: details.GetNext().AsRedisValue(), - consumerName: details.GetNext().AsRedisValue(), - idleTimeInMs: (long)details.GetNext().AsRedisValue(), - deliveryCount: (int)details.GetNext().AsRedisValue()); - }); + if (!itemReader.TryMoveNext() || !itemReader.TryReadInt64(out var deliveryCount)) + { + throw new InvalidOperationException("Expected integer for delivery count"); + } + + return new StreamPendingMessageInfo( + messageId: messageId, + consumerName: consumerName, + idleTimeInMs: idleTimeInMs, + deliveryCount: checked((int)deliveryCount)); + }, + scalar: false); - SetResult(message, messageInfoArray); + SetResult(message, messageInfoArray!); return true; } } @@ -3025,21 +3347,10 @@ protected internal StreamEntry[] ParseRedisStreamEntries(ref RespReader reader, return []; } - int count = reader.AggregateLength(); - if (count == 0) - { - return []; - } - - var entries = new StreamEntry[count]; - var iter = reader.AggregateChildren(); - for (int i = 0; i < count; i++) - { - iter.DemandNext(); - entries[i] = ParseRedisStreamEntry(ref iter.Value, protocol); - } - - return entries; + return reader.ReadPastArray( + ref protocol, + static (ref protocol, ref r) => ParseRedisStreamEntry(ref r, protocol), + scalar: false) ?? []; } protected static NameValueEntry[] ParseStreamEntryValues(in RawResult result) @@ -3223,100 +3534,189 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + /// + /// Filters out null values from an endpoint array efficiently. + /// + /// The array to filter, or null. + /// + /// - null if input is null. + /// - original array if no nulls found. + /// - empty array if all nulls. + /// - new array with nulls removed otherwise. + /// + private static EndPoint[]? FilterNullEndpoints(EndPoint?[]? endpoints) + { + if (endpoints is null) return null; + + // Count nulls in a single pass + int nullCount = 0; + for (int i = 0; i < endpoints.Length; i++) + { + if (endpoints[i] is null) nullCount++; + } + + // No nulls - return original array + if (nullCount == 0) return endpoints!; + + // All nulls - return empty array + if (nullCount == endpoints.Length) return []; + + // Some nulls - allocate new array and copy non-nulls + var result = new EndPoint[endpoints.Length - nullCount]; + int writeIndex = 0; + for (int i = 0; i < endpoints.Length; i++) + { + if (endpoints[i] is not null) + { + result[writeIndex++] = endpoints[i]!; + } + } + + return result; + } + private sealed class SentinelGetPrimaryAddressByNameProcessor : ResultProcessor { - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { - switch (result.Resp2TypeArray) + if (reader.IsAggregate && reader.AggregateLengthIs(2)) { - case ResultType.Array: - var items = result.GetItems(); - if (result.IsNull) - { - return true; - } - else if (items.Length == 2 && items[1].TryGetInt64(out var port)) - { - SetResult(message, Format.ParseEndPoint(items[0].GetString()!, checked((int)port))); - return true; - } - else if (items.Length == 0) - { - SetResult(message, null); - return true; - } - break; + reader.MoveNext(); + var host = reader.ReadString(); + reader.MoveNext(); + if (host is not null && reader.TryReadInt64(out var port)) + { + SetResult(message, Format.ParseEndPoint(host, checked((int)port))); + return true; + } + } + else if (reader.IsNull || (reader.IsAggregate && reader.AggregateLengthIs(0))) + { + SetResult(message, null); + return true; } return false; } } - private sealed class SentinelGetSentinelAddressesProcessor : ResultProcessor + private sealed partial class SentinelGetSentinelAddressesProcessor : ResultProcessor { - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { - List endPoints = []; - - switch (result.Resp2TypeArray) + if (reader.IsAggregate && !reader.IsNull) { - case ResultType.Array: - foreach (RawResult item in result.GetItems()) + var endpoints = reader.ReadPastArray( + static (ref RespReader itemReader) => { - var pairs = item.GetItems(); - string? ip = null; - int port = default; - if (KeyValuePairParser.TryRead(pairs, in KeyValuePairParser.IP, ref ip) - && KeyValuePairParser.TryRead(pairs, in KeyValuePairParser.Port, ref port)) + if (itemReader.IsAggregate) { - endPoints.Add(Format.ParseEndPoint(ip, port)); + // Parse key-value pairs by name: ["ip", "127.0.0.1", "port", "26379"] + // or ["port", "26379", "ip", "127.0.0.1"] - order doesn't matter + string? host = null; + long portValue = 0; + + Span keyBuffer = stackalloc byte[16]; // Buffer for key names + while (itemReader.TryMoveNext() && itemReader.IsScalar) + { + // Capture the scalar key + var keyBytes = itemReader.TryGetSpan(out var tmp) ? tmp : itemReader.Buffer(keyBuffer); + var hash = FastHash.HashCS(keyBytes); + + // Check for second scalar value + if (!(itemReader.TryMoveNext() && itemReader.IsScalar)) break; + + // Use FastHash pattern to identify "ip" vs "port" + switch (hash) + { + case Literals.ip.HashCS when Literals.ip.IsCS(hash, keyBytes): + host = itemReader.ReadString(); + break; + + case Literals.port.HashCS when Literals.port.IsCS(hash, keyBytes): + itemReader.TryReadInt64(out portValue); + break; + } + } + + if (host is not null && portValue > 0) + { + return Format.ParseEndPoint(host, checked((int)portValue)); + } } - } - SetResult(message, endPoints.ToArray()); - return true; + return null; + }, + scalar: false); - case ResultType.SimpleString: - // We don't want to blow up if the primary is not found - if (result.IsNull) - return true; - break; + var filtered = FilterNullEndpoints(endpoints); + if (filtered is not null) + { + SetResult(message, filtered); + return true; + } } return false; } } - private sealed class SentinelGetReplicaAddressesProcessor : ResultProcessor + private sealed partial class SentinelGetReplicaAddressesProcessor : ResultProcessor { - protected override bool SetResultCore(PhysicalConnection connection, Message message, RawResult result) + protected override bool SetResultCore(PhysicalConnection connection, Message message, ref RespReader reader) { - List endPoints = []; - - switch (result.Resp2TypeArray) + if (reader.IsAggregate) { - case ResultType.Array: - foreach (RawResult item in result.GetItems()) + var endPoints = reader.ReadPastArray( + static (ref RespReader r) => { - var pairs = item.GetItems(); - string? ip = null; - int port = default; - if (KeyValuePairParser.TryRead(pairs, in KeyValuePairParser.IP, ref ip) - && KeyValuePairParser.TryRead(pairs, in KeyValuePairParser.Port, ref port)) + if (r.IsAggregate) { - endPoints.Add(Format.ParseEndPoint(ip, port)); + // Parse key-value pairs by name: ["ip", "127.0.0.1", "port", "6380"] + // or ["port", "6380", "ip", "127.0.0.1"] - order doesn't matter + string? host = null; + long portValue = 0; + + Span keyBuffer = stackalloc byte[16]; // Buffer for key names + while (r.TryMoveNext() && r.IsScalar) + { + // Capture the scalar key + var keyBytes = r.TryGetSpan(out var tmp) ? tmp : r.Buffer(keyBuffer); + var hash = FastHash.HashCS(keyBytes); + + // Check for second scalar value + if (!(r.TryMoveNext() && r.IsScalar)) break; + + // Use FastHash pattern to identify "ip" vs "port" + switch (hash) + { + case Literals.ip.HashCS when Literals.ip.IsCS(hash, keyBytes): + host = r.ReadString(); + break; + + case Literals.port.HashCS when Literals.port.IsCS(hash, keyBytes): + r.TryReadInt64(out portValue); + break; + } + } + + if (host is not null && portValue > 0) + { + return Format.ParseEndPoint(host, checked((int)portValue)); + } } - } - break; + return null; + }, + scalar: false); - case ResultType.SimpleString: - // We don't want to blow up if the primary is not found - if (result.IsNull) - return true; - break; + var filtered = FilterNullEndpoints(endPoints); + if (filtered is not null && filtered.Length > 0) + { + SetResult(message, filtered); + return true; + } } - - if (endPoints.Count > 0) + else if (reader.IsScalar && reader.IsNull) { - SetResult(message, endPoints.ToArray()); + // We don't want to blow up if the primary is not found return true; } @@ -3335,23 +3735,21 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (reader.IsAggregate && !reader.IsNull) { - int outerCount = reader.AggregateLength(); - var returnArray = new KeyValuePair[outerCount][]; - var iter = reader.AggregateChildren(); var protocol = connection.Protocol.GetValueOrDefault(); - - for (int i = 0; i < outerCount; i++) - { - iter.DemandNext(); - var innerReader = iter.Value; - if (!innerReader.IsAggregate) + var state = (innerProcessor, protocol, message); + var returnArray = reader.ReadPastArray( + ref state, + static (ref state, ref innerReader) => { - throw new ArgumentOutOfRangeException(nameof(innerReader), $"Error processing {message.CommandAndKey}, expected array but got scalar"); - } - returnArray[i] = innerProcessor.ParseArray(ref innerReader, protocol, false, out _, null)!; - } + if (!innerReader.IsAggregate) + { + throw new ArgumentOutOfRangeException(nameof(innerReader), $"Error processing {state.message.CommandAndKey}, expected array but got scalar"); + } + return state.innerProcessor.ParseArray(ref innerReader, state.protocol, false, out _, null)!; + }, + scalar: false); - SetResult(message, returnArray); + SetResult(message, returnArray!); return true; } return false; diff --git a/tests/StackExchange.Redis.Benchmarks/FastHashBenchmarks.cs b/tests/StackExchange.Redis.Benchmarks/FastHashBenchmarks.cs index 78877f163..d362e7437 100644 --- a/tests/StackExchange.Redis.Benchmarks/FastHashBenchmarks.cs +++ b/tests/StackExchange.Redis.Benchmarks/FastHashBenchmarks.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Text; using BenchmarkDotNet.Attributes; +using RESPite; namespace StackExchange.Redis.Benchmarks; @@ -51,11 +52,11 @@ public void Setup() var bytes = _sourceBytes.Span; var expected = FastHash.Hash64Fallback(bytes); - Assert(bytes.Hash64(), nameof(FastHash.Hash64)); + Assert(FastHash.HashCS(bytes), nameof(FastHash.HashCS)); Assert(FastHash.Hash64Unsafe(bytes), nameof(FastHash.Hash64Unsafe)); #pragma warning restore CS0618 // Type or member is obsolete - Assert(SingleSegmentBytes.Hash64(), nameof(FastHash.Hash64) + " (single segment)"); - Assert(_sourceMultiSegmentBytes.Hash64(), nameof(FastHash.Hash64) + " (multi segment)"); + Assert(FastHash.HashCS(SingleSegmentBytes), nameof(FastHash.HashCS) + " (single segment)"); + Assert(FastHash.HashCS(_sourceMultiSegmentBytes), nameof(FastHash.HashCS) + " (multi segment)"); void Assert(long actual, string name) { @@ -89,7 +90,7 @@ public void Hash64() var val = _sourceBytes.Span; for (int i = 0; i < OperationsPerInvoke; i++) { - _ = val.Hash64(); + _ = FastHash.HashCS(val); } } @@ -123,7 +124,7 @@ public void Hash64_SingleSegment() var val = SingleSegmentBytes; for (int i = 0; i < OperationsPerInvoke; i++) { - _ = val.Hash64(); + _ = FastHash.HashCS(val); } } @@ -133,7 +134,7 @@ public void Hash64_MultiSegment() var val = _sourceMultiSegmentBytes; for (int i = 0; i < OperationsPerInvoke; i++) { - _ = val.Hash64(); + _ = FastHash.HashCS(val); } } } diff --git a/tests/StackExchange.Redis.Benchmarks/FastHashSwitch.cs b/tests/StackExchange.Redis.Benchmarks/FastHashSwitch.cs new file mode 100644 index 000000000..bd1281354 --- /dev/null +++ b/tests/StackExchange.Redis.Benchmarks/FastHashSwitch.cs @@ -0,0 +1,517 @@ +using System; +using System.Text; +using BenchmarkDotNet.Attributes; +using RESPite; +// ReSharper disable InconsistentNaming +// ReSharper disable ArrangeTypeMemberModifiers +// ReSharper disable MemberCanBePrivate.Local +#pragma warning disable SA1300, SA1134, CS8981, SA1400 +namespace StackExchange.Redis.Benchmarks; + +[ShortRunJob, MemoryDiagnoser] +public class FastHashSwitch +{ + // conclusion: it doesn't matter; switch on the hash or length is fine, just: remember to do the Is check + // CS vs CI: CI misses are cheap, because of the hash fail; CI hits of values <= 8 characters are cheap if + // it turns out to be a CS match, because of the CS hash check which can cheaply test CS equality; CI inequality + // and CI equality over 8 characters has a bit more overhead, but still fine + public enum Field + { + key, + abc, + port, + test, + tracking_active, + sample_ratio, + selected_slots, + all_commands_all_slots_us, + all_commands_selected_slots_us, + sampled_command_selected_slots_us, + sampled_commands_selected_slots_us, + net_bytes_all_commands_all_slots, + net_bytes_all_commands_selected_slots, + net_bytes_sampled_commands_selected_slots, + collection_start_time_unix_ms, + collection_duration_ms, + collection_duration_us, + total_cpu_time_user_ms, + total_cpu_time_user_us, + total_cpu_time_sys_ms, + total_cpu_time_sys_us, + total_net_bytes, + by_cpu_time_us, + by_net_bytes, + + Unknown = -1, + } + + private byte[] _bytes = []; + [GlobalSetup] + public void Init() => _bytes = Encoding.UTF8.GetBytes(Value); + + public static string[] GetValues() => + [ + key.Text, + abc.Text, + port.Text, + test.Text, + tracking_active.Text, + sample_ratio.Text, + selected_slots.Text, + all_commands_all_slots_us.Text, + net_bytes_sampled_commands_selected_slots.Text, + total_cpu_time_sys_us.Text, + total_net_bytes.Text, + by_cpu_time_us.Text, + by_net_bytes.Text, + "miss", + "PORT", + "much longer miss", + ]; + + [ParamsSource(nameof(GetValues))] + public string Value { get; set; } = ""; + + [Benchmark] + public Field SwitchOnHash() + { + ReadOnlySpan span = _bytes; + var hash = FastHash.HashCS(span); + return hash switch + { + key.HashCS when key.IsCS(hash, span) => Field.key, + abc.HashCS when abc.IsCS(hash, span) => Field.abc, + port.HashCS when port.IsCS(hash, span) => Field.port, + test.HashCS when test.IsCS(hash, span) => Field.test, + tracking_active.HashCS when tracking_active.IsCS(hash, span) => Field.tracking_active, + sample_ratio.HashCS when sample_ratio.IsCS(hash, span) => Field.sample_ratio, + selected_slots.HashCS when selected_slots.IsCS(hash, span) => Field.selected_slots, + all_commands_all_slots_us.HashCS when all_commands_all_slots_us.IsCS(hash, span) => Field.all_commands_all_slots_us, + all_commands_selected_slots_us.HashCS when all_commands_selected_slots_us.IsCS(hash, span) => Field.all_commands_selected_slots_us, + sampled_command_selected_slots_us.HashCS when sampled_command_selected_slots_us.IsCS(hash, span) => Field.sampled_command_selected_slots_us, + sampled_commands_selected_slots_us.HashCS when sampled_commands_selected_slots_us.IsCS(hash, span) => Field.sampled_commands_selected_slots_us, + net_bytes_all_commands_all_slots.HashCS when net_bytes_all_commands_all_slots.IsCS(hash, span) => Field.net_bytes_all_commands_all_slots, + net_bytes_all_commands_selected_slots.HashCS when net_bytes_all_commands_selected_slots.IsCS(hash, span) => Field.net_bytes_all_commands_selected_slots, + net_bytes_sampled_commands_selected_slots.HashCS when net_bytes_sampled_commands_selected_slots.IsCS(hash, span) => Field.net_bytes_sampled_commands_selected_slots, + collection_start_time_unix_ms.HashCS when collection_start_time_unix_ms.IsCS(hash, span) => Field.collection_start_time_unix_ms, + collection_duration_ms.HashCS when collection_duration_ms.IsCS(hash, span) => Field.collection_duration_ms, + collection_duration_us.HashCS when collection_duration_us.IsCS(hash, span) => Field.collection_duration_us, + total_cpu_time_user_ms.HashCS when total_cpu_time_user_ms.IsCS(hash, span) => Field.total_cpu_time_user_ms, + total_cpu_time_user_us.HashCS when total_cpu_time_user_us.IsCS(hash, span) => Field.total_cpu_time_user_us, + total_cpu_time_sys_ms.HashCS when total_cpu_time_sys_ms.IsCS(hash, span) => Field.total_cpu_time_sys_ms, + total_cpu_time_sys_us.HashCS when total_cpu_time_sys_us.IsCS(hash, span) => Field.total_cpu_time_sys_us, + total_net_bytes.HashCS when total_net_bytes.IsCS(hash, span) => Field.total_net_bytes, + by_cpu_time_us.HashCS when by_cpu_time_us.IsCS(hash, span) => Field.by_cpu_time_us, + by_net_bytes.HashCS when by_net_bytes.IsCS(hash, span) => Field.by_net_bytes, + _ => Field.Unknown, + }; + } + + [Benchmark] + public Field SequenceEqual() + { + ReadOnlySpan span = _bytes; + if (span.SequenceEqual(key.U8)) return Field.key; + if (span.SequenceEqual(abc.U8)) return Field.abc; + if (span.SequenceEqual(port.U8)) return Field.port; + if (span.SequenceEqual(test.U8)) return Field.test; + if (span.SequenceEqual(tracking_active.U8)) return Field.tracking_active; + if (span.SequenceEqual(sample_ratio.U8)) return Field.sample_ratio; + if (span.SequenceEqual(selected_slots.U8)) return Field.selected_slots; + if (span.SequenceEqual(all_commands_all_slots_us.U8)) return Field.all_commands_all_slots_us; + if (span.SequenceEqual(all_commands_selected_slots_us.U8)) return Field.all_commands_selected_slots_us; + if (span.SequenceEqual(sampled_command_selected_slots_us.U8)) return Field.sampled_command_selected_slots_us; + if (span.SequenceEqual(sampled_commands_selected_slots_us.U8)) return Field.sampled_commands_selected_slots_us; + if (span.SequenceEqual(net_bytes_all_commands_all_slots.U8)) return Field.net_bytes_all_commands_all_slots; + if (span.SequenceEqual(net_bytes_all_commands_selected_slots.U8)) return Field.net_bytes_all_commands_selected_slots; + if (span.SequenceEqual(net_bytes_sampled_commands_selected_slots.U8)) return Field.net_bytes_sampled_commands_selected_slots; + if (span.SequenceEqual(collection_start_time_unix_ms.U8)) return Field.collection_start_time_unix_ms; + if (span.SequenceEqual(collection_duration_ms.U8)) return Field.collection_duration_ms; + if (span.SequenceEqual(collection_duration_us.U8)) return Field.collection_duration_us; + if (span.SequenceEqual(total_cpu_time_user_ms.U8)) return Field.total_cpu_time_user_ms; + if (span.SequenceEqual(total_cpu_time_user_us.U8)) return Field.total_cpu_time_user_us; + if (span.SequenceEqual(total_cpu_time_sys_ms.U8)) return Field.total_cpu_time_sys_ms; + if (span.SequenceEqual(total_cpu_time_sys_us.U8)) return Field.total_cpu_time_sys_us; + if (span.SequenceEqual(total_net_bytes.U8)) return Field.total_net_bytes; + if (span.SequenceEqual(by_cpu_time_us.U8)) return Field.by_cpu_time_us; + if (span.SequenceEqual(by_net_bytes.U8)) return Field.by_net_bytes; + + return Field.Unknown; + } + + [Benchmark] + public Field SwitchOnLength() + { + ReadOnlySpan span = _bytes; + var hash = FastHash.HashCS(span); + return span.Length switch + { + key.Length when key.IsCS(hash, span) => Field.key, + abc.Length when abc.IsCS(hash, span) => Field.abc, + port.Length when port.IsCS(hash, span) => Field.port, + test.Length when test.IsCS(hash, span) => Field.test, + tracking_active.Length when tracking_active.IsCS(hash, span) => Field.tracking_active, + sample_ratio.Length when sample_ratio.IsCS(hash, span) => Field.sample_ratio, + selected_slots.Length when selected_slots.IsCS(hash, span) => Field.selected_slots, + all_commands_all_slots_us.Length when all_commands_all_slots_us.IsCS(hash, span) => Field.all_commands_all_slots_us, + all_commands_selected_slots_us.Length when all_commands_selected_slots_us.IsCS(hash, span) => Field.all_commands_selected_slots_us, + sampled_command_selected_slots_us.Length when sampled_command_selected_slots_us.IsCS(hash, span) => Field.sampled_command_selected_slots_us, + sampled_commands_selected_slots_us.Length when sampled_commands_selected_slots_us.IsCS(hash, span) => Field.sampled_commands_selected_slots_us, + net_bytes_all_commands_all_slots.Length when net_bytes_all_commands_all_slots.IsCS(hash, span) => Field.net_bytes_all_commands_all_slots, + net_bytes_all_commands_selected_slots.Length when net_bytes_all_commands_selected_slots.IsCS(hash, span) => Field.net_bytes_all_commands_selected_slots, + net_bytes_sampled_commands_selected_slots.Length when net_bytes_sampled_commands_selected_slots.IsCS(hash, span) => Field.net_bytes_sampled_commands_selected_slots, + collection_start_time_unix_ms.Length when collection_start_time_unix_ms.IsCS(hash, span) => Field.collection_start_time_unix_ms, + collection_duration_ms.Length when collection_duration_ms.IsCS(hash, span) => Field.collection_duration_ms, + collection_duration_us.Length when collection_duration_us.IsCS(hash, span) => Field.collection_duration_us, + total_cpu_time_user_ms.Length when total_cpu_time_user_ms.IsCS(hash, span) => Field.total_cpu_time_user_ms, + total_cpu_time_user_us.Length when total_cpu_time_user_us.IsCS(hash, span) => Field.total_cpu_time_user_us, + total_cpu_time_sys_ms.Length when total_cpu_time_sys_ms.IsCS(hash, span) => Field.total_cpu_time_sys_ms, + total_cpu_time_sys_us.Length when total_cpu_time_sys_us.IsCS(hash, span) => Field.total_cpu_time_sys_us, + total_net_bytes.Length when total_net_bytes.IsCS(hash, span) => Field.total_net_bytes, + by_cpu_time_us.Length when by_cpu_time_us.IsCS(hash, span) => Field.by_cpu_time_us, + by_net_bytes.Length when by_net_bytes.IsCS(hash, span) => Field.by_net_bytes, + _ => Field.Unknown, + }; + } + + [Benchmark] + public Field SwitchOnHash_CI() + { + ReadOnlySpan span = _bytes; + var hash = FastHash.HashCI(span); + return hash switch + { + key.HashCI when key.IsCI(hash, span) => Field.key, + abc.HashCI when abc.IsCI(hash, span) => Field.abc, + port.HashCI when port.IsCI(hash, span) => Field.port, + test.HashCI when test.IsCI(hash, span) => Field.test, + tracking_active.HashCI when tracking_active.IsCI(hash, span) => Field.tracking_active, + sample_ratio.HashCI when sample_ratio.IsCI(hash, span) => Field.sample_ratio, + selected_slots.HashCI when selected_slots.IsCI(hash, span) => Field.selected_slots, + all_commands_all_slots_us.HashCI when all_commands_all_slots_us.IsCI(hash, span) => Field.all_commands_all_slots_us, + all_commands_selected_slots_us.HashCI when all_commands_selected_slots_us.IsCI(hash, span) => Field.all_commands_selected_slots_us, + sampled_command_selected_slots_us.HashCI when sampled_command_selected_slots_us.IsCI(hash, span) => Field.sampled_command_selected_slots_us, + sampled_commands_selected_slots_us.HashCI when sampled_commands_selected_slots_us.IsCI(hash, span) => Field.sampled_commands_selected_slots_us, + net_bytes_all_commands_all_slots.HashCI when net_bytes_all_commands_all_slots.IsCI(hash, span) => Field.net_bytes_all_commands_all_slots, + net_bytes_all_commands_selected_slots.HashCI when net_bytes_all_commands_selected_slots.IsCI(hash, span) => Field.net_bytes_all_commands_selected_slots, + net_bytes_sampled_commands_selected_slots.HashCI when net_bytes_sampled_commands_selected_slots.IsCI(hash, span) => Field.net_bytes_sampled_commands_selected_slots, + collection_start_time_unix_ms.HashCI when collection_start_time_unix_ms.IsCI(hash, span) => Field.collection_start_time_unix_ms, + collection_duration_ms.HashCI when collection_duration_ms.IsCI(hash, span) => Field.collection_duration_ms, + collection_duration_us.HashCI when collection_duration_us.IsCI(hash, span) => Field.collection_duration_us, + total_cpu_time_user_ms.HashCI when total_cpu_time_user_ms.IsCI(hash, span) => Field.total_cpu_time_user_ms, + total_cpu_time_user_us.HashCI when total_cpu_time_user_us.IsCI(hash, span) => Field.total_cpu_time_user_us, + total_cpu_time_sys_ms.HashCI when total_cpu_time_sys_ms.IsCI(hash, span) => Field.total_cpu_time_sys_ms, + total_cpu_time_sys_us.HashCI when total_cpu_time_sys_us.IsCI(hash, span) => Field.total_cpu_time_sys_us, + total_net_bytes.HashCI when total_net_bytes.IsCI(hash, span) => Field.total_net_bytes, + by_cpu_time_us.HashCI when by_cpu_time_us.IsCI(hash, span) => Field.by_cpu_time_us, + by_net_bytes.HashCI when by_net_bytes.IsCI(hash, span) => Field.by_net_bytes, + _ => Field.Unknown, + }; + } + + [Benchmark] + public Field SwitchOnLength_CI() + { + ReadOnlySpan span = _bytes; + var hash = FastHash.HashCI(span); + return span.Length switch + { + key.Length when key.IsCI(hash, span) => Field.key, + abc.Length when abc.IsCI(hash, span) => Field.abc, + port.Length when port.IsCI(hash, span) => Field.port, + test.Length when test.IsCI(hash, span) => Field.test, + tracking_active.Length when tracking_active.IsCI(hash, span) => Field.tracking_active, + sample_ratio.Length when sample_ratio.IsCI(hash, span) => Field.sample_ratio, + selected_slots.Length when selected_slots.IsCI(hash, span) => Field.selected_slots, + all_commands_all_slots_us.Length when all_commands_all_slots_us.IsCI(hash, span) => Field.all_commands_all_slots_us, + all_commands_selected_slots_us.Length when all_commands_selected_slots_us.IsCI(hash, span) => Field.all_commands_selected_slots_us, + sampled_command_selected_slots_us.Length when sampled_command_selected_slots_us.IsCI(hash, span) => Field.sampled_command_selected_slots_us, + sampled_commands_selected_slots_us.Length when sampled_commands_selected_slots_us.IsCI(hash, span) => Field.sampled_commands_selected_slots_us, + net_bytes_all_commands_all_slots.Length when net_bytes_all_commands_all_slots.IsCI(hash, span) => Field.net_bytes_all_commands_all_slots, + net_bytes_all_commands_selected_slots.Length when net_bytes_all_commands_selected_slots.IsCI(hash, span) => Field.net_bytes_all_commands_selected_slots, + net_bytes_sampled_commands_selected_slots.Length when net_bytes_sampled_commands_selected_slots.IsCI(hash, span) => Field.net_bytes_sampled_commands_selected_slots, + collection_start_time_unix_ms.Length when collection_start_time_unix_ms.IsCI(hash, span) => Field.collection_start_time_unix_ms, + collection_duration_ms.Length when collection_duration_ms.IsCI(hash, span) => Field.collection_duration_ms, + collection_duration_us.Length when collection_duration_us.IsCI(hash, span) => Field.collection_duration_us, + total_cpu_time_user_ms.Length when total_cpu_time_user_ms.IsCI(hash, span) => Field.total_cpu_time_user_ms, + total_cpu_time_user_us.Length when total_cpu_time_user_us.IsCI(hash, span) => Field.total_cpu_time_user_us, + total_cpu_time_sys_ms.Length when total_cpu_time_sys_ms.IsCI(hash, span) => Field.total_cpu_time_sys_ms, + total_cpu_time_sys_us.Length when total_cpu_time_sys_us.IsCI(hash, span) => Field.total_cpu_time_sys_us, + total_net_bytes.Length when total_net_bytes.IsCI(hash, span) => Field.total_net_bytes, + by_cpu_time_us.Length when by_cpu_time_us.IsCI(hash, span) => Field.by_cpu_time_us, + by_net_bytes.Length when by_net_bytes.IsCI(hash, span) => Field.by_net_bytes, + _ => Field.Unknown, + }; + } + + /* + we're using raw output from the code-gen, because BDN kinda hates the tooling, because + of the complex build pipe; this is left for reference only + + [FastHash] internal static partial class key { } + [FastHash] internal static partial class abc { } + [FastHash] internal static partial class port { } + [FastHash] internal static partial class test { } + [FastHash] internal static partial class tracking_active { } + [FastHash] internal static partial class sample_ratio { } + [FastHash] internal static partial class selected_slots { } + [FastHash] internal static partial class all_commands_all_slots_us { } + [FastHash] internal static partial class all_commands_selected_slots_us { } + [FastHash] internal static partial class sampled_command_selected_slots_us { } + [FastHash] internal static partial class sampled_commands_selected_slots_us { } + [FastHash] internal static partial class net_bytes_all_commands_all_slots { } + [FastHash] internal static partial class net_bytes_all_commands_selected_slots { } + [FastHash] internal static partial class net_bytes_sampled_commands_selected_slots { } + [FastHash] internal static partial class collection_start_time_unix_ms { } + [FastHash] internal static partial class collection_duration_ms { } + [FastHash] internal static partial class collection_duration_us { } + [FastHash] internal static partial class total_cpu_time_user_ms { } + [FastHash] internal static partial class total_cpu_time_user_us { } + [FastHash] internal static partial class total_cpu_time_sys_ms { } + [FastHash] internal static partial class total_cpu_time_sys_us { } + [FastHash] internal static partial class total_net_bytes { } + [FastHash] internal static partial class by_cpu_time_us { } + [FastHash] internal static partial class by_net_bytes { } + */ + + static class key + { + public const int Length = 3; + public const long HashCS = 7955819; + public const long HashCI = 5850443; + public static ReadOnlySpan U8 => "key"u8; + public const string Text = "key"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS & value.Length == Length; + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && (global::RESPite.FastHash.HashCS(value) == HashCS || global::RESPite.FastHash.EqualsCI(value, U8)); + } + static class abc + { + public const int Length = 3; + public const long HashCS = 6513249; + public const long HashCI = 4407873; + public static ReadOnlySpan U8 => "abc"u8; + public const string Text = "abc"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS & value.Length == Length; + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && (global::RESPite.FastHash.HashCS(value) == HashCS || global::RESPite.FastHash.EqualsCI(value, U8)); + } + static class port + { + public const int Length = 4; + public const long HashCS = 1953656688; + public const long HashCI = 1414680400; + public static ReadOnlySpan U8 => "port"u8; + public const string Text = "port"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS & value.Length == Length; + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && (global::RESPite.FastHash.HashCS(value) == HashCS || global::RESPite.FastHash.EqualsCI(value, U8)); + } + static class test + { + public const int Length = 4; + public const long HashCS = 1953719668; + public const long HashCI = 1414743380; + public static ReadOnlySpan U8 => "test"u8; + public const string Text = "test"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS & value.Length == Length; + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && (global::RESPite.FastHash.HashCS(value) == HashCS || global::RESPite.FastHash.EqualsCI(value, U8)); + } + static class tracking_active + { + public const int Length = 15; + public const long HashCS = 7453010343294497396; + public const long HashCI = 5138124812476043860; + public static ReadOnlySpan U8 => "tracking-active"u8; + public const string Text = "tracking-active"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class sample_ratio + { + public const int Length = 12; + public const long HashCS = 8227343610692854131; + public const long HashCI = 5912458079874400595; + public static ReadOnlySpan U8 => "sample-ratio"u8; + public const string Text = "sample-ratio"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class selected_slots + { + public const int Length = 14; + public const long HashCS = 7234316346692756851; + public const long HashCI = 4919430815874303315; + public static ReadOnlySpan U8 => "selected-slots"u8; + public const string Text = "selected-slots"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class all_commands_all_slots_us + { + public const int Length = 25; + public const long HashCS = 7885080994350132321; + public const long HashCI = 5570195463531678785; + public static ReadOnlySpan U8 => "all-commands-all-slots-us"u8; + public const string Text = "all-commands-all-slots-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class all_commands_selected_slots_us + { + public const int Length = 30; + public const long HashCS = 7885080994350132321; + public const long HashCI = 5570195463531678785; + public static ReadOnlySpan U8 => "all-commands-selected-slots-us"u8; + public const string Text = "all-commands-selected-slots-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class sampled_command_selected_slots_us + { + public const int Length = 33; + public const long HashCS = 3270850745794912627; + public const long HashCI = 955965214976459091; + public static ReadOnlySpan U8 => "sampled-command-selected-slots-us"u8; + public const string Text = "sampled-command-selected-slots-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class sampled_commands_selected_slots_us + { + public const int Length = 34; + public const long HashCS = 3270850745794912627; + public const long HashCI = 955965214976459091; + public static ReadOnlySpan U8 => "sampled-commands-selected-slots-us"u8; + public const string Text = "sampled-commands-selected-slots-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class net_bytes_all_commands_all_slots + { + public const int Length = 32; + public const long HashCS = 7310601557705516398; + public const long HashCI = 4995716026887062862; + public static ReadOnlySpan U8 => "net-bytes-all-commands-all-slots"u8; + public const string Text = "net-bytes-all-commands-all-slots"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class net_bytes_all_commands_selected_slots + { + public const int Length = 37; + public const long HashCS = 7310601557705516398; + public const long HashCI = 4995716026887062862; + public static ReadOnlySpan U8 => "net-bytes-all-commands-selected-slots"u8; + public const string Text = "net-bytes-all-commands-selected-slots"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class net_bytes_sampled_commands_selected_slots + { + public const int Length = 41; + public const long HashCS = 7310601557705516398; + public const long HashCI = 4995716026887062862; + public static ReadOnlySpan U8 => "net-bytes-sampled-commands-selected-slots"u8; + public const string Text = "net-bytes-sampled-commands-selected-slots"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class collection_start_time_unix_ms + { + public const int Length = 29; + public const long HashCS = 7598807758542761827; + public const long HashCI = 5283922227724308291; + public static ReadOnlySpan U8 => "collection-start-time-unix-ms"u8; + public const string Text = "collection-start-time-unix-ms"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class collection_duration_ms + { + public const int Length = 22; + public const long HashCS = 7598807758542761827; + public const long HashCI = 5283922227724308291; + public static ReadOnlySpan U8 => "collection-duration-ms"u8; + public const string Text = "collection-duration-ms"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class collection_duration_us + { + public const int Length = 22; + public const long HashCS = 7598807758542761827; + public const long HashCI = 5283922227724308291; + public static ReadOnlySpan U8 => "collection-duration-us"u8; + public const string Text = "collection-duration-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class total_cpu_time_user_ms + { + public const int Length = 22; + public const long HashCS = 8098366498457022324; + public const long HashCI = 5783480967638568788; + public static ReadOnlySpan U8 => "total-cpu-time-user-ms"u8; + public const string Text = "total-cpu-time-user-ms"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class total_cpu_time_user_us + { + public const int Length = 22; + public const long HashCS = 8098366498457022324; + public const long HashCI = 5783480967638568788; + public static ReadOnlySpan U8 => "total-cpu-time-user-us"u8; + public const string Text = "total-cpu-time-user-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class total_cpu_time_sys_ms + { + public const int Length = 21; + public const long HashCS = 8098366498457022324; + public const long HashCI = 5783480967638568788; + public static ReadOnlySpan U8 => "total-cpu-time-sys-ms"u8; + public const string Text = "total-cpu-time-sys-ms"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class total_cpu_time_sys_us + { + public const int Length = 21; + public const long HashCS = 8098366498457022324; + public const long HashCI = 5783480967638568788; + public static ReadOnlySpan U8 => "total-cpu-time-sys-us"u8; + public const string Text = "total-cpu-time-sys-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class total_net_bytes + { + public const int Length = 15; + public const long HashCS = 7308829188783632244; + public const long HashCI = 4993943657965178708; + public static ReadOnlySpan U8 => "total-net-bytes"u8; + public const string Text = "total-net-bytes"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class by_cpu_time_us + { + public const int Length = 14; + public const long HashCS = 8371476407912331618; + public const long HashCI = 6056590877093878082; + public static ReadOnlySpan U8 => "by-cpu-time-us"u8; + public const string Text = "by-cpu-time-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static class by_net_bytes + { + public const int Length = 12; + public const long HashCS = 7074438568657910114; + public const long HashCI = 4759553037839456578; + public static ReadOnlySpan U8 => "by-net-bytes"u8; + public const string Text = "by-net-bytes"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } +} diff --git a/tests/StackExchange.Redis.Benchmarks/FastHashSwitch.generated.cs b/tests/StackExchange.Redis.Benchmarks/FastHashSwitch.generated.cs new file mode 100644 index 000000000..935443117 --- /dev/null +++ b/tests/StackExchange.Redis.Benchmarks/FastHashSwitch.generated.cs @@ -0,0 +1,252 @@ +/* +using System; +using StackExchange.Redis; +#pragma warning disable CS8981 + +namespace StackExchange.Redis.Benchmarks +{ + partial class FastHashSwitch + { + static partial class key + { + public const int Length = 3; + public const long HashCS = 7955819; + public const long HashCI = 5850443; + public static ReadOnlySpan U8 => "key"u8; + public const string Text = "key"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS & value.Length == Length; + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && (global::RESPite.FastHash.HashCS(value) == HashCS || global::RESPite.FastHash.EqualsCI(value, U8)); + } + static partial class abc + { + public const int Length = 3; + public const long HashCS = 6513249; + public const long HashCI = 4407873; + public static ReadOnlySpan U8 => "abc"u8; + public const string Text = "abc"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS & value.Length == Length; + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && (global::RESPite.FastHash.HashCS(value) == HashCS || global::RESPite.FastHash.EqualsCI(value, U8)); + } + static partial class port + { + public const int Length = 4; + public const long HashCS = 1953656688; + public const long HashCI = 1414680400; + public static ReadOnlySpan U8 => "port"u8; + public const string Text = "port"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS & value.Length == Length; + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && (global::RESPite.FastHash.HashCS(value) == HashCS || global::RESPite.FastHash.EqualsCI(value, U8)); + } + static partial class test + { + public const int Length = 4; + public const long HashCS = 1953719668; + public const long HashCI = 1414743380; + public static ReadOnlySpan U8 => "test"u8; + public const string Text = "test"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS & value.Length == Length; + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && (global::RESPite.FastHash.HashCS(value) == HashCS || global::RESPite.FastHash.EqualsCI(value, U8)); + } + static partial class tracking_active + { + public const int Length = 15; + public const long HashCS = 7453010343294497396; + public const long HashCI = 5138124812476043860; + public static ReadOnlySpan U8 => "tracking-active"u8; + public const string Text = "tracking-active"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class sample_ratio + { + public const int Length = 12; + public const long HashCS = 8227343610692854131; + public const long HashCI = 5912458079874400595; + public static ReadOnlySpan U8 => "sample-ratio"u8; + public const string Text = "sample-ratio"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class selected_slots + { + public const int Length = 14; + public const long HashCS = 7234316346692756851; + public const long HashCI = 4919430815874303315; + public static ReadOnlySpan U8 => "selected-slots"u8; + public const string Text = "selected-slots"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class all_commands_all_slots_us + { + public const int Length = 25; + public const long HashCS = 7885080994350132321; + public const long HashCI = 5570195463531678785; + public static ReadOnlySpan U8 => "all-commands-all-slots-us"u8; + public const string Text = "all-commands-all-slots-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class all_commands_selected_slots_us + { + public const int Length = 30; + public const long HashCS = 7885080994350132321; + public const long HashCI = 5570195463531678785; + public static ReadOnlySpan U8 => "all-commands-selected-slots-us"u8; + public const string Text = "all-commands-selected-slots-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class sampled_command_selected_slots_us + { + public const int Length = 33; + public const long HashCS = 3270850745794912627; + public const long HashCI = 955965214976459091; + public static ReadOnlySpan U8 => "sampled-command-selected-slots-us"u8; + public const string Text = "sampled-command-selected-slots-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class sampled_commands_selected_slots_us + { + public const int Length = 34; + public const long HashCS = 3270850745794912627; + public const long HashCI = 955965214976459091; + public static ReadOnlySpan U8 => "sampled-commands-selected-slots-us"u8; + public const string Text = "sampled-commands-selected-slots-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class net_bytes_all_commands_all_slots + { + public const int Length = 32; + public const long HashCS = 7310601557705516398; + public const long HashCI = 4995716026887062862; + public static ReadOnlySpan U8 => "net-bytes-all-commands-all-slots"u8; + public const string Text = "net-bytes-all-commands-all-slots"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class net_bytes_all_commands_selected_slots + { + public const int Length = 37; + public const long HashCS = 7310601557705516398; + public const long HashCI = 4995716026887062862; + public static ReadOnlySpan U8 => "net-bytes-all-commands-selected-slots"u8; + public const string Text = "net-bytes-all-commands-selected-slots"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class net_bytes_sampled_commands_selected_slots + { + public const int Length = 41; + public const long HashCS = 7310601557705516398; + public const long HashCI = 4995716026887062862; + public static ReadOnlySpan U8 => "net-bytes-sampled-commands-selected-slots"u8; + public const string Text = "net-bytes-sampled-commands-selected-slots"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class collection_start_time_unix_ms + { + public const int Length = 29; + public const long HashCS = 7598807758542761827; + public const long HashCI = 5283922227724308291; + public static ReadOnlySpan U8 => "collection-start-time-unix-ms"u8; + public const string Text = "collection-start-time-unix-ms"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class collection_duration_ms + { + public const int Length = 22; + public const long HashCS = 7598807758542761827; + public const long HashCI = 5283922227724308291; + public static ReadOnlySpan U8 => "collection-duration-ms"u8; + public const string Text = "collection-duration-ms"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class collection_duration_us + { + public const int Length = 22; + public const long HashCS = 7598807758542761827; + public const long HashCI = 5283922227724308291; + public static ReadOnlySpan U8 => "collection-duration-us"u8; + public const string Text = "collection-duration-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class total_cpu_time_user_ms + { + public const int Length = 22; + public const long HashCS = 8098366498457022324; + public const long HashCI = 5783480967638568788; + public static ReadOnlySpan U8 => "total-cpu-time-user-ms"u8; + public const string Text = "total-cpu-time-user-ms"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class total_cpu_time_user_us + { + public const int Length = 22; + public const long HashCS = 8098366498457022324; + public const long HashCI = 5783480967638568788; + public static ReadOnlySpan U8 => "total-cpu-time-user-us"u8; + public const string Text = "total-cpu-time-user-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class total_cpu_time_sys_ms + { + public const int Length = 21; + public const long HashCS = 8098366498457022324; + public const long HashCI = 5783480967638568788; + public static ReadOnlySpan U8 => "total-cpu-time-sys-ms"u8; + public const string Text = "total-cpu-time-sys-ms"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class total_cpu_time_sys_us + { + public const int Length = 21; + public const long HashCS = 8098366498457022324; + public const long HashCI = 5783480967638568788; + public static ReadOnlySpan U8 => "total-cpu-time-sys-us"u8; + public const string Text = "total-cpu-time-sys-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class total_net_bytes + { + public const int Length = 15; + public const long HashCS = 7308829188783632244; + public const long HashCI = 4993943657965178708; + public static ReadOnlySpan U8 => "total-net-bytes"u8; + public const string Text = "total-net-bytes"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class by_cpu_time_us + { + public const int Length = 14; + public const long HashCS = 8371476407912331618; + public const long HashCI = 6056590877093878082; + public static ReadOnlySpan U8 => "by-cpu-time-us"u8; + public const string Text = "by-cpu-time-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + static partial class by_net_bytes + { + public const int Length = 12; + public const long HashCS = 7074438568657910114; + public const long HashCI = 4759553037839456578; + public static ReadOnlySpan U8 => "by-net-bytes"u8; + public const string Text = "by-net-bytes"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8); + } + } +} +*/ diff --git a/tests/StackExchange.Redis.Benchmarks/FormatBenchmarks.cs b/tests/StackExchange.Redis.Benchmarks/FormatBenchmarks.cs index 77548b254..714e1724a 100644 --- a/tests/StackExchange.Redis.Benchmarks/FormatBenchmarks.cs +++ b/tests/StackExchange.Redis.Benchmarks/FormatBenchmarks.cs @@ -1,4 +1,5 @@ -using System; +/* +using System; using System.Net; using BenchmarkDotNet.Attributes; @@ -51,3 +52,4 @@ public void Setup() { } public EndPoint ParseEndPoint(string host, int port) => Format.ParseEndPoint(host, port); } } +*/ diff --git a/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj b/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj index 8b335ab02..665adae3b 100644 --- a/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj +++ b/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj @@ -1,7 +1,7 @@  StackExchange.Redis MicroBenchmark Suite - net481;net8.0 + net481;net8.0;net10.0 Release Exe true @@ -10,7 +10,8 @@ - - + + + diff --git a/tests/StackExchange.Redis.Tests/FastHashTests.cs b/tests/StackExchange.Redis.Tests/FastHashTests.cs index a032cfc80..d506d183e 100644 --- a/tests/StackExchange.Redis.Tests/FastHashTests.cs +++ b/tests/StackExchange.Redis.Tests/FastHashTests.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.InteropServices; using System.Text; +using RESPite; using Xunit; using Xunit.Sdk; @@ -14,31 +15,31 @@ public partial class FastHashTests(ITestOutputHelper log) // note: if the hashing algorithm changes, we can update the last parameter freely; it doesn't matter // what it *is* - what matters is that we can see that it has entropy between different values [Theory] - [InlineData(1, a.Length, a.Text, a.Hash, 97)] - [InlineData(2, ab.Length, ab.Text, ab.Hash, 25185)] - [InlineData(3, abc.Length, abc.Text, abc.Hash, 6513249)] - [InlineData(4, abcd.Length, abcd.Text, abcd.Hash, 1684234849)] - [InlineData(5, abcde.Length, abcde.Text, abcde.Hash, 435475931745)] - [InlineData(6, abcdef.Length, abcdef.Text, abcdef.Hash, 112585661964897)] - [InlineData(7, abcdefg.Length, abcdefg.Text, abcdefg.Hash, 29104508263162465)] - [InlineData(8, abcdefgh.Length, abcdefgh.Text, abcdefgh.Hash, 7523094288207667809)] - - [InlineData(1, x.Length, x.Text, x.Hash, 120)] - [InlineData(2, xx.Length, xx.Text, xx.Hash, 30840)] - [InlineData(3, xxx.Length, xxx.Text, xxx.Hash, 7895160)] - [InlineData(4, xxxx.Length, xxxx.Text, xxxx.Hash, 2021161080)] - [InlineData(5, xxxxx.Length, xxxxx.Text, xxxxx.Hash, 517417236600)] - [InlineData(6, xxxxxx.Length, xxxxxx.Text, xxxxxx.Hash, 132458812569720)] - [InlineData(7, xxxxxxx.Length, xxxxxxx.Text, xxxxxxx.Hash, 33909456017848440)] - [InlineData(8, xxxxxxxx.Length, xxxxxxxx.Text, xxxxxxxx.Hash, 8680820740569200760)] - - [InlineData(3, 窓.Length, 窓.Text, 窓.Hash, 9677543, "窓")] - [InlineData(20, abcdefghijklmnopqrst.Length, abcdefghijklmnopqrst.Text, abcdefghijklmnopqrst.Hash, 7523094288207667809)] + [InlineData(1, a.Length, a.Text, a.HashCS, 97)] + [InlineData(2, ab.Length, ab.Text, ab.HashCS, 25185)] + [InlineData(3, abc.Length, abc.Text, abc.HashCS, 6513249)] + [InlineData(4, abcd.Length, abcd.Text, abcd.HashCS, 1684234849)] + [InlineData(5, abcde.Length, abcde.Text, abcde.HashCS, 435475931745)] + [InlineData(6, abcdef.Length, abcdef.Text, abcdef.HashCS, 112585661964897)] + [InlineData(7, abcdefg.Length, abcdefg.Text, abcdefg.HashCS, 29104508263162465)] + [InlineData(8, abcdefgh.Length, abcdefgh.Text, abcdefgh.HashCS, 7523094288207667809)] + + [InlineData(1, x.Length, x.Text, x.HashCS, 120)] + [InlineData(2, xx.Length, xx.Text, xx.HashCS, 30840)] + [InlineData(3, xxx.Length, xxx.Text, xxx.HashCS, 7895160)] + [InlineData(4, xxxx.Length, xxxx.Text, xxxx.HashCS, 2021161080)] + [InlineData(5, xxxxx.Length, xxxxx.Text, xxxxx.HashCS, 517417236600)] + [InlineData(6, xxxxxx.Length, xxxxxx.Text, xxxxxx.HashCS, 132458812569720)] + [InlineData(7, xxxxxxx.Length, xxxxxxx.Text, xxxxxxx.HashCS, 33909456017848440)] + [InlineData(8, xxxxxxxx.Length, xxxxxxxx.Text, xxxxxxxx.HashCS, 8680820740569200760)] + + [InlineData(3, 窓.Length, 窓.Text, 窓.HashCS, 9677543, "窓")] + [InlineData(20, abcdefghijklmnopqrst.Length, abcdefghijklmnopqrst.Text, abcdefghijklmnopqrst.HashCS, 7523094288207667809)] // show that foo_bar is interpreted as foo-bar - [InlineData(7, foo_bar.Length, foo_bar.Text, foo_bar.Hash, 32195221641981798, "foo-bar", nameof(foo_bar))] - [InlineData(7, foo_bar_hyphen.Length, foo_bar_hyphen.Text, foo_bar_hyphen.Hash, 32195221641981798, "foo-bar", nameof(foo_bar_hyphen))] - [InlineData(7, foo_bar_underscore.Length, foo_bar_underscore.Text, foo_bar_underscore.Hash, 32195222480842598, "foo_bar", nameof(foo_bar_underscore))] + [InlineData(7, foo_bar.Length, foo_bar.Text, foo_bar.HashCS, 32195221641981798, "foo-bar", nameof(foo_bar))] + [InlineData(7, foo_bar_hyphen.Length, foo_bar_hyphen.Text, foo_bar_hyphen.HashCS, 32195221641981798, "foo-bar", nameof(foo_bar_hyphen))] + [InlineData(7, foo_bar_underscore.Length, foo_bar_underscore.Text, foo_bar_underscore.HashCS, 32195222480842598, "foo_bar", nameof(foo_bar_underscore))] public void Validate(int expectedLength, int actualLength, string actualValue, long actualHash, long expectedHash, string? expectedValue = null, string originForDisambiguation = "") { _ = originForDisambiguation; // to allow otherwise-identical test data to coexist @@ -46,7 +47,7 @@ public void Validate(int expectedLength, int actualLength, string actualValue, l Assert.Equal(expectedHash, actualHash); var bytes = Encoding.UTF8.GetBytes(actualValue); Assert.Equal(expectedLength, bytes.Length); - Assert.Equal(expectedHash, FastHash.Hash64(bytes)); + Assert.Equal(expectedHash, FastHash.HashCS(bytes)); #pragma warning disable CS0618 // Type or member is obsolete Assert.Equal(expectedHash, FastHash.Hash64Fallback(bytes)); #pragma warning restore CS0618 // Type or member is obsolete @@ -60,28 +61,376 @@ public void Validate(int expectedLength, int actualLength, string actualValue, l public void FastHashIs_Short() { ReadOnlySpan value = "abc"u8; - var hash = value.Hash64(); - Assert.Equal(abc.Hash, hash); - Assert.True(abc.Is(hash, value)); + var hash = FastHash.HashCS(value); + Assert.Equal(abc.HashCS, hash); + Assert.True(abc.IsCS(hash, value)); value = "abz"u8; - hash = value.Hash64(); - Assert.NotEqual(abc.Hash, hash); - Assert.False(abc.Is(hash, value)); + hash = FastHash.HashCS(value); + Assert.NotEqual(abc.HashCS, hash); + Assert.False(abc.IsCS(hash, value)); } [Fact] public void FastHashIs_Long() { ReadOnlySpan value = "abcdefghijklmnopqrst"u8; - var hash = value.Hash64(); - Assert.Equal(abcdefghijklmnopqrst.Hash, hash); - Assert.True(abcdefghijklmnopqrst.Is(hash, value)); + var hash = FastHash.HashCS(value); + Assert.Equal(abcdefghijklmnopqrst.HashCS, hash); + Assert.True(abcdefghijklmnopqrst.IsCS(hash, value)); value = "abcdefghijklmnopqrsz"u8; - hash = value.Hash64(); - Assert.Equal(abcdefghijklmnopqrst.Hash, hash); // hash collision, fine - Assert.False(abcdefghijklmnopqrst.Is(hash, value)); + hash = FastHash.HashCS(value); + Assert.Equal(abcdefghijklmnopqrst.HashCS, hash); // hash collision, fine + Assert.False(abcdefghijklmnopqrst.IsCS(hash, value)); + } + + // Test case-sensitive and case-insensitive equality for various lengths + [Theory] + [InlineData("a")] // length 1 + [InlineData("ab")] // length 2 + [InlineData("abc")] // length 3 + [InlineData("abcd")] // length 4 + [InlineData("abcde")] // length 5 + [InlineData("abcdef")] // length 6 + [InlineData("abcdefg")] // length 7 + [InlineData("abcdefgh")] // length 8 + [InlineData("abcdefghi")] // length 9 + [InlineData("abcdefghij")] // length 10 + [InlineData("abcdefghijklmnop")] // length 16 + [InlineData("abcdefghijklmnopqrst")] // length 20 + public void CaseSensitiveEquality(string text) + { + var lower = Encoding.UTF8.GetBytes(text); + var upper = Encoding.UTF8.GetBytes(text.ToUpperInvariant()); + + var hashLowerCS = FastHash.HashCS(lower); + var hashUpperCS = FastHash.HashCS(upper); + + // Case-sensitive: same case should match + Assert.True(FastHash.EqualsCS(lower, lower), "CS: lower == lower"); + Assert.True(FastHash.EqualsCS(upper, upper), "CS: upper == upper"); + + // Case-sensitive: different case should NOT match + Assert.False(FastHash.EqualsCS(lower, upper), "CS: lower != upper"); + Assert.False(FastHash.EqualsCS(upper, lower), "CS: upper != lower"); + + // Hashes should be different for different cases + Assert.NotEqual(hashLowerCS, hashUpperCS); + } + + [Theory] + [InlineData("a")] // length 1 + [InlineData("ab")] // length 2 + [InlineData("abc")] // length 3 + [InlineData("abcd")] // length 4 + [InlineData("abcde")] // length 5 + [InlineData("abcdef")] // length 6 + [InlineData("abcdefg")] // length 7 + [InlineData("abcdefgh")] // length 8 + [InlineData("abcdefghi")] // length 9 + [InlineData("abcdefghij")] // length 10 + [InlineData("abcdefghijklmnop")] // length 16 + [InlineData("abcdefghijklmnopqrst")] // length 20 + public void CaseInsensitiveEquality(string text) + { + var lower = Encoding.UTF8.GetBytes(text); + var upper = Encoding.UTF8.GetBytes(text.ToUpperInvariant()); + + var hashLowerCI = FastHash.HashCI(lower); + var hashUpperCI = FastHash.HashCI(upper); + + // Case-insensitive: same case should match + Assert.True(FastHash.EqualsCI(lower, lower), "CI: lower == lower"); + Assert.True(FastHash.EqualsCI(upper, upper), "CI: upper == upper"); + + // Case-insensitive: different case SHOULD match + Assert.True(FastHash.EqualsCI(lower, upper), "CI: lower == upper"); + Assert.True(FastHash.EqualsCI(upper, lower), "CI: upper == lower"); + + // CI hashes should be the same for different cases + Assert.Equal(hashLowerCI, hashUpperCI); + } + + [Theory] + [InlineData("a")] // length 1 + [InlineData("ab")] // length 2 + [InlineData("abc")] // length 3 + [InlineData("abcd")] // length 4 + [InlineData("abcde")] // length 5 + [InlineData("abcdef")] // length 6 + [InlineData("abcdefg")] // length 7 + [InlineData("abcdefgh")] // length 8 + [InlineData("abcdefghi")] // length 9 + [InlineData("abcdefghij")] // length 10 + [InlineData("abcdefghijklmnop")] // length 16 + [InlineData("abcdefghijklmnopqrst")] // length 20 + [InlineData("foo-bar")] // foo_bar_hyphen + [InlineData("foo_bar")] // foo_bar_underscore + public void GeneratedTypes_CaseSensitive(string text) + { + var lower = Encoding.UTF8.GetBytes(text); + var upper = Encoding.UTF8.GetBytes(text.ToUpperInvariant()); + + var hashLowerCS = FastHash.HashCS(lower); + var hashUpperCS = FastHash.HashCS(upper); + + // Use the generated types to verify CS behavior + switch (text) + { + case "a": + Assert.True(a.IsCS(hashLowerCS, lower)); + Assert.False(a.IsCS(hashUpperCS, upper)); + break; + case "ab": + Assert.True(ab.IsCS(hashLowerCS, lower)); + Assert.False(ab.IsCS(hashUpperCS, upper)); + break; + case "abc": + Assert.True(abc.IsCS(hashLowerCS, lower)); + Assert.False(abc.IsCS(hashUpperCS, upper)); + break; + case "abcd": + Assert.True(abcd.IsCS(hashLowerCS, lower)); + Assert.False(abcd.IsCS(hashUpperCS, upper)); + break; + case "abcde": + Assert.True(abcde.IsCS(hashLowerCS, lower)); + Assert.False(abcde.IsCS(hashUpperCS, upper)); + break; + case "abcdef": + Assert.True(abcdef.IsCS(hashLowerCS, lower)); + Assert.False(abcdef.IsCS(hashUpperCS, upper)); + break; + case "abcdefg": + Assert.True(abcdefg.IsCS(hashLowerCS, lower)); + Assert.False(abcdefg.IsCS(hashUpperCS, upper)); + break; + case "abcdefgh": + Assert.True(abcdefgh.IsCS(hashLowerCS, lower)); + Assert.False(abcdefgh.IsCS(hashUpperCS, upper)); + break; + case "abcdefghijklmnopqrst": + Assert.True(abcdefghijklmnopqrst.IsCS(hashLowerCS, lower)); + Assert.False(abcdefghijklmnopqrst.IsCS(hashUpperCS, upper)); + break; + case "foo-bar": + Assert.True(foo_bar_hyphen.IsCS(hashLowerCS, lower)); + Assert.False(foo_bar_hyphen.IsCS(hashUpperCS, upper)); + break; + case "foo_bar": + Assert.True(foo_bar_underscore.IsCS(hashLowerCS, lower)); + Assert.False(foo_bar_underscore.IsCS(hashUpperCS, upper)); + break; + } + } + + [Theory] + [InlineData("a")] // length 1 + [InlineData("ab")] // length 2 + [InlineData("abc")] // length 3 + [InlineData("abcd")] // length 4 + [InlineData("abcde")] // length 5 + [InlineData("abcdef")] // length 6 + [InlineData("abcdefg")] // length 7 + [InlineData("abcdefgh")] // length 8 + [InlineData("abcdefghi")] // length 9 + [InlineData("abcdefghij")] // length 10 + [InlineData("abcdefghijklmnop")] // length 16 + [InlineData("abcdefghijklmnopqrst")] // length 20 + [InlineData("foo-bar")] // foo_bar_hyphen + [InlineData("foo_bar")] // foo_bar_underscore + public void GeneratedTypes_CaseInsensitive(string text) + { + var lower = Encoding.UTF8.GetBytes(text); + var upper = Encoding.UTF8.GetBytes(text.ToUpperInvariant()); + + var hashLowerCI = FastHash.HashCI(lower); + var hashUpperCI = FastHash.HashCI(upper); + + // Use the generated types to verify CI behavior + switch (text) + { + case "a": + Assert.True(a.IsCI(hashLowerCI, lower)); + Assert.True(a.IsCI(hashUpperCI, upper)); + break; + case "ab": + Assert.True(ab.IsCI(hashLowerCI, lower)); + Assert.True(ab.IsCI(hashUpperCI, upper)); + break; + case "abc": + Assert.True(abc.IsCI(hashLowerCI, lower)); + Assert.True(abc.IsCI(hashUpperCI, upper)); + break; + case "abcd": + Assert.True(abcd.IsCI(hashLowerCI, lower)); + Assert.True(abcd.IsCI(hashUpperCI, upper)); + break; + case "abcde": + Assert.True(abcde.IsCI(hashLowerCI, lower)); + Assert.True(abcde.IsCI(hashUpperCI, upper)); + break; + case "abcdef": + Assert.True(abcdef.IsCI(hashLowerCI, lower)); + Assert.True(abcdef.IsCI(hashUpperCI, upper)); + break; + case "abcdefg": + Assert.True(abcdefg.IsCI(hashLowerCI, lower)); + Assert.True(abcdefg.IsCI(hashUpperCI, upper)); + break; + case "abcdefgh": + Assert.True(abcdefgh.IsCI(hashLowerCI, lower)); + Assert.True(abcdefgh.IsCI(hashUpperCI, upper)); + break; + case "abcdefghijklmnopqrst": + Assert.True(abcdefghijklmnopqrst.IsCI(hashLowerCI, lower)); + Assert.True(abcdefghijklmnopqrst.IsCI(hashUpperCI, upper)); + break; + case "foo-bar": + Assert.True(foo_bar_hyphen.IsCI(hashLowerCI, lower)); + Assert.True(foo_bar_hyphen.IsCI(hashUpperCI, upper)); + break; + case "foo_bar": + Assert.True(foo_bar_underscore.IsCI(hashLowerCI, lower)); + Assert.True(foo_bar_underscore.IsCI(hashUpperCI, upper)); + break; + } + } + + // Test each generated FastHash type individually for case sensitivity + [Fact] + public void GeneratedType_a_CaseSensitivity() + { + ReadOnlySpan lower = "a"u8; + ReadOnlySpan upper = "A"u8; + + Assert.True(a.IsCS(FastHash.HashCS(lower), lower)); + Assert.False(a.IsCS(FastHash.HashCS(upper), upper)); + Assert.True(a.IsCI(FastHash.HashCI(lower), lower)); + Assert.True(a.IsCI(FastHash.HashCI(upper), upper)); + } + + [Fact] + public void GeneratedType_ab_CaseSensitivity() + { + ReadOnlySpan lower = "ab"u8; + ReadOnlySpan upper = "AB"u8; + + Assert.True(ab.IsCS(FastHash.HashCS(lower), lower)); + Assert.False(ab.IsCS(FastHash.HashCS(upper), upper)); + Assert.True(ab.IsCI(FastHash.HashCI(lower), lower)); + Assert.True(ab.IsCI(FastHash.HashCI(upper), upper)); + } + + [Fact] + public void GeneratedType_abc_CaseSensitivity() + { + ReadOnlySpan lower = "abc"u8; + ReadOnlySpan upper = "ABC"u8; + + Assert.True(abc.IsCS(FastHash.HashCS(lower), lower)); + Assert.False(abc.IsCS(FastHash.HashCS(upper), upper)); + Assert.True(abc.IsCI(FastHash.HashCI(lower), lower)); + Assert.True(abc.IsCI(FastHash.HashCI(upper), upper)); + } + + [Fact] + public void GeneratedType_abcd_CaseSensitivity() + { + ReadOnlySpan lower = "abcd"u8; + ReadOnlySpan upper = "ABCD"u8; + + Assert.True(abcd.IsCS(FastHash.HashCS(lower), lower)); + Assert.False(abcd.IsCS(FastHash.HashCS(upper), upper)); + Assert.True(abcd.IsCI(FastHash.HashCI(lower), lower)); + Assert.True(abcd.IsCI(FastHash.HashCI(upper), upper)); + } + + [Fact] + public void GeneratedType_abcde_CaseSensitivity() + { + ReadOnlySpan lower = "abcde"u8; + ReadOnlySpan upper = "ABCDE"u8; + + Assert.True(abcde.IsCS(FastHash.HashCS(lower), lower)); + Assert.False(abcde.IsCS(FastHash.HashCS(upper), upper)); + Assert.True(abcde.IsCI(FastHash.HashCI(lower), lower)); + Assert.True(abcde.IsCI(FastHash.HashCI(upper), upper)); + } + + [Fact] + public void GeneratedType_abcdef_CaseSensitivity() + { + ReadOnlySpan lower = "abcdef"u8; + ReadOnlySpan upper = "ABCDEF"u8; + + Assert.True(abcdef.IsCS(FastHash.HashCS(lower), lower)); + Assert.False(abcdef.IsCS(FastHash.HashCS(upper), upper)); + Assert.True(abcdef.IsCI(FastHash.HashCI(lower), lower)); + Assert.True(abcdef.IsCI(FastHash.HashCI(upper), upper)); + } + + [Fact] + public void GeneratedType_abcdefg_CaseSensitivity() + { + ReadOnlySpan lower = "abcdefg"u8; + ReadOnlySpan upper = "ABCDEFG"u8; + + Assert.True(abcdefg.IsCS(FastHash.HashCS(lower), lower)); + Assert.False(abcdefg.IsCS(FastHash.HashCS(upper), upper)); + Assert.True(abcdefg.IsCI(FastHash.HashCI(lower), lower)); + Assert.True(abcdefg.IsCI(FastHash.HashCI(upper), upper)); + } + + [Fact] + public void GeneratedType_abcdefgh_CaseSensitivity() + { + ReadOnlySpan lower = "abcdefgh"u8; + ReadOnlySpan upper = "ABCDEFGH"u8; + + Assert.True(abcdefgh.IsCS(FastHash.HashCS(lower), lower)); + Assert.False(abcdefgh.IsCS(FastHash.HashCS(upper), upper)); + Assert.True(abcdefgh.IsCI(FastHash.HashCI(lower), lower)); + Assert.True(abcdefgh.IsCI(FastHash.HashCI(upper), upper)); + } + + [Fact] + public void GeneratedType_abcdefghijklmnopqrst_CaseSensitivity() + { + ReadOnlySpan lower = "abcdefghijklmnopqrst"u8; + ReadOnlySpan upper = "ABCDEFGHIJKLMNOPQRST"u8; + + Assert.True(abcdefghijklmnopqrst.IsCS(FastHash.HashCS(lower), lower)); + Assert.False(abcdefghijklmnopqrst.IsCS(FastHash.HashCS(upper), upper)); + Assert.True(abcdefghijklmnopqrst.IsCI(FastHash.HashCI(lower), lower)); + Assert.True(abcdefghijklmnopqrst.IsCI(FastHash.HashCI(upper), upper)); + } + + [Fact] + public void GeneratedType_foo_bar_CaseSensitivity() + { + // foo_bar is interpreted as foo-bar + ReadOnlySpan lower = "foo-bar"u8; + ReadOnlySpan upper = "FOO-BAR"u8; + + Assert.True(foo_bar.IsCS(FastHash.HashCS(lower), lower)); + Assert.False(foo_bar.IsCS(FastHash.HashCS(upper), upper)); + Assert.True(foo_bar.IsCI(FastHash.HashCI(lower), lower)); + Assert.True(foo_bar.IsCI(FastHash.HashCI(upper), upper)); + } + + [Fact] + public void GeneratedType_foo_bar_hyphen_CaseSensitivity() + { + // foo_bar_hyphen is explicitly "foo-bar" + ReadOnlySpan lower = "foo-bar"u8; + ReadOnlySpan upper = "FOO-BAR"u8; + + Assert.True(foo_bar_hyphen.IsCS(FastHash.HashCS(lower), lower)); + Assert.False(foo_bar_hyphen.IsCS(FastHash.HashCS(upper), upper)); + Assert.True(foo_bar_hyphen.IsCI(FastHash.HashCI(lower), lower)); + Assert.True(foo_bar_hyphen.IsCI(FastHash.HashCI(upper), upper)); } [Fact] diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/AutoConfigure.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/AutoConfigure.cs new file mode 100644 index 000000000..f8b0daa0d --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/AutoConfigure.cs @@ -0,0 +1,130 @@ +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class AutoConfigure(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + [Fact] + public void ClientId_Integer_Success() + { + // CLIENT ID response + var resp = ":11\r\n"; + var message = Message.Create(-1, default, RedisCommand.CLIENT); + + // Note: This will return false because we don't have a real connection with a server endpoint + // The processor will throw because it can't set the connection ID without a real connection + var success = TryExecute(resp, ResultProcessor.AutoConfigure, out bool result, out var exception, message); + + Assert.False(success); + Assert.NotNull(exception); + Assert.IsType(exception); + } + + [Fact] + public void Info_BulkString_Success() + { + // INFO response with replication info + var info = "# Replication\r\n" + + "role:master\r\n" + + "connected_slaves:0\r\n" + + "master_failover_state:no-failover\r\n" + + "master_replid:8c3e3c3e3c3e3c3e3c3e3c3e3c3e3c3e3c3e3c3e\r\n" + + "master_replid2:0000000000000000000000000000000000000000\r\n" + + "master_repl_offset:0\r\n" + + "second_repl_offset:-1\r\n" + + "repl_backlog_active:0\r\n" + + "repl_backlog_size:1048576\r\n" + + "repl_backlog_first_byte_offset:0\r\n" + + "repl_backlog_histlen:0\r\n"; + + var resp = $"${info.Length}\r\n{info}\r\n"; + var message = Message.Create(-1, default, RedisCommand.INFO); + + // Note: This will return false because we don't have a real connection with a server endpoint + var success = TryExecute(resp, ResultProcessor.AutoConfigure, out bool result, out var exception, message); + + Assert.False(success); + Assert.NotNull(exception); + Assert.IsType(exception); + } + + [Fact] + public void Info_WithVersion_Success() + { + // INFO response with version info + var info = "# Server\r\n" + + "redis_version:7.2.4\r\n" + + "redis_git_sha1:00000000\r\n" + + "redis_mode:standalone\r\n" + + "os:Linux 5.15.0-1-amd64 x86_64\r\n" + + "arch_bits:64\r\n"; + + var resp = $"${info.Length}\r\n{info}\r\n"; + var message = Message.Create(-1, default, RedisCommand.INFO); + + var success = TryExecute(resp, ResultProcessor.AutoConfigure, out bool result, out var exception, message); + + Assert.False(success); + Assert.NotNull(exception); + Assert.IsType(exception); + } + + [Fact] + public void Info_EmptyString_Success() + { + // Empty INFO response + var resp = "$0\r\n\r\n"; + var message = Message.Create(-1, default, RedisCommand.INFO); + + var success = TryExecute(resp, ResultProcessor.AutoConfigure, out bool result, out var exception, message); + + Assert.False(success); + Assert.NotNull(exception); + Assert.IsType(exception); + } + + [Fact] + public void Info_Null_Success() + { + // Null INFO response + var resp = "$-1\r\n"; + var message = Message.Create(-1, default, RedisCommand.INFO); + + var success = TryExecute(resp, ResultProcessor.AutoConfigure, out bool result, out var exception, message); + + Assert.False(success); + Assert.NotNull(exception); + Assert.IsType(exception); + } + + [Fact] + public void Config_Array_Success() + { + // CONFIG GET timeout response + var resp = "*2\r\n" + + "$7\r\ntimeout\r\n" + + "$3\r\n300\r\n"; + var message = Message.Create(-1, default, RedisCommand.CONFIG); + + var success = TryExecute(resp, ResultProcessor.AutoConfigure, out bool result, out var exception, message); + + Assert.False(success); + Assert.NotNull(exception); + Assert.IsType(exception); + } + + [Fact] + public void ReadonlyError_Success() + { + // READONLY error response + var resp = "-READONLY You can't write against a read only replica.\r\n"; + var message = DummyMessage(); + + var success = TryExecute(resp, ResultProcessor.AutoConfigure, out bool result, out var exception, message); + + // Should handle the error - returns RedisServerException for error responses + Assert.False(success); + Assert.NotNull(exception); + Assert.IsType(exception); + } +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/ClientInfo.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/ClientInfo.cs new file mode 100644 index 000000000..086ef0566 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/ClientInfo.cs @@ -0,0 +1,93 @@ +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class ClientInfo(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + [Fact] + public void SingleClient_Success() + { + // CLIENT LIST returns a bulk string with newline-separated client information + var content = "id=86 addr=172.17.0.1:40750 laddr=172.17.0.2:3000 fd=22 name= age=7 idle=0 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=26 qbuf-free=20448 argv-mem=10 multi-mem=0 rbs=1024 rbp=0 obl=0 oll=0 omem=0 tot-mem=22810 events=r cmd=client|list user=default redir=-1 resp=2 lib-name= lib-ver= io-thread=0 tot-net-in=48 tot-net-out=36 tot-cmds=0\n"; + var resp = $"${content.Length}\r\n{content}\r\n"; + + var result = Execute(resp, StackExchange.Redis.ClientInfo.Processor); + + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal(86, result[0].Id); + Assert.Equal("172.17.0.1:40750", result[0].Address?.ToString()); + Assert.Equal(7, result[0].AgeSeconds); + Assert.Equal(0, result[0].Database); + } + + [Fact] + public void MultipleClients_Success() + { + // Two clients (newline-separated, using \n not \r\n) + var line1 = "id=86 addr=172.17.0.1:40750 laddr=172.17.0.2:3000 fd=22 name= age=39 idle=32 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=1024 rbp=0 obl=0 oll=0 omem=0 tot-mem=2304 events=r cmd=client|list user=default redir=-1 resp=2 lib-name= lib-ver= io-thread=0 tot-net-in=48 tot-net-out=390 tot-cmds=1\n"; + var line2 = "id=87 addr=172.17.0.1:60630 laddr=172.17.0.2:3000 fd=23 name= age=4 idle=0 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=26 qbuf-free=20448 argv-mem=10 multi-mem=0 rbs=1024 rbp=7 obl=0 oll=0 omem=0 tot-mem=22810 events=r cmd=client|list user=default redir=-1 resp=2 lib-name= lib-ver= io-thread=0 tot-net-in=40 tot-net-out=7 tot-cmds=1\n"; + var content = line1 + line2; + var resp = $"${content.Length}\r\n{content}\r\n"; + + var result = Execute(resp, StackExchange.Redis.ClientInfo.Processor); + + Assert.NotNull(result); + Assert.Equal(2, result.Length); + Assert.Equal(86, result[0].Id); + Assert.Equal(87, result[1].Id); + Assert.Equal("172.17.0.1:40750", result[0].Address?.ToString()); + Assert.Equal("172.17.0.1:60630", result[1].Address?.ToString()); + } + + [Fact] + public void EmptyString_Success() + { + // Empty bulk string + var resp = "$0\r\n\r\n"; + + var result = Execute(resp, StackExchange.Redis.ClientInfo.Processor); + + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void NullBulkString_Failure() + { + // Null bulk string should fail + var resp = "$-1\r\n"; + + ExecuteUnexpected(resp, StackExchange.Redis.ClientInfo.Processor); + } + + [Fact] + public void NotBulkString_Failure() + { + // Simple string should fail + var resp = "+OK\r\n"; + + ExecuteUnexpected(resp, StackExchange.Redis.ClientInfo.Processor); + } + + [Fact] + public void VerbatimString_Success() + { + // Verbatim string with TXT encoding - should behave identically to bulk string + // Format: ={len}\r\n{enc}:{payload}\r\n where enc is exactly 3 bytes (e.g., "TXT") + var content = "id=86 addr=172.17.0.1:40750 laddr=172.17.0.2:3000 fd=22 name= age=7 idle=0 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=26 qbuf-free=20448 argv-mem=10 multi-mem=0 rbs=1024 rbp=0 obl=0 oll=0 omem=0 tot-mem=22810 events=r cmd=client|list user=default redir=-1 resp=2 lib-name= lib-ver= io-thread=0 tot-net-in=48 tot-net-out=36 tot-cmds=0\n"; + var totalLen = 4 + content.Length; // "TXT:" + content + var resp = $"={totalLen}\r\nTXT:{content}\r\n"; + + var result = Execute(resp, StackExchange.Redis.ClientInfo.Processor); + + // ReadString() automatically strips the "TXT:" encoding prefix, + // so the result should be identical to the bulk string test + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal(86, result[0].Id); + Assert.Equal("172.17.0.1:40750", result[0].Address?.ToString()); + Assert.Equal(7, result[0].AgeSeconds); + Assert.Equal(0, result[0].Database); + } +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/ClusterNodes.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/ClusterNodes.cs new file mode 100644 index 000000000..91e089c57 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/ClusterNodes.cs @@ -0,0 +1,12 @@ +using System.Linq; +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class ClusterNodes(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + // NOTE: ClusterNodesProcessor cannot be unit tested in isolation because it requires + // a real PhysicalConnection with a bridge to parse the cluster configuration. + // The processor calls connection.BridgeCouldBeNull which throws ObjectDisposedException + // in the test environment. These tests are covered by integration tests instead. +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/ConditionTests.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/ConditionTests.cs new file mode 100644 index 000000000..fbce6d4d5 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/ConditionTests.cs @@ -0,0 +1,408 @@ +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +/// +/// Unit tests for Condition subclasses using the RespReader path. +/// +public class ConditionTests(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + private static Message CreateConditionMessage(Condition condition, RedisCommand command, RedisKey key, params RedisValue[] values) + { + return values.Length switch + { + 0 => Condition.ConditionProcessor.CreateMessage(condition, 0, CommandFlags.None, command, key), + 1 => Condition.ConditionProcessor.CreateMessage(condition, 0, CommandFlags.None, command, key, values[0]), + 2 => Condition.ConditionProcessor.CreateMessage(condition, 0, CommandFlags.None, command, key, values[0], values[1]), + 5 => Condition.ConditionProcessor.CreateMessage(condition, 0, CommandFlags.None, command, key, values[0], values[1], values[2], values[3], values[4]), + _ => throw new System.NotSupportedException($"Unsupported value count: {values.Length}"), + }; + } + + [Fact] + public void ExistsCondition_KeyExists_True() + { + var condition = Condition.KeyExists("mykey"); + var message = CreateConditionMessage(condition, RedisCommand.EXISTS, "mykey"); + var result = Execute(":1\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void ExistsCondition_KeyExists_False() + { + var condition = Condition.KeyExists("mykey"); + var message = CreateConditionMessage(condition, RedisCommand.EXISTS, "mykey"); + var result = Execute(":0\r\n", Condition.ConditionProcessor.Default, message); + Assert.False(result); + } + + [Fact] + public void ExistsCondition_KeyNotExists_True() + { + var condition = Condition.KeyNotExists("mykey"); + var message = CreateConditionMessage(condition, RedisCommand.EXISTS, "mykey"); + var result = Execute(":0\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void ExistsCondition_KeyNotExists_False() + { + var condition = Condition.KeyNotExists("mykey"); + var message = CreateConditionMessage(condition, RedisCommand.EXISTS, "mykey"); + var result = Execute(":1\r\n", Condition.ConditionProcessor.Default, message); + Assert.False(result); + } + + [Fact] + public void ExistsCondition_HashExists_True() + { + var condition = Condition.HashExists("myhash", "field1"); + var message = CreateConditionMessage(condition, RedisCommand.HEXISTS, "myhash", "field1"); + var result = Execute(":1\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void ExistsCondition_HashNotExists_True() + { + var condition = Condition.HashNotExists("myhash", "field1"); + var message = CreateConditionMessage(condition, RedisCommand.HEXISTS, "myhash", "field1"); + var result = Execute(":0\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void ExistsCondition_SetContains_True() + { + var condition = Condition.SetContains("myset", "member1"); + var message = CreateConditionMessage(condition, RedisCommand.SISMEMBER, "myset", "member1"); + var result = Execute(":1\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void ExistsCondition_SetNotContains_True() + { + var condition = Condition.SetNotContains("myset", "member1"); + var message = CreateConditionMessage(condition, RedisCommand.SISMEMBER, "myset", "member1"); + var result = Execute(":0\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void ExistsCondition_SortedSetContains_True() + { + var condition = Condition.SortedSetContains("myzset", "member1"); + var message = CreateConditionMessage(condition, RedisCommand.ZSCORE, "myzset", "member1"); + var result = Execute("$1\r\n5\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void ExistsCondition_SortedSetContains_Null_False() + { + var condition = Condition.SortedSetContains("myzset", "member1"); + var message = CreateConditionMessage(condition, RedisCommand.ZSCORE, "myzset", "member1"); + var result = Execute("$-1\r\n", Condition.ConditionProcessor.Default, message); + Assert.False(result); + } + + [Fact] + public void ExistsCondition_SortedSetNotContains_True() + { + var condition = Condition.SortedSetNotContains("myzset", "member1"); + var message = CreateConditionMessage(condition, RedisCommand.ZSCORE, "myzset", "member1"); + var result = Execute("$-1\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void StartsWithCondition_Match_True() + { + var condition = Condition.SortedSetContainsStarting("myzset", "pre"); + var message = CreateConditionMessage(condition, RedisCommand.ZRANGEBYLEX, "myzset", "[pre", "+", "LIMIT", 0, 1); + var result = Execute("*1\r\n$6\r\nprefix\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void StartsWithCondition_NoMatch_False() + { + var condition = Condition.SortedSetContainsStarting("myzset", "pre"); + var message = CreateConditionMessage(condition, RedisCommand.ZRANGEBYLEX, "myzset", "[pre", "+", "LIMIT", 0, 1); + var result = Execute("*0\r\n", Condition.ConditionProcessor.Default, message); + Assert.False(result); + } + + [Fact] + public void StartsWithCondition_NotContainsStarting_True() + { + var condition = Condition.SortedSetNotContainsStarting("myzset", "pre"); + var message = CreateConditionMessage(condition, RedisCommand.ZRANGEBYLEX, "myzset", "[pre", "+", "LIMIT", 0, 1); + var result = Execute("*0\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void EqualsCondition_StringEqual_True() + { + var condition = Condition.StringEqual("mykey", "value1"); + var message = CreateConditionMessage(condition, RedisCommand.GET, "mykey", RedisValue.Null); + var result = Execute("$6\r\nvalue1\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void EqualsCondition_StringEqual_False() + { + var condition = Condition.StringEqual("mykey", "value1"); + var message = CreateConditionMessage(condition, RedisCommand.GET, "mykey", RedisValue.Null); + var result = Execute("$6\r\nvalue2\r\n", Condition.ConditionProcessor.Default, message); + Assert.False(result); + } + + [Fact] + public void EqualsCondition_StringNotEqual_True() + { + var condition = Condition.StringNotEqual("mykey", "value1"); + var message = CreateConditionMessage(condition, RedisCommand.GET, "mykey", RedisValue.Null); + var result = Execute("$6\r\nvalue2\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void EqualsCondition_HashEqual_True() + { + var condition = Condition.HashEqual("myhash", "field1", "value1"); + var message = CreateConditionMessage(condition, RedisCommand.HGET, "myhash", "field1"); + var result = Execute("$6\r\nvalue1\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void EqualsCondition_HashNotEqual_True() + { + var condition = Condition.HashNotEqual("myhash", "field1", "value1"); + var message = CreateConditionMessage(condition, RedisCommand.HGET, "myhash", "field1"); + var result = Execute("$6\r\nvalue2\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void EqualsCondition_SortedSetEqual_True() + { + var condition = Condition.SortedSetEqual("myzset", "member1", 5.0); + var message = CreateConditionMessage(condition, RedisCommand.ZSCORE, "myzset", "member1"); + var result = Execute("$1\r\n5\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void EqualsCondition_SortedSetEqual_False() + { + var condition = Condition.SortedSetEqual("myzset", "member1", 5.0); + var message = CreateConditionMessage(condition, RedisCommand.ZSCORE, "myzset", "member1"); + var result = Execute("$1\r\n3\r\n", Condition.ConditionProcessor.Default, message); + Assert.False(result); + } + + [Fact] + public void EqualsCondition_SortedSetNotEqual_True() + { + var condition = Condition.SortedSetNotEqual("myzset", "member1", 5.0); + var message = CreateConditionMessage(condition, RedisCommand.ZSCORE, "myzset", "member1"); + var result = Execute("$1\r\n3\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void ListCondition_IndexEqual_True() + { + var condition = Condition.ListIndexEqual("mylist", 0, "value1"); + var message = CreateConditionMessage(condition, RedisCommand.LINDEX, "mylist", 0); + var result = Execute("$6\r\nvalue1\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void ListCondition_IndexEqual_False() + { + var condition = Condition.ListIndexEqual("mylist", 0, "value1"); + var message = CreateConditionMessage(condition, RedisCommand.LINDEX, "mylist", 0); + var result = Execute("$6\r\nvalue2\r\n", Condition.ConditionProcessor.Default, message); + Assert.False(result); + } + + [Fact] + public void ListCondition_IndexNotEqual_True() + { + var condition = Condition.ListIndexNotEqual("mylist", 0, "value1"); + var message = CreateConditionMessage(condition, RedisCommand.LINDEX, "mylist", 0); + var result = Execute("$6\r\nvalue2\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void ListCondition_IndexExists_True() + { + var condition = Condition.ListIndexExists("mylist", 0); + var message = CreateConditionMessage(condition, RedisCommand.LINDEX, "mylist", 0); + var result = Execute("$6\r\nvalue1\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void ListCondition_IndexExists_Null_False() + { + var condition = Condition.ListIndexExists("mylist", 0); + var message = CreateConditionMessage(condition, RedisCommand.LINDEX, "mylist", 0); + var result = Execute("$-1\r\n", Condition.ConditionProcessor.Default, message); + Assert.False(result); + } + + [Fact] + public void ListCondition_IndexNotExists_True() + { + var condition = Condition.ListIndexNotExists("mylist", 0); + var message = CreateConditionMessage(condition, RedisCommand.LINDEX, "mylist", 0); + var result = Execute("$-1\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void LengthCondition_StringLengthEqual_True() + { + var condition = Condition.StringLengthEqual("mykey", 10); + var message = CreateConditionMessage(condition, RedisCommand.STRLEN, "mykey"); + var result = Execute(":10\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void LengthCondition_StringLengthEqual_False() + { + var condition = Condition.StringLengthEqual("mykey", 10); + var message = CreateConditionMessage(condition, RedisCommand.STRLEN, "mykey"); + var result = Execute(":5\r\n", Condition.ConditionProcessor.Default, message); + Assert.False(result); + } + + [Fact] + public void LengthCondition_StringLengthLessThan_True() + { + var condition = Condition.StringLengthLessThan("mykey", 10); + var message = CreateConditionMessage(condition, RedisCommand.STRLEN, "mykey"); + var result = Execute(":5\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void LengthCondition_StringLengthGreaterThan_True() + { + var condition = Condition.StringLengthGreaterThan("mykey", 10); + var message = CreateConditionMessage(condition, RedisCommand.STRLEN, "mykey"); + var result = Execute(":15\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void LengthCondition_HashLengthEqual_True() + { + var condition = Condition.HashLengthEqual("myhash", 5); + var message = CreateConditionMessage(condition, RedisCommand.HLEN, "myhash"); + var result = Execute(":5\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void LengthCondition_ListLengthEqual_True() + { + var condition = Condition.ListLengthEqual("mylist", 3); + var message = CreateConditionMessage(condition, RedisCommand.LLEN, "mylist"); + var result = Execute(":3\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void LengthCondition_SetLengthEqual_True() + { + var condition = Condition.SetLengthEqual("myset", 7); + var message = CreateConditionMessage(condition, RedisCommand.SCARD, "myset"); + var result = Execute(":7\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void LengthCondition_SortedSetLengthEqual_True() + { + var condition = Condition.SortedSetLengthEqual("myzset", 4); + var message = CreateConditionMessage(condition, RedisCommand.ZCARD, "myzset"); + var result = Execute(":4\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void LengthCondition_StreamLengthEqual_True() + { + var condition = Condition.StreamLengthEqual("mystream", 10); + var message = CreateConditionMessage(condition, RedisCommand.XLEN, "mystream"); + var result = Execute(":10\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void SortedSetRangeLengthCondition_Equal_True() + { + var condition = Condition.SortedSetLengthEqual("myzset", 5, 0, 10); + var message = CreateConditionMessage(condition, RedisCommand.ZCOUNT, "myzset", 0, 10); + var result = Execute(":5\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void SortedSetRangeLengthCondition_LessThan_True() + { + var condition = Condition.SortedSetLengthLessThan("myzset", 10, 0, 100); + var message = CreateConditionMessage(condition, RedisCommand.ZCOUNT, "myzset", 0, 100); + var result = Execute(":5\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void SortedSetRangeLengthCondition_GreaterThan_True() + { + var condition = Condition.SortedSetLengthGreaterThan("myzset", 3, 0, 100); + var message = CreateConditionMessage(condition, RedisCommand.ZCOUNT, "myzset", 0, 100); + var result = Execute(":10\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void SortedSetScoreCondition_ScoreExists_True() + { + var condition = Condition.SortedSetScoreExists("myzset", 5.0); + var message = CreateConditionMessage(condition, RedisCommand.ZCOUNT, "myzset", 5.0, 5.0); + var result = Execute(":3\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } + + [Fact] + public void SortedSetScoreCondition_ScoreExists_False() + { + var condition = Condition.SortedSetScoreExists("myzset", 5.0); + var message = CreateConditionMessage(condition, RedisCommand.ZCOUNT, "myzset", 5.0, 5.0); + var result = Execute(":0\r\n", Condition.ConditionProcessor.Default, message); + Assert.False(result); + } + + [Fact] + public void SortedSetScoreCondition_ScoreNotExists_True() + { + var condition = Condition.SortedSetScoreNotExists("myzset", 5.0); + var message = CreateConditionMessage(condition, RedisCommand.ZCOUNT, "myzset", 5.0, 5.0); + var result = Execute(":0\r\n", Condition.ConditionProcessor.Default, message); + Assert.True(result); + } +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/DemandZeroOrOne.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/DemandZeroOrOne.cs new file mode 100644 index 000000000..e5ce63ce6 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/DemandZeroOrOne.cs @@ -0,0 +1,29 @@ +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class DemandZeroOrOne(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + [Theory] + [InlineData(":0\r\n", false)] + [InlineData(":1\r\n", true)] + [InlineData("+0\r\n", false)] + [InlineData("+1\r\n", true)] + [InlineData("$1\r\n0\r\n", false)] + [InlineData("$1\r\n1\r\n", true)] + public void ValidZeroOrOne_Success(string resp, bool expected) + { + var result = Execute(resp, ResultProcessor.DemandZeroOrOne); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(":2\r\n")] + [InlineData("+OK\r\n")] + [InlineData("*1\r\n:1\r\n")] + [InlineData("$-1\r\n")] + public void InvalidResponse_Failure(string resp) + { + ExecuteUnexpected(resp, ResultProcessor.DemandZeroOrOne); + } +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/ExpectBasicString.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/ExpectBasicString.cs new file mode 100644 index 000000000..a6fc80a42 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/ExpectBasicString.cs @@ -0,0 +1,58 @@ +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class ExpectBasicString(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + [Theory] + [InlineData("+OK\r\n", true)] + [InlineData("$2\r\nOK\r\n", true)] + public void DemandOK_Success(string resp, bool expected) => Assert.Equal(expected, Execute(resp, ResultProcessor.DemandOK)); + + [Theory] + [InlineData("+PONG\r\n", true)] + [InlineData("$4\r\nPONG\r\n", true)] + public void DemandPONG_Success(string resp, bool expected) => Assert.Equal(expected, Execute(resp, ResultProcessor.DemandPONG)); + + [Theory] + [InlineData("+FAIL\r\n")] + [InlineData("$4\r\nFAIL\r\n")] + [InlineData(":1\r\n")] + public void DemandOK_Failure(string resp) => Assert.False(TryExecute(resp, ResultProcessor.DemandOK, out _, out _)); + + [Theory] + [InlineData("+FAIL\r\n")] + [InlineData("$4\r\nFAIL\r\n")] + [InlineData(":1\r\n")] + public void DemandPONG_Failure(string resp) => Assert.False(TryExecute(resp, ResultProcessor.DemandPONG, out _, out _)); + + [Theory] + [InlineData("+Background saving started\r\n", true)] + [InlineData("$25\r\nBackground saving started\r\n", true)] + [InlineData("+Background saving started by parent\r\n", true)] + public void BackgroundSaveStarted_Success(string resp, bool expected) => Assert.Equal(expected, Execute(resp, ResultProcessor.BackgroundSaveStarted)); + + [Theory] + [InlineData("+Background append only file rewriting started\r\n", true)] + [InlineData("$45\r\nBackground append only file rewriting started\r\n", true)] + public void BackgroundSaveAOFStarted_Success(string resp, bool expected) => Assert.Equal(expected, Execute(resp, ResultProcessor.BackgroundSaveAOFStarted)); + + // Case sensitivity tests - these demonstrate that the new implementation is case-sensitive + // The old CommandBytes implementation was case-insensitive (stored uppercase) + [Theory] + [InlineData("+ok\r\n")] // lowercase + [InlineData("+Ok\r\n")] // mixed case + [InlineData("$2\r\nok\r\n")] // lowercase bulk string + public void DemandOK_CaseSensitive_Failure(string resp) => Assert.False(TryExecute(resp, ResultProcessor.DemandOK, out _, out _)); + + [Theory] + [InlineData("+pong\r\n")] // lowercase + [InlineData("+Pong\r\n")] // mixed case + [InlineData("$4\r\npong\r\n")] // lowercase bulk string + public void DemandPONG_CaseSensitive_Failure(string resp) => Assert.False(TryExecute(resp, ResultProcessor.DemandPONG, out _, out _)); + + [Theory] + [InlineData("+background saving started\r\n")] // lowercase + [InlineData("+BACKGROUND SAVING STARTED\r\n")] // uppercase + public void BackgroundSaveStarted_CaseSensitive_Failure(string resp) => Assert.False(TryExecute(resp, ResultProcessor.BackgroundSaveStarted, out _, out _)); +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/Info.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/Info.cs new file mode 100644 index 000000000..82c01c458 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/Info.cs @@ -0,0 +1,80 @@ +using System.Linq; +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class Info(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + [Fact] + public void SingleSection_Success() + { + var resp = "$651\r\n# Server\r\nredis_version:8.6.0\r\nredis_git_sha1:00000000\r\nredis_git_dirty:1\r\nredis_build_id:a7d515010e105f80\r\nredis_mode:standalone\r\nos:Linux 6.17.0-14-generic x86_64\r\narch_bits:64\r\nmonotonic_clock:POSIX clock_gettime\r\nmultiplexing_api:epoll\r\natomicvar_api:c11-builtin\r\ngcc_version:14.2.0\r\nprocess_id:16\r\nprocess_supervised:no\r\nrun_id:b5ab1b382ec845e0a6989e550f36c187fdef3bc0\r\ntcp_port:3000\r\nserver_time_usec:1771514460547930\r\nuptime_in_seconds:13\r\nuptime_in_days:0\r\nhz:10\r\nconfigured_hz:10\r\nlru_clock:9906780\r\nexecutable:/data/redis-server\r\nconfig_file:/redis/work/node-0/redis.conf\r\nio_threads_active:0\r\nlistener0:name=tcp,bind=*,bind=-::*,port=3000\r\n\r\n"; + + var result = Execute(resp, ResultProcessor.Info); + + Assert.NotNull(result); + Assert.Single(result); + + var serverSection = result.Single(g => g.Key == "Server"); + Assert.Equal(25, serverSection.Count()); + + var versionPair = serverSection.First(kv => kv.Key == "redis_version"); + Assert.Equal("8.6.0", versionPair.Value); + + var portPair = serverSection.First(kv => kv.Key == "tcp_port"); + Assert.Equal("3000", portPair.Value); + } + + [Fact] + public void MultipleSection_Success() + { + var resp = "$978\r\n# Server\r\nredis_version:8.6.0\r\nredis_git_sha1:00000000\r\nredis_git_dirty:1\r\nredis_build_id:a7d515010e105f80\r\nredis_mode:standalone\r\nos:Linux 6.17.0-14-generic x86_64\r\narch_bits:64\r\nmonotonic_clock:POSIX clock_gettime\r\nmultiplexing_api:epoll\r\natomicvar_api:c11-builtin\r\ngcc_version:14.2.0\r\nprocess_id:16\r\nprocess_supervised:no\r\nrun_id:b5ab1b382ec845e0a6989e550f36c187fdef3bc0\r\ntcp_port:3000\r\nserver_time_usec:1771514577242937\r\nuptime_in_seconds:130\r\nuptime_in_days:0\r\nhz:10\r\nconfigured_hz:10\r\nlru_clock:9906897\r\nexecutable:/data/redis-server\r\nconfig_file:/redis/work/node-0/redis.conf\r\nio_threads_active:0\r\nlistener0:name=tcp,bind=*,bind=-::*,port=3000\r\n\r\n# Clients\r\nconnected_clients:1\r\ncluster_connections:0\r\nmaxclients:10000\r\nclient_recent_max_input_buffer:0\r\nclient_recent_max_output_buffer:0\r\nblocked_clients:0\r\ntracking_clients:0\r\npubsub_clients:0\r\nwatching_clients:0\r\nclients_in_timeout_table:0\r\ntotal_watched_keys:0\r\ntotal_blocking_keys:0\r\ntotal_blocking_keys_on_nokey:0\r\n\r\n"; + + var result = Execute(resp, ResultProcessor.Info); + + Assert.NotNull(result); + Assert.Equal(2, result.Length); + + var serverSection = result.Single(g => g.Key == "Server"); + Assert.Equal(25, serverSection.Count()); + + var clientsSection = result.Single(g => g.Key == "Clients"); + Assert.Equal(13, clientsSection.Count()); + + var connectedClients = clientsSection.First(kv => kv.Key == "connected_clients"); + Assert.Equal("1", connectedClients.Value); + } + + [Fact] + public void EmptyString_Success() + { + var resp = "$0\r\n\r\n"; + var result = Execute(resp, ResultProcessor.Info); + + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void NullBulkString_Success() + { + var resp = "$-1\r\n"; + var result = Execute(resp, ResultProcessor.Info); + + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void NoSectionHeader_UsesDefaultCategory() + { + var resp = "$26\r\nkey1:value1\r\nkey2:value2\r\n\r\n"; + var result = Execute(resp, ResultProcessor.Info); + + Assert.NotNull(result); + Assert.Single(result); + + var miscSection = result.Single(g => g.Key == "miscellaneous"); + Assert.Equal(2, miscSection.Count()); + } +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/LongestCommonSubsequence.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/LongestCommonSubsequence.cs new file mode 100644 index 000000000..46e7d15aa --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/LongestCommonSubsequence.cs @@ -0,0 +1,105 @@ +using System.Linq; +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class LongestCommonSubsequence(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + [Fact] + public void SingleMatch_Success() + { + // LCS key1 key2 IDX MINMATCHLEN 4 WITHMATCHLEN + // 1) "matches" + // 2) 1) 1) 1) (integer) 4 + // 2) (integer) 7 + // 2) 1) (integer) 5 + // 2) (integer) 8 + // 3) (integer) 4 + // 3) "len" + // 4) (integer) 6 + var resp = "*4\r\n$7\r\nmatches\r\n*1\r\n*3\r\n*2\r\n:4\r\n:7\r\n*2\r\n:5\r\n:8\r\n:4\r\n$3\r\nlen\r\n:6\r\n"; + var result = Execute(resp, ResultProcessor.LCSMatchResult); + + Assert.Equal(6, result.LongestMatchLength); + Assert.Single(result.Matches); + + // Verify backward-compatible properties + Assert.Equal(4, result.Matches[0].FirstStringIndex); + Assert.Equal(5, result.Matches[0].SecondStringIndex); + Assert.Equal(4, result.Matches[0].Length); + + // Verify new Position properties + Assert.Equal(4, result.Matches[0].First.Start); + Assert.Equal(7, result.Matches[0].First.End); + Assert.Equal(5, result.Matches[0].Second.Start); + Assert.Equal(8, result.Matches[0].Second.End); + } + + [Fact] + public void TwoMatches_Success() + { + // LCS key1 key2 IDX MINMATCHLEN 0 WITHMATCHLEN + // 1) "matches" + // 2) 1) 1) 1) (integer) 4 + // 2) (integer) 7 + // 2) 1) (integer) 5 + // 2) (integer) 8 + // 3) (integer) 4 + // 2) 1) 1) (integer) 2 + // 2) (integer) 3 + // 2) 1) (integer) 0 + // 2) (integer) 1 + // 3) (integer) 2 + // 3) "len" + // 4) (integer) 6 + var resp = "*4\r\n$7\r\nmatches\r\n*2\r\n*3\r\n*2\r\n:4\r\n:7\r\n*2\r\n:5\r\n:8\r\n:4\r\n*3\r\n*2\r\n:2\r\n:3\r\n*2\r\n:0\r\n:1\r\n:2\r\n$3\r\nlen\r\n:6\r\n"; + var result = Execute(resp, ResultProcessor.LCSMatchResult); + + Assert.Equal(6, result.LongestMatchLength); + Assert.Equal(2, result.Matches.Length); + + // First match - verify backward-compatible properties + Assert.Equal(4, result.Matches[0].FirstStringIndex); + Assert.Equal(5, result.Matches[0].SecondStringIndex); + Assert.Equal(4, result.Matches[0].Length); + + // First match - verify new Position properties + Assert.Equal(4, result.Matches[0].First.Start); + Assert.Equal(7, result.Matches[0].First.End); + Assert.Equal(5, result.Matches[0].Second.Start); + Assert.Equal(8, result.Matches[0].Second.End); + + // Second match - verify backward-compatible properties + Assert.Equal(2, result.Matches[1].FirstStringIndex); + Assert.Equal(0, result.Matches[1].SecondStringIndex); + Assert.Equal(2, result.Matches[1].Length); + + // Second match - verify new Position properties + Assert.Equal(2, result.Matches[1].First.Start); + Assert.Equal(3, result.Matches[1].First.End); + Assert.Equal(0, result.Matches[1].Second.Start); + Assert.Equal(1, result.Matches[1].Second.End); + } + + [Fact] + public void NoMatches_Success() + { + // LCS key1 key2 IDX + // 1) "matches" + // 2) (empty array) + // 3) "len" + // 4) (integer) 0 + var resp = "*4\r\n$7\r\nmatches\r\n*0\r\n$3\r\nlen\r\n:0\r\n"; + var result = Execute(resp, ResultProcessor.LCSMatchResult); + + Assert.Equal(0, result.LongestMatchLength); + Assert.Empty(result.Matches); + } + + [Fact] + public void NotArray_Failure() + { + var resp = "+OK\r\n"; + ExecuteUnexpected(resp, ResultProcessor.LCSMatchResult); + } +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/RedisValueFromArray.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/RedisValueFromArray.cs new file mode 100644 index 000000000..7966a9a7c --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/RedisValueFromArray.cs @@ -0,0 +1,34 @@ +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class RedisValueFromArray(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + [Fact] + public void SingleElementArray_String() + { + var result = Execute("*1\r\n$5\r\nhello\r\n", ResultProcessor.RedisValueFromArray); + Assert.Equal("hello", (string?)result); + } + + [Fact] + public void SingleElementArray_Integer() + { + var result = Execute("*1\r\n:42\r\n", ResultProcessor.RedisValueFromArray); + Assert.Equal(42, (long)result); + } + + [Fact] + public void SingleElementArray_Null() + { + var result = Execute("*1\r\n$-1\r\n", ResultProcessor.RedisValueFromArray); + Assert.True(result.IsNull); + } + + [Fact] + public void SingleElementArray_EmptyString() + { + var result = Execute("*1\r\n$0\r\n\r\n", ResultProcessor.RedisValueFromArray); + Assert.Equal("", (string?)result); + } +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/SentinelGetPrimaryAddressByName.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/SentinelGetPrimaryAddressByName.cs new file mode 100644 index 000000000..341e0c1ed --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/SentinelGetPrimaryAddressByName.cs @@ -0,0 +1,84 @@ +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class SentinelGetPrimaryAddressByName(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + [Fact] + public void ValidHostAndPort_Success() + { + // Array with 2 elements: host (bulk string) and port (integer) + var resp = "*2\r\n$9\r\n127.0.0.1\r\n:6379\r\n"; + var result = Execute(resp, ResultProcessor.SentinelPrimaryEndpoint); + + Assert.NotNull(result); + var ipEndpoint = Assert.IsType(result); + Assert.Equal("127.0.0.1", ipEndpoint.Address.ToString()); + Assert.Equal(6379, ipEndpoint.Port); + } + + [Fact] + public void DomainNameAndPort_Success() + { + // Array with 2 elements: domain name (bulk string) and port (integer) + var resp = "*2\r\n$17\r\nredis.example.com\r\n:6380\r\n"; + var result = Execute(resp, ResultProcessor.SentinelPrimaryEndpoint); + + Assert.NotNull(result); + var dnsEndpoint = Assert.IsType(result); + Assert.Equal("redis.example.com", dnsEndpoint.Host); + Assert.Equal(6380, dnsEndpoint.Port); + } + + [Fact] + public void NullArray_Success() + { + // Null array - primary doesn't exist + var resp = "*-1\r\n"; + var result = Execute(resp, ResultProcessor.SentinelPrimaryEndpoint); + + Assert.Null(result); + } + + [Fact] + public void EmptyArray_Success() + { + // Empty array - primary doesn't exist + var resp = "*0\r\n"; + var result = Execute(resp, ResultProcessor.SentinelPrimaryEndpoint); + + Assert.Null(result); + } + + [Fact] + public void NotArray_Failure() + { + // Simple string instead of array + var resp = "+OK\r\n"; + ExecuteUnexpected(resp, ResultProcessor.SentinelPrimaryEndpoint); + } + + [Fact] + public void ArrayWithOneElement_Failure() + { + // Array with only 1 element (missing port) + var resp = "*1\r\n$9\r\n127.0.0.1\r\n"; + ExecuteUnexpected(resp, ResultProcessor.SentinelPrimaryEndpoint); + } + + [Fact] + public void ArrayWithThreeElements_Failure() + { + // Array with 3 elements (too many) + var resp = "*3\r\n$9\r\n127.0.0.1\r\n:6379\r\n$5\r\nextra\r\n"; + ExecuteUnexpected(resp, ResultProcessor.SentinelPrimaryEndpoint); + } + + [Fact] + public void ArrayWithNonIntegerPort_Failure() + { + // Array with 2 elements but port is not an integer + var resp = "*2\r\n$9\r\n127.0.0.1\r\n$4\r\nport\r\n"; + ExecuteUnexpected(resp, ResultProcessor.SentinelPrimaryEndpoint); + } +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/SentinelGetReplicaAddresses.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/SentinelGetReplicaAddresses.cs new file mode 100644 index 000000000..172c79322 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/SentinelGetReplicaAddresses.cs @@ -0,0 +1,88 @@ +using System.Net; +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class SentinelGetReplicaAddresses(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + [Fact] + public void SingleReplica_Success() + { + var resp = "*1\r\n*4\r\n$2\r\nip\r\n$9\r\n127.0.0.1\r\n$4\r\nport\r\n$4\r\n6380\r\n"; + var result = Execute(resp, ResultProcessor.SentinelReplicaEndPoints); + + Assert.NotNull(result); + Assert.Single(result); + + var endpoint = Assert.IsType(result[0]); + Assert.Equal("127.0.0.1", endpoint.Address.ToString()); + Assert.Equal(6380, endpoint.Port); + } + + [Fact] + public void MultipleReplicas_Success() + { + var resp = "*2\r\n*4\r\n$2\r\nip\r\n$9\r\n127.0.0.1\r\n$4\r\nport\r\n$4\r\n6380\r\n*4\r\n$2\r\nip\r\n$9\r\n127.0.0.2\r\n$4\r\nport\r\n$4\r\n6381\r\n"; + var result = Execute(resp, ResultProcessor.SentinelReplicaEndPoints); + + Assert.NotNull(result); + Assert.Equal(2, result.Length); + + var endpoint1 = Assert.IsType(result[0]); + Assert.Equal("127.0.0.1", endpoint1.Address.ToString()); + Assert.Equal(6380, endpoint1.Port); + + var endpoint2 = Assert.IsType(result[1]); + Assert.Equal("127.0.0.2", endpoint2.Address.ToString()); + Assert.Equal(6381, endpoint2.Port); + } + + [Fact] + public void DnsEndpoint_Success() + { + var resp = "*1\r\n*4\r\n$2\r\nip\r\n$17\r\nredis.example.com\r\n$4\r\nport\r\n$4\r\n6380\r\n"; + var result = Execute(resp, ResultProcessor.SentinelReplicaEndPoints); + + Assert.NotNull(result); + Assert.Single(result); + + var endpoint = Assert.IsType(result[0]); + Assert.Equal("redis.example.com", endpoint.Host); + Assert.Equal(6380, endpoint.Port); + } + + [Fact] + public void ReversedOrder_Success() + { + // Test that order doesn't matter - port before ip + var resp = "*1\r\n*4\r\n$4\r\nport\r\n$4\r\n6380\r\n$2\r\nip\r\n$9\r\n127.0.0.1\r\n"; + var result = Execute(resp, ResultProcessor.SentinelReplicaEndPoints); + + Assert.NotNull(result); + Assert.Single(result); + var endpoint = Assert.IsType(result[0]); + Assert.Equal("127.0.0.1", endpoint.Address.ToString()); + Assert.Equal(6380, endpoint.Port); + } + + [Fact] + public void EmptyArray_Failure() + { + var resp = "*0\r\n"; + ExecuteUnexpected(resp, ResultProcessor.SentinelReplicaEndPoints); + } + + [Fact] + public void NullArray_Failure() + { + var resp = "*-1\r\n"; + ExecuteUnexpected(resp, ResultProcessor.SentinelReplicaEndPoints); + } + + [Fact] + public void NotArray_Failure() + { + var resp = "+OK\r\n"; + ExecuteUnexpected(resp, ResultProcessor.SentinelReplicaEndPoints); + } +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/SentinelGetSentinelAddresses.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/SentinelGetSentinelAddresses.cs new file mode 100644 index 000000000..98360d13a --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/SentinelGetSentinelAddresses.cs @@ -0,0 +1,90 @@ +using System.Net; +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class SentinelGetSentinelAddresses(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + [Fact] + public void SingleSentinel_Success() + { + var resp = "*1\r\n*4\r\n$2\r\nip\r\n$9\r\n127.0.0.1\r\n$4\r\nport\r\n$5\r\n26379\r\n"; + var result = Execute(resp, ResultProcessor.SentinelAddressesEndPoints); + + Assert.NotNull(result); + Assert.Single(result); + var endpoint = Assert.IsType(result[0]); + Assert.Equal("127.0.0.1", endpoint.Address.ToString()); + Assert.Equal(26379, endpoint.Port); + } + + [Fact] + public void MultipleSentinels_Success() + { + var resp = "*2\r\n*4\r\n$2\r\nip\r\n$9\r\n127.0.0.1\r\n$4\r\nport\r\n$5\r\n26379\r\n*4\r\n$2\r\nip\r\n$9\r\n127.0.0.2\r\n$4\r\nport\r\n$5\r\n26380\r\n"; + var result = Execute(resp, ResultProcessor.SentinelAddressesEndPoints); + + Assert.NotNull(result); + Assert.Equal(2, result.Length); + + var endpoint1 = Assert.IsType(result[0]); + Assert.Equal("127.0.0.1", endpoint1.Address.ToString()); + Assert.Equal(26379, endpoint1.Port); + + var endpoint2 = Assert.IsType(result[1]); + Assert.Equal("127.0.0.2", endpoint2.Address.ToString()); + Assert.Equal(26380, endpoint2.Port); + } + + [Fact] + public void DnsEndpoint_Success() + { + var resp = "*1\r\n*4\r\n$2\r\nip\r\n$20\r\nsentinel.example.com\r\n$4\r\nport\r\n$5\r\n26379\r\n"; + var result = Execute(resp, ResultProcessor.SentinelAddressesEndPoints); + + Assert.NotNull(result); + Assert.Single(result); + var endpoint = Assert.IsType(result[0]); + Assert.Equal("sentinel.example.com", endpoint.Host); + Assert.Equal(26379, endpoint.Port); + } + + [Fact] + public void ReversedOrder_Success() + { + var resp = "*1\r\n*4\r\n$4\r\nport\r\n$5\r\n26379\r\n$2\r\nip\r\n$9\r\n127.0.0.1\r\n"; + var result = Execute(resp, ResultProcessor.SentinelAddressesEndPoints); + + Assert.NotNull(result); + Assert.Single(result); + var endpoint = Assert.IsType(result[0]); + Assert.Equal("127.0.0.1", endpoint.Address.ToString()); + Assert.Equal(26379, endpoint.Port); + } + + [Fact] + public void EmptyArray_Success() + { + var resp = "*0\r\n"; + var result = Execute(resp, ResultProcessor.SentinelAddressesEndPoints); + + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void NullBulkString_Failure() + { + var resp = "$-1\r\n"; + var success = TryExecute(resp, ResultProcessor.SentinelAddressesEndPoints, out var result, out var exception); + + Assert.False(success); + } + + [Fact] + public void NotArray_Failure() + { + var resp = "+OK\r\n"; + ExecuteUnexpected(resp, ResultProcessor.SentinelAddressesEndPoints); + } +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/StreamAutoClaim.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/StreamAutoClaim.cs new file mode 100644 index 000000000..7b80f4ad8 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/StreamAutoClaim.cs @@ -0,0 +1,122 @@ +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class StreamAutoClaim(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + [Fact] + public void WithEntries_ThreeElements_Success() + { + // XAUTOCLAIM mystream mygroup Alice 3600000 0-0 COUNT 25 + // 1) "0-0" + // 2) 1) 1) "1609338752495-0" + // 2) 1) "field" + // 2) "value" + // 3) (empty array) + var resp = "*3\r\n" + + "$3\r\n0-0\r\n" + + "*1\r\n" + // Array of 1 entry + "*2\r\n" + // Entry: [id, fields] + "$15\r\n1609338752495-0\r\n" + + "*2\r\n" + // Fields array + "$5\r\nfield\r\n" + + "$5\r\nvalue\r\n" + + "*0\r\n"; // Empty deleted IDs array + + var result = Execute(resp, ResultProcessor.StreamAutoClaim); + + Assert.Equal("0-0", result.NextStartId.ToString()); + Assert.Single(result.ClaimedEntries); + Assert.Equal("1609338752495-0", result.ClaimedEntries[0].Id.ToString()); + Assert.Equal("value", result.ClaimedEntries[0]["field"]); + Assert.Empty(result.DeletedIds); + } + + [Fact] + public void WithEntries_TwoElements_OlderServer_Success() + { + // Older Redis 6.2 - only returns 2 elements (no deleted IDs) + var resp = "*2\r\n" + + "$3\r\n0-0\r\n" + + "*1\r\n" + + "*2\r\n" + + "$15\r\n1609338752495-0\r\n" + + "*2\r\n" + + "$5\r\nfield\r\n" + + "$5\r\nvalue\r\n"; + + var result = Execute(resp, ResultProcessor.StreamAutoClaim); + + Assert.Equal("0-0", result.NextStartId.ToString()); + Assert.Single(result.ClaimedEntries); + Assert.Empty(result.DeletedIds); + } + + [Fact] + public void EmptyEntries_Success() + { + // No entries claimed + var resp = "*3\r\n" + + "$3\r\n0-0\r\n" + + "*0\r\n" + // Empty entries array + "*0\r\n"; // Empty deleted IDs array + + var result = Execute(resp, ResultProcessor.StreamAutoClaim); + + Assert.Equal("0-0", result.NextStartId.ToString()); + Assert.Empty(result.ClaimedEntries); + Assert.Empty(result.DeletedIds); + } + + [Fact] + public void NullEntries_Success() + { + // Null entries array (alternative representation) + var resp = "*3\r\n" + + "$3\r\n0-0\r\n" + + "$-1\r\n" + // Null entries + "*0\r\n"; + + var result = Execute(resp, ResultProcessor.StreamAutoClaim); + + Assert.Equal("0-0", result.NextStartId.ToString()); + Assert.Empty(result.ClaimedEntries); + Assert.Empty(result.DeletedIds); + } + + [Fact] + public void WithDeletedIds_Success() + { + // Some entries were deleted + var resp = "*3\r\n" + + "$3\r\n0-0\r\n" + + "*0\r\n" + // No claimed entries + "*2\r\n" + // 2 deleted IDs + "$15\r\n1609338752495-0\r\n" + + "$15\r\n1609338752496-0\r\n"; + + var result = Execute(resp, ResultProcessor.StreamAutoClaim); + + Assert.Equal("0-0", result.NextStartId.ToString()); + Assert.Empty(result.ClaimedEntries); + Assert.Equal(2, result.DeletedIds.Length); + Assert.Equal("1609338752495-0", result.DeletedIds[0].ToString()); + Assert.Equal("1609338752496-0", result.DeletedIds[1].ToString()); + } + + [Fact] + public void NotArray_Failure() + { + var resp = "$5\r\nhello\r\n"; + + ExecuteUnexpected(resp, ResultProcessor.StreamAutoClaim); + } + + [Fact] + public void Null_Failure() + { + var resp = "$-1\r\n"; + + ExecuteUnexpected(resp, ResultProcessor.StreamAutoClaim); + } +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/StreamAutoClaimIdsOnly.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/StreamAutoClaimIdsOnly.cs new file mode 100644 index 000000000..355c8aa95 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/StreamAutoClaimIdsOnly.cs @@ -0,0 +1,117 @@ +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class StreamAutoClaimIdsOnly(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + [Fact] + public void WithIds_ThreeElements_Success() + { + // XAUTOCLAIM mystream mygroup Alice 3600000 0-0 COUNT 25 JUSTID + // 1) "0-0" + // 2) 1) "1609338752495-0" + // 2) "1609338752496-0" + // 3) (empty array) + var resp = "*3\r\n" + + "$3\r\n0-0\r\n" + + "*2\r\n" + // Array of 2 claimed IDs + "$15\r\n1609338752495-0\r\n" + + "$15\r\n1609338752496-0\r\n" + + "*0\r\n"; // Empty deleted IDs array + + var result = Execute(resp, ResultProcessor.StreamAutoClaimIdsOnly); + + Assert.Equal("0-0", result.NextStartId.ToString()); + Assert.Equal(2, result.ClaimedIds.Length); + Assert.Equal("1609338752495-0", result.ClaimedIds[0].ToString()); + Assert.Equal("1609338752496-0", result.ClaimedIds[1].ToString()); + Assert.Empty(result.DeletedIds); + } + + [Fact] + public void WithIds_TwoElements_OlderServer_Success() + { + // Older Redis 6.2 - only returns 2 elements (no deleted IDs) + var resp = "*2\r\n" + + "$3\r\n0-0\r\n" + + "*2\r\n" + + "$15\r\n1609338752495-0\r\n" + + "$15\r\n1609338752496-0\r\n"; + + var result = Execute(resp, ResultProcessor.StreamAutoClaimIdsOnly); + + Assert.Equal("0-0", result.NextStartId.ToString()); + Assert.Equal(2, result.ClaimedIds.Length); + Assert.Empty(result.DeletedIds); + } + + [Fact] + public void EmptyIds_Success() + { + // No IDs claimed + var resp = "*3\r\n" + + "$3\r\n0-0\r\n" + + "*0\r\n" + // Empty claimed IDs array + "*0\r\n"; // Empty deleted IDs array + + var result = Execute(resp, ResultProcessor.StreamAutoClaimIdsOnly); + + Assert.Equal("0-0", result.NextStartId.ToString()); + Assert.Empty(result.ClaimedIds); + Assert.Empty(result.DeletedIds); + } + + [Fact] + public void NullIds_Success() + { + // Null IDs array (alternative representation) + var resp = "*3\r\n" + + "$3\r\n0-0\r\n" + + "$-1\r\n" + // Null claimed IDs + "*0\r\n"; + + var result = Execute(resp, ResultProcessor.StreamAutoClaimIdsOnly); + + Assert.Equal("0-0", result.NextStartId.ToString()); + Assert.Empty(result.ClaimedIds); + Assert.Empty(result.DeletedIds); + } + + [Fact] + public void WithDeletedIds_Success() + { + // Some entries were deleted + var resp = "*3\r\n" + + "$3\r\n0-0\r\n" + + "*1\r\n" + // 1 claimed ID + "$15\r\n1609338752495-0\r\n" + + "*2\r\n" + // 2 deleted IDs + "$15\r\n1609338752496-0\r\n" + + "$15\r\n1609338752497-0\r\n"; + + var result = Execute(resp, ResultProcessor.StreamAutoClaimIdsOnly); + + Assert.Equal("0-0", result.NextStartId.ToString()); + Assert.Single(result.ClaimedIds); + Assert.Equal("1609338752495-0", result.ClaimedIds[0].ToString()); + Assert.Equal(2, result.DeletedIds.Length); + Assert.Equal("1609338752496-0", result.DeletedIds[0].ToString()); + Assert.Equal("1609338752497-0", result.DeletedIds[1].ToString()); + } + + [Fact] + public void NotArray_Failure() + { + var resp = "$5\r\nhello\r\n"; + + ExecuteUnexpected(resp, ResultProcessor.StreamAutoClaimIdsOnly); + } + + [Fact] + public void Null_Failure() + { + var resp = "$-1\r\n"; + + ExecuteUnexpected(resp, ResultProcessor.StreamAutoClaimIdsOnly); + } +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/StreamInfo.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/StreamInfo.cs new file mode 100644 index 000000000..5b33f0563 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/StreamInfo.cs @@ -0,0 +1,123 @@ +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class StreamInfo(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + [Fact] + public void BasicFormat_Success() + { + // XINFO STREAM mystream (basic format, not FULL) + // Interleaved key-value array with entries like first-entry and last-entry as nested arrays + var resp = "*32\r\n" + + "$6\r\nlength\r\n" + + ":2\r\n" + + "$15\r\nradix-tree-keys\r\n" + + ":1\r\n" + + "$16\r\nradix-tree-nodes\r\n" + + ":2\r\n" + + "$17\r\nlast-generated-id\r\n" + + "$15\r\n1638125141232-0\r\n" + + "$20\r\nmax-deleted-entry-id\r\n" + + "$3\r\n0-0\r\n" + + "$13\r\nentries-added\r\n" + + ":2\r\n" + + "$23\r\nrecorded-first-entry-id\r\n" + + "$15\r\n1719505260513-0\r\n" + + "$13\r\nidmp-duration\r\n" + + ":100\r\n" + + "$12\r\nidmp-maxsize\r\n" + + ":100\r\n" + + "$12\r\npids-tracked\r\n" + + ":1\r\n" + + "$12\r\niids-tracked\r\n" + + ":1\r\n" + + "$10\r\niids-added\r\n" + + ":1\r\n" + + "$15\r\niids-duplicates\r\n" + + ":0\r\n" + + "$6\r\ngroups\r\n" + + ":1\r\n" + + "$11\r\nfirst-entry\r\n" + + "*2\r\n" + + "$15\r\n1638125133432-0\r\n" + + "*2\r\n" + + "$7\r\nmessage\r\n" + + "$5\r\napple\r\n" + + "$10\r\nlast-entry\r\n" + + "*2\r\n" + + "$15\r\n1638125141232-0\r\n" + + "*2\r\n" + + "$7\r\nmessage\r\n" + + "$6\r\nbanana\r\n"; + + var result = Execute(resp, ResultProcessor.StreamInfo); + + Assert.Equal(2, result.Length); + Assert.Equal(1, result.RadixTreeKeys); + Assert.Equal(2, result.RadixTreeNodes); + Assert.Equal(1, result.ConsumerGroupCount); + Assert.Equal("1638125141232-0", result.LastGeneratedId.ToString()); + Assert.Equal("0-0", result.MaxDeletedEntryId.ToString()); + Assert.Equal(2, result.EntriesAdded); + Assert.Equal("1719505260513-0", result.RecordedFirstEntryId.ToString()); + Assert.Equal(100, result.IdmpDuration); + Assert.Equal(100, result.IdmpMaxSize); + Assert.Equal(1, result.PidsTracked); + Assert.Equal(1, result.IidsTracked); + Assert.Equal(1, result.IidsAdded); + Assert.Equal(0, result.IidsDuplicates); + + Assert.Equal("1638125133432-0", result.FirstEntry.Id.ToString()); + Assert.Equal("apple", result.FirstEntry["message"]); + + Assert.Equal("1638125141232-0", result.LastEntry.Id.ToString()); + Assert.Equal("banana", result.LastEntry["message"]); + } + + [Fact] + public void MinimalFormat_Success() + { + // Minimal XINFO STREAM response with just required fields + var resp = "*14\r\n" + + "$6\r\nlength\r\n" + + ":0\r\n" + + "$15\r\nradix-tree-keys\r\n" + + ":1\r\n" + + "$16\r\nradix-tree-nodes\r\n" + + ":1\r\n" + + "$6\r\ngroups\r\n" + + ":0\r\n" + + "$11\r\nfirst-entry\r\n" + + "$-1\r\n" + + "$10\r\nlast-entry\r\n" + + "$-1\r\n" + + "$17\r\nlast-generated-id\r\n" + + "$3\r\n0-0\r\n"; + + var result = Execute(resp, ResultProcessor.StreamInfo); + + Assert.Equal(0, result.Length); + Assert.Equal(1, result.RadixTreeKeys); + Assert.Equal(1, result.RadixTreeNodes); + Assert.Equal(0, result.ConsumerGroupCount); + Assert.True(result.FirstEntry.IsNull); + Assert.True(result.LastEntry.IsNull); + } + + [Fact] + public void NotArray_Failure() + { + var resp = "$5\r\nhello\r\n"; + + ExecuteUnexpected(resp, ResultProcessor.StreamInfo); + } + + [Fact] + public void Null_Failure() + { + var resp = "$-1\r\n"; + + ExecuteUnexpected(resp, ResultProcessor.StreamInfo); + } +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/StreamPendingInfo.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/StreamPendingInfo.cs new file mode 100644 index 000000000..6655c6c08 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/StreamPendingInfo.cs @@ -0,0 +1,113 @@ +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class StreamPendingInfo(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + [Fact] + public void SingleConsumer_Success() + { + // XPENDING mystream group55 + // 1) (integer) 1 + // 2) 1526984818136-0 + // 3) 1526984818136-0 + // 4) 1) 1) "consumer-123" + // 2) "1" + var resp = "*4\r\n" + + ":1\r\n" + + "$15\r\n1526984818136-0\r\n" + + "$15\r\n1526984818136-0\r\n" + + "*1\r\n" + // Array of 1 consumer + "*2\r\n" + // Each consumer is an array of 2 elements + "$12\r\nconsumer-123\r\n" + // Consumer name + "$1\r\n1\r\n"; // Pending count as string + + var result = Execute(resp, ResultProcessor.StreamPendingInfo); + + Assert.Equal(1, result.PendingMessageCount); + Assert.Equal("1526984818136-0", result.LowestPendingMessageId); + Assert.Equal("1526984818136-0", result.HighestPendingMessageId); + Assert.Single(result.Consumers); + Assert.Equal("consumer-123", result.Consumers[0].Name); + Assert.Equal(1, result.Consumers[0].PendingMessageCount); + } + + [Fact] + public void MultipleConsumers_Success() + { + // XPENDING mystream mygroup + // 1) (integer) 10 + // 2) 1526569498055-0 + // 3) 1526569506935-0 + // 4) 1) 1) "Bob" + // 2) "2" + // 2) 1) "Joe" + // 2) "8" + var resp = "*4\r\n" + + ":10\r\n" + + "$15\r\n1526569498055-0\r\n" + + "$15\r\n1526569506935-0\r\n" + + "*2\r\n" + // Array of 2 consumers + "*2\r\n" + // First consumer array + "$3\r\nBob\r\n" + + "$1\r\n2\r\n" + + "*2\r\n" + // Second consumer array + "$3\r\nJoe\r\n" + + "$1\r\n8\r\n"; + + var result = Execute(resp, ResultProcessor.StreamPendingInfo); + + Assert.Equal(10, result.PendingMessageCount); + Assert.Equal("1526569498055-0", result.LowestPendingMessageId); + Assert.Equal("1526569506935-0", result.HighestPendingMessageId); + Assert.Equal(2, result.Consumers.Length); + Assert.Equal("Bob", result.Consumers[0].Name); + Assert.Equal(2, result.Consumers[0].PendingMessageCount); + Assert.Equal("Joe", result.Consumers[1].Name); + Assert.Equal(8, result.Consumers[1].PendingMessageCount); + } + + [Fact] + public void NoConsumers_Success() + { + // When there are no consumers yet, the 4th element is null + var resp = "*4\r\n" + + ":0\r\n" + + "$15\r\n1526569498055-0\r\n" + + "$15\r\n1526569506935-0\r\n" + + "$-1\r\n"; // null + + var result = Execute(resp, ResultProcessor.StreamPendingInfo); + + Assert.Equal(0, result.PendingMessageCount); + Assert.Empty(result.Consumers); + } + + [Fact] + public void WrongArrayLength_Failure() + { + // Array with wrong length (3 instead of 4) + var resp = "*3\r\n" + + ":1\r\n" + + "$15\r\n1526984818136-0\r\n" + + "$15\r\n1526984818136-0\r\n"; + + ExecuteUnexpected(resp, ResultProcessor.StreamPendingInfo); + } + + [Fact] + public void NotArray_Failure() + { + var resp = "$5\r\nhello\r\n"; + + ExecuteUnexpected(resp, ResultProcessor.StreamPendingInfo); + } + + [Fact] + public void Null_Failure() + { + var resp = "$-1\r\n"; + + ExecuteUnexpected(resp, ResultProcessor.StreamPendingInfo); + } +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/StreamPendingMessages.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/StreamPendingMessages.cs new file mode 100644 index 000000000..48357a3f8 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/StreamPendingMessages.cs @@ -0,0 +1,99 @@ +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class StreamPendingMessages(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + [Fact] + public void SingleMessage_Success() + { + // XPENDING mystream group55 - + 10 + // 1) 1) 1526984818136-0 + // 2) "consumer-123" + // 3) (integer) 196415 + // 4) (integer) 1 + var resp = "*1\r\n" + // Array of 1 message + "*4\r\n" + // Each message is an array of 4 elements + "$15\r\n1526984818136-0\r\n" + // Message ID + "$12\r\nconsumer-123\r\n" + // Consumer name + ":196415\r\n" + // Idle time in ms + ":1\r\n"; // Delivery count + + var result = Execute(resp, ResultProcessor.StreamPendingMessages); + + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("1526984818136-0", result[0].MessageId); + Assert.Equal("consumer-123", result[0].ConsumerName); + Assert.Equal(196415, result[0].IdleTimeInMilliseconds); + Assert.Equal(1, result[0].DeliveryCount); + } + + [Fact] + public void MultipleMessages_Success() + { + // XPENDING mystream group55 - + 10 + // 1) 1) 1526984818136-0 + // 2) "consumer-123" + // 3) (integer) 196415 + // 4) (integer) 1 + // 2) 1) 1526984818137-0 + // 2) "consumer-456" + // 3) (integer) 5000 + // 4) (integer) 3 + var resp = "*2\r\n" + // Array of 2 messages + "*4\r\n" + // First message + "$15\r\n1526984818136-0\r\n" + + "$12\r\nconsumer-123\r\n" + + ":196415\r\n" + + ":1\r\n" + + "*4\r\n" + // Second message + "$15\r\n1526984818137-0\r\n" + + "$12\r\nconsumer-456\r\n" + + ":5000\r\n" + + ":3\r\n"; + + var result = Execute(resp, ResultProcessor.StreamPendingMessages); + + Assert.NotNull(result); + Assert.Equal(2, result.Length); + + Assert.Equal("1526984818136-0", result[0].MessageId); + Assert.Equal("consumer-123", result[0].ConsumerName); + Assert.Equal(196415, result[0].IdleTimeInMilliseconds); + Assert.Equal(1, result[0].DeliveryCount); + + Assert.Equal("1526984818137-0", result[1].MessageId); + Assert.Equal("consumer-456", result[1].ConsumerName); + Assert.Equal(5000, result[1].IdleTimeInMilliseconds); + Assert.Equal(3, result[1].DeliveryCount); + } + + [Fact] + public void EmptyArray_Success() + { + // No pending messages + var resp = "*0\r\n"; + + var result = Execute(resp, ResultProcessor.StreamPendingMessages); + + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void NotArray_Failure() + { + var resp = "$5\r\nhello\r\n"; + + ExecuteUnexpected(resp, ResultProcessor.StreamPendingMessages); + } + + [Fact] + public void Null_Failure() + { + var resp = "$-1\r\n"; + + ExecuteUnexpected(resp, ResultProcessor.StreamPendingMessages); + } +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/TracerProcessor.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/TracerProcessor.cs new file mode 100644 index 000000000..606f51242 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/TracerProcessor.cs @@ -0,0 +1,30 @@ +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class TracerProcessor(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + [Fact] + public void Ping_Pong_Success() + { + // PING response - simple string + var resp = "+PONG\r\n"; + var message = Message.Create(-1, default, RedisCommand.PING); + + var result = Execute(resp, ResultProcessor.Tracer, message); + + Assert.True(result); + } + + [Fact] + public void Time_Success() + { + // TIME response - array of 2 elements + var resp = "*2\r\n$10\r\n1609459200\r\n$6\r\n123456\r\n"; + var message = Message.Create(-1, default, RedisCommand.TIME); + + var result = Execute(resp, ResultProcessor.Tracer, message); + + Assert.True(result); + } +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/TrackSubscriptions.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/TrackSubscriptions.cs new file mode 100644 index 000000000..ceb82cb25 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/TrackSubscriptions.cs @@ -0,0 +1,19 @@ +using Xunit; + +namespace StackExchange.Redis.Tests.ResultProcessorUnitTests; + +public class TrackSubscriptions(ITestOutputHelper log) : ResultProcessorUnitTest(log) +{ + [Theory] + [InlineData("*3\r\n$9\r\nsubscribe\r\n$7\r\nchannel\r\n:1\r\n", 1)] // SUBSCRIBE response with count 1 + [InlineData("*3\r\n$9\r\nsubscribe\r\n$7\r\nchannel\r\n:5\r\n", 5)] // SUBSCRIBE response with count 5 + [InlineData("*3\r\n$11\r\nunsubscribe\r\n$7\r\nchannel\r\n:0\r\n", 0)] // UNSUBSCRIBE response with count 0 + [InlineData("*3\r\n$10\r\npsubscribe\r\n$8\r\npattern*\r\n:2\r\n", 2)] // PSUBSCRIBE response with count 2 + public void TrackSubscriptions_Success(string resp, int expectedCount) + { + var processor = ResultProcessor.TrackSubscriptions; + var result = Execute(resp, processor); + Assert.True(result); + Log($"Successfully parsed subscription response with count {expectedCount}"); + } +} diff --git a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/VectorSet.cs b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/VectorSet.cs index b48ebfcf4..8b54e2e2e 100644 --- a/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/VectorSet.cs +++ b/tests/StackExchange.Redis.Tests/ResultProcessorUnitTests/VectorSet.cs @@ -78,4 +78,113 @@ public void VectorSetInfo_SkipsNonScalarValues() Assert.Equal(VectorSetQuantization.Unknown, result.Value.Quantization); Assert.Equal(0, result.Value.Dimension); } + + [Fact] + public void VectorSetLinks_EmptyArray() + { + // VLINKS returns empty array + var resp = "*0\r\n"; + var processor = ResultProcessor.VectorSetLinks; + using var result = Execute(resp, processor); + + Assert.NotNull(result); + Assert.Equal(0, result.Length); + } + + [Theory] + [InlineData("*-1\r\n")] // null array (RESP2) + [InlineData("_\r\n")] // null (RESP3) + public void VectorSetLinks_NullArray(string resp) + { + var processor = ResultProcessor.VectorSetLinks; + using var result = Execute(resp, processor); + + Assert.NotNull(result); + Assert.Equal(0, result.Length); + } + + [Fact] + public void VectorSetLinks_SingleNestedArray() + { + // VLINKS returns [[element1]] + var resp = "*1\r\n*1\r\n$8\r\nelement1\r\n"; + var processor = ResultProcessor.VectorSetLinks; + using var result = Execute(resp, processor); + + Assert.NotNull(result); + Assert.Equal(1, result.Length); + Assert.Equal("element1", result.Span[0].ToString()); + } + + [Fact] + public void VectorSetLinks_MultipleNestedArrays() + { + // VLINKS returns [[element1], [element2, element3], [element4]] + var resp = "*3\r\n*1\r\n$8\r\nelement1\r\n*2\r\n$8\r\nelement2\r\n$8\r\nelement3\r\n*1\r\n$8\r\nelement4\r\n"; + var processor = ResultProcessor.VectorSetLinks; + using var result = Execute(resp, processor); + + Assert.NotNull(result); + Assert.Equal(4, result.Length); + Assert.Equal("element1", result.Span[0].ToString()); + Assert.Equal("element2", result.Span[1].ToString()); + Assert.Equal("element3", result.Span[2].ToString()); + Assert.Equal("element4", result.Span[3].ToString()); + } + + [Fact] + public void VectorSetLinksWithScores_EmptyArray() + { + // VLINKS WITHSCORES returns empty array + var resp = "*0\r\n"; + var processor = ResultProcessor.VectorSetLinksWithScores; + using var result = Execute(resp, processor); + + Assert.NotNull(result); + Assert.Equal(0, result.Length); + } + + [Theory] + [InlineData("*-1\r\n")] // null array (RESP2) + [InlineData("_\r\n")] // null (RESP3) + public void VectorSetLinksWithScores_NullArray(string resp) + { + var processor = ResultProcessor.VectorSetLinksWithScores; + using var result = Execute(resp, processor); + + Assert.NotNull(result); + Assert.Equal(0, result.Length); + } + + [Fact] + public void VectorSetLinksWithScores_SingleNestedArray() + { + // VLINKS WITHSCORES returns [[element1, score1]] + var resp = "*1\r\n*2\r\n$8\r\nelement1\r\n$3\r\n1.5\r\n"; + var processor = ResultProcessor.VectorSetLinksWithScores; + using var result = Execute(resp, processor); + + Assert.NotNull(result); + Assert.Equal(1, result.Length); + Assert.Equal("element1", result.Span[0].Member.ToString()); + Assert.Equal(1.5, result.Span[0].Score); + } + + [Fact] + public void VectorSetLinksWithScores_MultipleNestedArrays() + { + // VLINKS WITHSCORES returns [[element1, score1], [element2, score2], [element3, score3]] + var resp = "*3\r\n*2\r\n$8\r\nelement1\r\n$3\r\n1.5\r\n*2\r\n$8\r\nelement2\r\n$3\r\n2.5\r\n*2\r\n$8\r\nelement3\r\n$3\r\n3.5\r\n"; + var processor = ResultProcessor.VectorSetLinksWithScores; + using var result = Execute(resp, processor); + + Assert.NotNull(result); + Assert.Equal(3, result.Length); + Assert.Equal("element1", result.Span[0].Member.ToString()); + Assert.Equal(1.5, result.Span[0].Score); + Assert.Equal("element2", result.Span[1].Member.ToString()); + Assert.Equal(2.5, result.Span[1].Score); + Assert.Equal("element3", result.Span[2].Member.ToString()); + Assert.Equal(3.5, result.Span[2].Score); + } } diff --git a/tests/StackExchange.Redis.Tests/StringTests.cs b/tests/StackExchange.Redis.Tests/StringTests.cs index 1ade532d7..2dcf8f6fb 100644 --- a/tests/StackExchange.Redis.Tests/StringTests.cs +++ b/tests/StackExchange.Redis.Tests/StringTests.cs @@ -967,8 +967,8 @@ public async Task LongestCommonSubsequence() var stringMatchResult = db.StringLongestCommonSubsequenceWithMatches(key1, key2); Assert.Equal(2, stringMatchResult.Matches.Length); // "my" and "text" are the two matches of the result - Assert.Equivalent(new LCSMatchResult.LCSMatch(4, 5, length: 4), stringMatchResult.Matches[0]); // the string "text" starts at index 4 in the first string and at index 5 in the second string - Assert.Equivalent(new LCSMatchResult.LCSMatch(2, 0, length: 2), stringMatchResult.Matches[1]); // the string "my" starts at index 2 in the first string and at index 0 in the second string + Assert.Equivalent(new LCSMatchResult.LCSMatch(new(4, 7), new(5, 8), length: 4), stringMatchResult.Matches[0]); // the string "text" starts at index 4 in the first string and at index 5 in the second string + Assert.Equivalent(new LCSMatchResult.LCSMatch(new(2, 3), new(0, 1), length: 2), stringMatchResult.Matches[1]); // the string "my" starts at index 2 in the first string and at index 0 in the second string stringMatchResult = db.StringLongestCommonSubsequenceWithMatches(key1, key2, 5); Assert.Empty(stringMatchResult.Matches); // no matches longer than 5 characters @@ -1007,8 +1007,8 @@ public async Task LongestCommonSubsequenceAsync() var stringMatchResult = await db.StringLongestCommonSubsequenceWithMatchesAsync(key1, key2); Assert.Equal(2, stringMatchResult.Matches.Length); // "my" and "text" are the two matches of the result - Assert.Equivalent(new LCSMatchResult.LCSMatch(4, 5, length: 4), stringMatchResult.Matches[0]); // the string "text" starts at index 4 in the first string and at index 5 in the second string - Assert.Equivalent(new LCSMatchResult.LCSMatch(2, 0, length: 2), stringMatchResult.Matches[1]); // the string "my" starts at index 2 in the first string and at index 0 in the second string + Assert.Equivalent(new LCSMatchResult.LCSMatch(new(4, 7), new(5, 8), length: 4), stringMatchResult.Matches[0]); // the string "text" starts at index 4 in the first string and at index 5 in the second string + Assert.Equivalent(new LCSMatchResult.LCSMatch(new(2, 3), new(0, 1), length: 2), stringMatchResult.Matches[1]); // the string "my" starts at index 2 in the first string and at index 0 in the second string stringMatchResult = await db.StringLongestCommonSubsequenceWithMatchesAsync(key1, key2, 5); Assert.Empty(stringMatchResult.Matches); // no matches longer than 5 characters