From 6d12eab0515ae6a1362b50b7313d79cf572376b2 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 24 Feb 2026 17:10:02 +0000 Subject: [PATCH 1/3] WIP --- src/RESPite/PublicAPI/PublicAPI.Unshipped.txt | 3 +- src/RESPite/Shared/AsciiHash.Instance.cs | 3 +- src/StackExchange.Redis/CommandBytes.cs | 207 ------------------ src/StackExchange.Redis/CommandMap.cs | 25 ++- src/StackExchange.Redis/MessageWriter.cs | 34 +-- src/StackExchange.Redis/RedisDatabase.cs | 28 ++- src/StackExchange.Redis/ResultProcessor.cs | 14 -- 7 files changed, 49 insertions(+), 265 deletions(-) delete mode 100644 src/StackExchange.Redis/CommandBytes.cs diff --git a/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt b/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt index 9173b3c85..b66923219 100644 --- a/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt @@ -8,7 +8,6 @@ [SER004]RESPite.AsciiHash.AsciiHash() -> void [SER004]RESPite.AsciiHash.AsciiHash(byte[]! arr) -> void [SER004]RESPite.AsciiHash.AsciiHash(byte[]! arr, int index, int length) -> void -[SER004]RESPite.AsciiHash.AsciiHash(string! value) -> void [SER004]RESPite.AsciiHash.AsciiHash(System.ReadOnlySpan value) -> void [SER004]RESPite.AsciiHash.BufferLength.get -> int [SER004]RESPite.AsciiHash.Equals(in RESPite.AsciiHash other) -> bool @@ -21,6 +20,8 @@ [SER004]RESPite.AsciiHashAttribute.CaseSensitive.get -> bool [SER004]RESPite.AsciiHashAttribute.CaseSensitive.set -> void [SER004]RESPite.AsciiHashAttribute.Token.get -> string! +[SER004]RESPite.AsciiHash.AsciiHash(string? value) -> void +[SER004]RESPite.AsciiHash.IsEmpty.get -> bool [SER004]RESPite.Buffers.CycleBuffer [SER004]RESPite.Buffers.CycleBuffer.Commit(int count) -> void [SER004]RESPite.Buffers.CycleBuffer.CommittedIsEmpty.get -> bool diff --git a/src/RESPite/Shared/AsciiHash.Instance.cs b/src/RESPite/Shared/AsciiHash.Instance.cs index 704c78ee6..a43384397 100644 --- a/src/RESPite/Shared/AsciiHash.Instance.cs +++ b/src/RESPite/Shared/AsciiHash.Instance.cs @@ -20,9 +20,10 @@ namespace RESPite; public int BufferLength => (Length + 1 + 7) & ~7; // an extra byte, then round up to word-size public ReadOnlySpan Span => new(_arr ?? [], _index, _length); + public bool IsEmpty => Length != 0; public AsciiHash(ReadOnlySpan value) : this(value.ToArray(), 0, value.Length) { } - public AsciiHash(string value) : this(Encoding.ASCII.GetBytes(value)) { } + public AsciiHash(string? value) : this(value is null ? [] : Encoding.ASCII.GetBytes(value)) { } /// public override int GetHashCode() => _hashCS.GetHashCode(); diff --git a/src/StackExchange.Redis/CommandBytes.cs b/src/StackExchange.Redis/CommandBytes.cs deleted file mode 100644 index da6e3df6a..000000000 --- a/src/StackExchange.Redis/CommandBytes.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System; -using System.Buffers; -using System.Text; - -namespace StackExchange.Redis -{ - internal readonly struct CommandBytes : IEquatable - { - private static Encoding Encoding => Encoding.UTF8; - - internal static unsafe CommandBytes TrimToFit(string value) - { - if (string.IsNullOrWhiteSpace(value)) return default; - value = value.Trim(); - var len = Encoding.GetByteCount(value); - if (len <= MaxLength) return new CommandBytes(value); // all fits - - fixed (char* c = value) - { - byte* b = stackalloc byte[ChunkLength * sizeof(ulong)]; - var encoder = MessageWriter.GetPerThreadEncoder(); - encoder.Convert(c, value.Length, b, MaxLength, true, out var maxLen, out _, out var isComplete); - if (!isComplete) maxLen--; - return new CommandBytes(value.Substring(0, maxLen)); - } - } - - // Uses [n=4] x UInt64 values to store a command payload, - // allowing allocation free storage and efficient - // equality tests. If you're glancing at this and thinking - // "that's what fixed buffers are for", please see: - // https://github.com/dotnet/coreclr/issues/19149 - // - // note: this tries to use case insensitive comparison - private readonly ulong _0, _1, _2, _3; - private const int ChunkLength = 4; // must reflect qty above - - public const int MaxLength = (ChunkLength * sizeof(ulong)) - 1; - - public override int GetHashCode() - { - var hashCode = -1923861349; - hashCode = (hashCode * -1521134295) + _0.GetHashCode(); - hashCode = (hashCode * -1521134295) + _1.GetHashCode(); - hashCode = (hashCode * -1521134295) + _2.GetHashCode(); - hashCode = (hashCode * -1521134295) + _3.GetHashCode(); - return hashCode; - } - - public override bool Equals(object? obj) => obj is CommandBytes cb && Equals(cb); - - bool IEquatable.Equals(CommandBytes other) => _0 == other._0 && _1 == other._1 && _2 == other._2 && _3 == other._3; - - public bool Equals(in CommandBytes other) => _0 == other._0 && _1 == other._1 && _2 == other._2 && _3 == other._3; - - // note: don't add == operators; with the implicit op above, that invalidates "==null" compiler checks (which should report a failure!) - public static implicit operator CommandBytes(string value) => new CommandBytes(value); - - public override unsafe string ToString() - { - fixed (ulong* uPtr = &_0) - { - var bPtr = (byte*)uPtr; - int len = *bPtr; - return len == 0 ? "" : Encoding.GetString(bPtr + 1, *bPtr); - } - } - public unsafe int Length - { - get - { - fixed (ulong* uPtr = &_0) - { - return *(byte*)uPtr; - } - } - } - - public bool IsEmpty => _0 == 0L; // cheap way of checking zero length - - public unsafe void CopyTo(Span target) - { - fixed (ulong* uPtr = &_0) - { - byte* bPtr = (byte*)uPtr; - new Span(bPtr + 1, *bPtr).CopyTo(target); - } - } - - public unsafe byte this[int index] - { - get - { - fixed (ulong* uPtr = &_0) - { - byte* bPtr = (byte*)uPtr; - int len = *bPtr; - if (index < 0 || index >= len) throw new IndexOutOfRangeException(); - return bPtr[index + 1]; - } - } - } - - public unsafe CommandBytes(string? value) - { - _0 = _1 = _2 = _3 = 0L; - if (value.IsNullOrEmpty()) return; - - var len = Encoding.GetByteCount(value); - if (len > MaxLength) throw new ArgumentOutOfRangeException($"Command '{value}' exceeds library limit of {MaxLength} bytes"); - fixed (ulong* uPtr = &_0) - { - byte* bPtr = (byte*)uPtr; - fixed (char* cPtr = value) - { - len = Encoding.GetBytes(cPtr, value.Length, bPtr + 1, MaxLength); - } - *bPtr = (byte)UpperCasify(len, bPtr + 1); - } - } - - public unsafe CommandBytes(ReadOnlySpan value) - { - if (value.Length > MaxLength) throw new ArgumentOutOfRangeException("Maximum command length exceeded: " + value.Length + " bytes"); - _0 = _1 = _2 = _3 = 0L; - fixed (ulong* uPtr = &_0) - { - byte* bPtr = (byte*)uPtr; - value.CopyTo(new Span(bPtr + 1, value.Length)); - *bPtr = (byte)UpperCasify(value.Length, bPtr + 1); - } - } - - public unsafe CommandBytes(in ReadOnlySequence value) - { - if (value.Length > MaxLength) throw new ArgumentOutOfRangeException(nameof(value), "Maximum command length exceeded"); - int len = unchecked((int)value.Length); - _0 = _1 = _2 = _3 = 0L; - fixed (ulong* uPtr = &_0) - { - byte* bPtr = (byte*)uPtr; - var target = new Span(bPtr + 1, len); - - if (value.IsSingleSegment) - { - value.First.Span.CopyTo(target); - } - else - { - foreach (var segment in value) - { - segment.Span.CopyTo(target); - target = target.Slice(segment.Length); - } - } - *bPtr = (byte)UpperCasify(len, bPtr + 1); - } - } - private unsafe int UpperCasify(int len, byte* bPtr) - { - const ulong HighBits = 0x8080808080808080; - if (((_0 | _1 | _2 | _3) & HighBits) == 0) - { - // no Unicode; use ASCII bit bricks - for (int i = 0; i < len; i++) - { - *bPtr = ToUpperInvariantAscii(*bPtr++); - } - return len; - } - else - { - return UpperCasifyUnicode(len, bPtr); - } - } - - private static unsafe int UpperCasifyUnicode(int oldLen, byte* bPtr) - { - const int MaxChars = ChunkLength * sizeof(ulong); // leave rounded up; helps stackalloc - char* workspace = stackalloc char[MaxChars]; - int charCount = Encoding.GetChars(bPtr, oldLen, workspace, MaxChars); - char* c = workspace; - for (int i = 0; i < charCount; i++) - { - *c = char.ToUpperInvariant(*c++); - } - int newLen = Encoding.GetBytes(workspace, charCount, bPtr, MaxLength); - // don't forget to zero any shrink - for (int i = newLen; i < oldLen; i++) - { - bPtr[i] = 0; - } - return newLen; - } - - private static byte ToUpperInvariantAscii(byte b) => b >= 'a' && b <= 'z' ? (byte)(b - 32) : b; - - internal unsafe byte[] ToArray() - { - fixed (ulong* uPtr = &_0) - { - byte* bPtr = (byte*)uPtr; - return new Span(bPtr + 1, *bPtr).ToArray(); - } - } - } -} diff --git a/src/StackExchange.Redis/CommandMap.cs b/src/StackExchange.Redis/CommandMap.cs index 683e51219..d818cf7cb 100644 --- a/src/StackExchange.Redis/CommandMap.cs +++ b/src/StackExchange.Redis/CommandMap.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using RESPite; namespace StackExchange.Redis { @@ -9,9 +10,9 @@ namespace StackExchange.Redis /// public sealed class CommandMap { - private readonly CommandBytes[] map; + private readonly AsciiHash[] map; - internal CommandMap(CommandBytes[] map) => this.map = map; + internal CommandMap(AsciiHash[] map) => this.map = map; /// /// The default commands specified by redis. @@ -180,7 +181,7 @@ internal void AppendDeltas(StringBuilder sb) for (int i = 0; i < map.Length; i++) { var keyString = ((RedisCommand)i).ToString(); - var keyBytes = new CommandBytes(keyString); + var keyBytes = new AsciiHash(keyString); var value = map[i]; if (!keyBytes.Equals(value)) { @@ -195,17 +196,19 @@ internal void AssertAvailable(RedisCommand command) if (map[(int)command].IsEmpty) throw ExceptionFactory.CommandDisabled(command); } - internal CommandBytes GetBytes(RedisCommand command) => map[(int)command]; + internal AsciiHash GetBytes(RedisCommand command) => map[(int)command]; - internal CommandBytes GetBytes(string command) + internal bool TryGetBytes(string command, out AsciiHash bytes) { - if (command == null) return default; - if (Enum.TryParse(command, true, out RedisCommand cmd)) + if (command is { Length: > 0 } && Enum.TryParse(command, true, out RedisCommand cmd)) { // we know that one! - return map[(int)cmd]; + bytes = map[(int)cmd]; + return false; } - return new CommandBytes(command); + + bytes = default; + return false; } internal bool IsAvailable(RedisCommand command) => !map[(int)command].IsEmpty; @@ -214,7 +217,7 @@ private static CommandMap CreateImpl(Dictionary? caseInsensitiv { var commands = (RedisCommand[])Enum.GetValues(typeof(RedisCommand)); - var map = new CommandBytes[commands.Length]; + var map = new AsciiHash[commands.Length]; for (int i = 0; i < commands.Length; i++) { int idx = (int)commands[i]; @@ -230,7 +233,7 @@ private static CommandMap CreateImpl(Dictionary? caseInsensitiv { value = tmp; } - map[idx] = new CommandBytes(value); + map[idx] = new AsciiHash(value); } } return new CommandMap(map); diff --git a/src/StackExchange.Redis/MessageWriter.cs b/src/StackExchange.Redis/MessageWriter.cs index 49c7d6ec1..cc4e47d38 100644 --- a/src/StackExchange.Redis/MessageWriter.cs +++ b/src/StackExchange.Redis/MessageWriter.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using RESPite; using RESPite.Internal; namespace StackExchange.Redis; @@ -108,21 +109,14 @@ internal const int REDIS_MAX_ARGS = 1024 * 1024; // there is a <= 1024*1024 max constraint inside redis itself: https://github.com/antirez/redis/blob/6c60526db91e23fb2d666fc52facc9a11780a2a3/src/networking.c#L1024 - internal void WriteHeader(RedisCommand command, int arguments, CommandBytes commandBytes = default) - { - if (command == RedisCommand.UNKNOWN) - { - // using >= here because we will be adding 1 for the command itself (which is an arg for the purposes of the multi-bulk protocol) - if (arguments >= REDIS_MAX_ARGS) throw ExceptionFactory.TooManyArgs(commandBytes.ToString(), arguments); - } - else - { - // using >= here because we will be adding 1 for the command itself (which is an arg for the purposes of the multi-bulk protocol) - if (arguments >= REDIS_MAX_ARGS) throw ExceptionFactory.TooManyArgs(command.ToString(), arguments); + internal void WriteHeader(in AsciiHash command, int arguments) => WriteHeader(RedisCommand.UNKNOWN, arguments, command.Span); - // for everything that isn't custom commands: ask the muxer for the actual bytes - commandBytes = _map.GetBytes(command); - } + internal void WriteHeader(RedisCommand command, int arguments) => WriteHeader(command, arguments, _map.GetBytes(command).Span); + + internal void WriteHeader(RedisCommand command, int arguments, ReadOnlySpan commandBytes) + { + // using >= here because we will be adding 1 for the command itself (which is an arg for the purposes of the multi-bulk protocol) + if (arguments >= REDIS_MAX_ARGS) throw ExceptionFactory.TooManyArgs(command.ToString(), arguments); // in theory we should never see this; CheckMessage dealt with "regular" messages, and // ExecuteMessage should have dealt with everything else @@ -136,7 +130,7 @@ internal void WriteHeader(RedisCommand command, int arguments, CommandBytes comm int offset = WriteRaw(span, arguments + 1, offset: 1); - offset = AppendToSpanCommand(span, commandBytes, offset: offset); + offset = AppendToSpan(span, commandBytes, offset: offset); _writer.Advance(offset); } @@ -519,16 +513,6 @@ private static void WriteUnifiedSpan(IBufferWriter writer, ReadOnlySpan span, in CommandBytes value, int offset = 0) - { - span[offset++] = (byte)'$'; - int len = value.Length; - offset = WriteRaw(span, len, offset: offset); - value.CopyTo(span.Slice(offset, len)); - offset += value.Length; - return WriteCrlf(span, offset); - } - private static int AppendToSpan(Span span, ReadOnlySpan value, int offset = 0) { offset = WriteRaw(span, value.Length, offset: offset); diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index f79c5db20..fe547c5a2 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -5605,7 +5605,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes internal sealed class ExecuteMessage : Message { private readonly ICollection _args; - public new CommandBytes Command { get; } + private string? _commandUnknown; public ExecuteMessage(CommandMap? map, int db, CommandFlags flags, string command, ICollection? args) : base(db, flags, RedisCommand.UNKNOWN) { @@ -5613,14 +5613,30 @@ public ExecuteMessage(CommandMap? map, int db, CommandFlags flags, string comman { throw ExceptionFactory.TooManyArgs(command, args.Count); } - Command = map?.GetBytes(command) ?? default; - if (Command.IsEmpty) throw ExceptionFactory.CommandDisabled(command); + if (map.TryGetBytes(command, out var commandBytes)) + { + if (commandBytes.IsEmpty) throw ExceptionFactory.CommandDisabled(command); + _commandKnown = commandBytes; + } + else + { + _commandUnknown = command; + } + _args = args ?? Array.Empty(); } protected override void WriteImpl(in MessageWriter writer) { - writer.WriteHeader(RedisCommand.UNKNOWN, _args.Count, Command); + if (_commandUnknown is null) + { + Debug.Assert(!_commandKnown.IsEmpty); + writer.WriteHeader(RedisCommand.UNKNOWN, _args.Count, _commandKnown); + } + else + { + writer.WriteHeader(RedisCommand.UNKNOWN, _args.Count, _commandUnknown); + } foreach (object arg in _args) { if (arg is RedisKey key) @@ -5640,8 +5656,8 @@ protected override void WriteImpl(in MessageWriter writer) } } - public override string CommandString => Command.ToString(); - public override string CommandAndKey => Command.ToString(); + public override string CommandString => CommandRaw.ToString(); + public override string CommandAndKey => CommandRaw.ToString(); public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) { diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 1fab98d86..d7e22e57a 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -2602,20 +2602,6 @@ protected override StreamConsumerInfo ParseItem(ref RespReader reader) } } - private static class KeyValuePairParser - { - internal static readonly CommandBytes - Name = "name", - Consumers = "consumers", - Pending = "pending", - Idle = "idle", - LastDeliveredId = "last-delivered-id", - EntriesRead = "entries-read", - Lag = "lag", - IP = "ip", - Port = "port"; - } - internal sealed class StreamGroupInfoProcessor : InterleavedStreamInfoProcessorBase { protected override StreamGroupInfo ParseItem(ref RespReader reader) From 1e4105f60f718306ceeae296bc725761d5069586 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 24 Feb 2026 20:27:37 +0000 Subject: [PATCH 2/3] this might work --- src/StackExchange.Redis/CommandMap.cs | 23 +++----- src/StackExchange.Redis/Enums/RedisCommand.cs | 9 ++- src/StackExchange.Redis/MessageWriter.cs | 18 +++++- src/StackExchange.Redis/RedisDatabase.cs | 29 +++++----- .../StackExchange.Redis.Tests/CommandTests.cs | 56 ------------------- .../StackExchange.Redis.Server/RedisServer.cs | 12 +--- 6 files changed, 48 insertions(+), 99 deletions(-) delete mode 100644 tests/StackExchange.Redis.Tests/CommandTests.cs diff --git a/src/StackExchange.Redis/CommandMap.cs b/src/StackExchange.Redis/CommandMap.cs index d818cf7cb..8ea4cbf8d 100644 --- a/src/StackExchange.Redis/CommandMap.cs +++ b/src/StackExchange.Redis/CommandMap.cs @@ -133,7 +133,7 @@ public static CommandMap Create(HashSet commands, bool available = true) { var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); // nix everything - foreach (RedisCommand command in (RedisCommand[])Enum.GetValues(typeof(RedisCommand))) + foreach (RedisCommand command in AllCommands) { dictionary[command.ToString()] = null; } @@ -155,7 +155,7 @@ public static CommandMap Create(HashSet commands, bool available = true) // nix the things that are specified foreach (var command in commands) { - if (Enum.TryParse(command, true, out RedisCommand parsed)) + if (RedisCommandMetadata.TryParseCI(command, out RedisCommand parsed)) { (exclusions ??= new HashSet()).Add(parsed); } @@ -198,24 +198,15 @@ internal void AssertAvailable(RedisCommand command) internal AsciiHash GetBytes(RedisCommand command) => map[(int)command]; - internal bool TryGetBytes(string command, out AsciiHash bytes) - { - if (command is { Length: > 0 } && Enum.TryParse(command, true, out RedisCommand cmd)) - { - // we know that one! - bytes = map[(int)cmd]; - return false; - } + internal bool IsAvailable(RedisCommand command) => !map[(int)command].IsEmpty; - bytes = default; - return false; - } + private static RedisCommand[]? s_AllCommands; - internal bool IsAvailable(RedisCommand command) => !map[(int)command].IsEmpty; + private static ReadOnlySpan AllCommands => s_AllCommands ??= (RedisCommand[])Enum.GetValues(typeof(RedisCommand)); private static CommandMap CreateImpl(Dictionary? caseInsensitiveOverrides, HashSet? exclusions) { - var commands = (RedisCommand[])Enum.GetValues(typeof(RedisCommand)); + var commands = AllCommands; var map = new AsciiHash[commands.Length]; for (int i = 0; i < commands.Length; i++) @@ -223,7 +214,7 @@ private static CommandMap CreateImpl(Dictionary? caseInsensitiv int idx = (int)commands[i]; string? name = commands[i].ToString(), value = name; - if (exclusions?.Contains(commands[i]) == true) + if (commands[i] is RedisCommand.UNKNOWN || exclusions?.Contains(commands[i]) == true) { map[idx] = default; } diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index c55a39d8a..bca822031 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -1,4 +1,5 @@ using System; +using RESPite; namespace StackExchange.Redis; @@ -279,8 +280,14 @@ internal enum RedisCommand UNKNOWN, } -internal static class RedisCommandExtensions +internal static partial class RedisCommandMetadata { + [AsciiHash(CaseSensitive = false)] + public static partial bool TryParseCI(ReadOnlySpan command, out RedisCommand value); + + [AsciiHash(CaseSensitive = false)] + public static partial bool TryParseCI(ReadOnlySpan command, out RedisCommand value); + /// /// Gets whether a given command can be issued only to a primary, or if any server is eligible. /// diff --git a/src/StackExchange.Redis/MessageWriter.cs b/src/StackExchange.Redis/MessageWriter.cs index cc4e47d38..5004521d5 100644 --- a/src/StackExchange.Redis/MessageWriter.cs +++ b/src/StackExchange.Redis/MessageWriter.cs @@ -109,7 +109,21 @@ internal const int REDIS_MAX_ARGS = 1024 * 1024; // there is a <= 1024*1024 max constraint inside redis itself: https://github.com/antirez/redis/blob/6c60526db91e23fb2d666fc52facc9a11780a2a3/src/networking.c#L1024 - internal void WriteHeader(in AsciiHash command, int arguments) => WriteHeader(RedisCommand.UNKNOWN, arguments, command.Span); + internal void WriteHeader(string command, int arguments) + { + byte[]? lease = null; + try + { + int bytes = Encoding.ASCII.GetMaxByteCount(command.Length); + Span buffer = command.Length <= 32 ? stackalloc byte[32] : (lease = ArrayPool.Shared.Rent(bytes)); + bytes = Encoding.ASCII.GetBytes(command, buffer); + WriteHeader(RedisCommand.UNKNOWN, arguments, buffer.Slice(0, bytes)); + } + finally + { + if (lease is not null) ArrayPool.Shared.Return(lease); + } + } internal void WriteHeader(RedisCommand command, int arguments) => WriteHeader(command, arguments, _map.GetBytes(command).Span); @@ -129,7 +143,7 @@ internal void WriteHeader(RedisCommand command, int arguments, ReadOnlySpan _args; - private string? _commandUnknown; + private string _unknownCommand; - public ExecuteMessage(CommandMap? map, int db, CommandFlags flags, string command, ICollection? args) : base(db, flags, RedisCommand.UNKNOWN) + public ExecuteMessage(CommandMap? map, int db, CommandFlags flags, string command, ICollection? args) + : base(db, flags, RedisCommandMetadata.TryParseCI(command, out var value) ? value : RedisCommand.UNKNOWN) { if (args != null && args.Count >= MessageWriter.REDIS_MAX_ARGS) // using >= here because we will be adding 1 for the command itself (which is an arg for the purposes of the multi-bulk protocol) { throw ExceptionFactory.TooManyArgs(command, args.Count); } - if (map.TryGetBytes(command, out var commandBytes)) + + map ??= CommandMap.Default; + _unknownCommand = ""; + if (Command is RedisCommand.UNKNOWN) { - if (commandBytes.IsEmpty) throw ExceptionFactory.CommandDisabled(command); - _commandKnown = commandBytes; + _unknownCommand = command; } - else + else if (!map.IsAvailable(Command)) { - _commandUnknown = command; + throw ExceptionFactory.CommandDisabled(command); } - _args = args ?? Array.Empty(); } protected override void WriteImpl(in MessageWriter writer) { - if (_commandUnknown is null) + if (Command is RedisCommand.UNKNOWN) { - Debug.Assert(!_commandKnown.IsEmpty); - writer.WriteHeader(RedisCommand.UNKNOWN, _args.Count, _commandKnown); + writer.WriteHeader(_unknownCommand, _args.Count); } else { - writer.WriteHeader(RedisCommand.UNKNOWN, _args.Count, _commandUnknown); + writer.WriteHeader(Command, _args.Count); } foreach (object arg in _args) { @@ -5656,8 +5657,8 @@ protected override void WriteImpl(in MessageWriter writer) } } - public override string CommandString => CommandRaw.ToString(); - public override string CommandAndKey => CommandRaw.ToString(); + public override string CommandString => Command is RedisCommand.UNKNOWN ? _unknownCommand : Command.ToString(); + public override string CommandAndKey => CommandString; public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) { diff --git a/tests/StackExchange.Redis.Tests/CommandTests.cs b/tests/StackExchange.Redis.Tests/CommandTests.cs deleted file mode 100644 index 42df92dd1..000000000 --- a/tests/StackExchange.Redis.Tests/CommandTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Net; -using Xunit; - -namespace StackExchange.Redis.Tests; - -public class CommandTests -{ - [Fact] - public void CommandByteLength() - { - Assert.Equal(31, CommandBytes.MaxLength); - } - - [Fact] - public void CheckCommandContents() - { - for (int len = 0; len <= CommandBytes.MaxLength; len++) - { - var s = new string('A', len); - CommandBytes b = s; - Assert.Equal(len, b.Length); - - var t = b.ToString(); - Assert.Equal(s, t); - - CommandBytes b2 = t; - Assert.Equal(b, b2); - - Assert.Equal(len == 0, ReferenceEquals(s, t)); - } - } - - [Fact] - public void Basic() - { - var config = ConfigurationOptions.Parse(".,$PING=p"); - Assert.Single(config.EndPoints); - config.SetDefaultPorts(); - Assert.Contains(new DnsEndPoint(".", 6379), config.EndPoints); - var map = config.CommandMap; - Assert.Equal("$PING=P", map.ToString()); - Assert.Equal(".:6379,$PING=P", config.ToString()); - } - - [Theory] - [InlineData("redisql.CREATE_STATEMENT")] - [InlineData("INSERTINTOTABLE1STMT")] - public void CanHandleNonTrivialCommands(string command) - { - var cmd = new CommandBytes(command); - Assert.Equal(command.Length, cmd.Length); - Assert.Equal(command.ToUpperInvariant(), cmd.ToString()); - - Assert.Equal(31, CommandBytes.MaxLength); - } -} diff --git a/toys/StackExchange.Redis.Server/RedisServer.cs b/toys/StackExchange.Redis.Server/RedisServer.cs index e38177e8a..2923b1d9b 100644 --- a/toys/StackExchange.Redis.Server/RedisServer.cs +++ b/toys/StackExchange.Redis.Server/RedisServer.cs @@ -479,7 +479,7 @@ private TypedRedisValue SubscribeImpl(RedisClient client, RedisRequest request) { var reply = TypedRedisValue.Rent(3 * (request.Count - 1), out var span); - _ = RedisCommandParser.TryParse(request.Command.Span, out var cmd); + _ = RedisCommandMetadata.TryParseCI(request.Command.Span, out var cmd); var mode = cmd switch { RedisCommand.PSUBSCRIBE or RedisCommand.PUNSUBSCRIBE => RedisChannel.RedisChannelOptions.Pattern, @@ -519,9 +519,7 @@ private TypedRedisValue SubscribeImpl(RedisClient client, RedisRequest request) } return reply; } - private static readonly CommandBytes - s_Subscribe = new CommandBytes("subscribe"), - s_Unsubscribe = new CommandBytes("unsubscribe"); + private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); [RedisCommand(1, LockFree = true)] @@ -560,10 +558,4 @@ protected virtual long IncrBy(int database, RedisKey key, long delta) return value; } } - - internal static partial class RedisCommandParser - { - [AsciiHash(CaseSensitive = false)] - public static partial bool TryParse(ReadOnlySpan command, out RedisCommand value); - } } From a840329c7f5702821f0629c39c73647eb7008510 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 24 Feb 2026 23:17:53 +0000 Subject: [PATCH 3/3] optimize enum parser --- .../AsciiHashGenerator.cs | 47 +++++++++++++--- src/RESPite/PublicAPI/PublicAPI.Unshipped.txt | 6 +++ src/RESPite/Shared/AsciiHash.cs | 53 ++++++++++++++++++- .../EnumParseBenchmarks.cs | 1 + 4 files changed, 97 insertions(+), 10 deletions(-) diff --git a/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs b/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs index eae78126e..48e224019 100644 --- a/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs +++ b/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs @@ -484,13 +484,30 @@ private void BuildEnumParsers( alwaysCaseSensitive = true; } + bool twoPart = method.Members.Max(x => x.ParseText.Length) > AsciiHash.MaxBytesHashed; if (alwaysCaseSensitive) { - NewLine().Append("var hashCS = global::RESPite.AsciiHash.HashCS(").Append(method.From.Name).Append(");"); + if (twoPart) + { + NewLine().Append("global::RESPite.AsciiHash.HashCS(").Append(method.From.Name).Append(", out var cs0, out var cs1);"); + } + else + { + NewLine().Append("var cs0 = global::RESPite.AsciiHash.HashCS(").Append(method.From.Name).Append(");"); + } } else { - NewLine().Append("global::RESPite.AsciiHash.Hash(").Append(method.From.Name).Append(", out var hashCS, out var hashUC);"); + if (twoPart) + { + NewLine().Append("global::RESPite.AsciiHash.Hash(").Append(method.From.Name) + .Append(", out var cs0, out var uc0, out var cs1, out var uc1);"); + } + else + { + NewLine().Append("global::RESPite.AsciiHash.Hash(").Append(method.From.Name) + .Append(", out var cs0, out var uc0);"); + } } if (string.IsNullOrEmpty(method.CaseSensitive.Name)) @@ -544,31 +561,45 @@ void Write(bool caseSensitive) .ThenBy(x => x.ParseText)) { var len = member.ParseText.Length; - AsciiHash.Hash(member.ParseText, out var hashCS, out var hashUC); + AsciiHash.Hash(member.ParseText, out var cs0, out var uc0, out var cs1, out var uc1); bool valueCaseSensitive = caseSensitive || !HasCaseSensitiveCharacters(member.ParseText); - line = NewLine().Append(len); + line = NewLine().Append(len).Append(" when "); + if (twoPart) line.Append("("); if (valueCaseSensitive) { - line.Append(" when hashCS is ").Append(hashCS); + line.Append("cs0 is ").Append(cs0); } else { - line.Append(" when hashUC is ").Append(hashUC); + line.Append("uc0 is ").Append(uc0); } + if (len > AsciiHash.MaxBytesHashed) + { + if (valueCaseSensitive) + { + line.Append(" & cs1 is ").Append(cs1); + } + else + { + line.Append(" & uc1 is ").Append(uc1); + } + } + if (twoPart) line.Append(")"); + if (len > 2 * AsciiHash.MaxBytesHashed) { line.Append(" && "); var csValue = SyntaxFactory .LiteralExpression( SyntaxKind.StringLiteralExpression, - SyntaxFactory.Literal(member.ParseText)) + SyntaxFactory.Literal(member.ParseText.Substring(2 * AsciiHash.MaxBytesHashed))) .ToFullString(); line.Append("global::RESPite.AsciiHash.") .Append(valueCaseSensitive ? nameof(AsciiHash.SequenceEqualsCS) : nameof(AsciiHash.SequenceEqualsCI)) - .Append("(").Append(method.From.Name).Append(", ").Append(csValue); + .Append("(").Append(method.From.Name).Append(".Slice(").Append(2 * AsciiHash.MaxBytesHashed).Append("), ").Append(csValue); if (method.From.IsBytes) line.Append("u8"); line.Append(")"); } diff --git a/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt b/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt index b66923219..9208c8e31 100644 --- a/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt @@ -59,11 +59,17 @@ [SER004]static RESPite.AsciiHash.EqualsCS(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool [SER004]static RESPite.AsciiHash.EqualsCS(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool [SER004]static RESPite.AsciiHash.Hash(scoped System.ReadOnlySpan value, out long cs, out long uc) -> void +[SER004]static RESPite.AsciiHash.Hash(scoped System.ReadOnlySpan value, out long cs0, out long uc0, out long cs1, out long uc1) -> void [SER004]static RESPite.AsciiHash.Hash(scoped System.ReadOnlySpan value, out long cs, out long uc) -> void +[SER004]static RESPite.AsciiHash.Hash(scoped System.ReadOnlySpan value, out long cs0, out long uc0, out long cs1, out long uc1) -> void [SER004]static RESPite.AsciiHash.HashCS(scoped System.ReadOnlySpan value) -> long +[SER004]static RESPite.AsciiHash.HashCS(scoped System.ReadOnlySpan value, out long cs0, out long cs1) -> void [SER004]static RESPite.AsciiHash.HashCS(scoped System.ReadOnlySpan value) -> long +[SER004]static RESPite.AsciiHash.HashCS(scoped System.ReadOnlySpan value, out long cs0, out long cs1) -> void [SER004]static RESPite.AsciiHash.HashUC(scoped System.ReadOnlySpan value) -> long +[SER004]static RESPite.AsciiHash.HashUC(scoped System.ReadOnlySpan value, out long cs0, out long cs1) -> void [SER004]static RESPite.AsciiHash.HashUC(scoped System.ReadOnlySpan value) -> long +[SER004]static RESPite.AsciiHash.HashUC(scoped System.ReadOnlySpan value, out long cs0, out long cs1) -> void [SER004]static RESPite.AsciiHash.SequenceEqualsCI(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool [SER004]static RESPite.AsciiHash.SequenceEqualsCI(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool [SER004]static RESPite.AsciiHash.SequenceEqualsCS(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool diff --git a/src/RESPite/Shared/AsciiHash.cs b/src/RESPite/Shared/AsciiHash.cs index 8d4646a5c..0d5ddd858 100644 --- a/src/RESPite/Shared/AsciiHash.cs +++ b/src/RESPite/Shared/AsciiHash.cs @@ -1,5 +1,4 @@ -using System.Buffers; -using System.Buffers.Binary; +using System.Buffers.Binary; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -241,4 +240,54 @@ public static long HashCS(scoped ReadOnlySpan value) } return (long)tally; } + + public static void HashCS(scoped ReadOnlySpan value, out long cs0, out long cs1) + { + cs0 = HashCS(value); + cs1 = value.Length > MaxBytesHashed ? HashCS(value.Slice(start: MaxBytesHashed)) : 0; + } + + public static void HashCS(scoped ReadOnlySpan value, out long cs0, out long cs1) + { + cs0 = HashCS(value); + cs1 = value.Length > MaxBytesHashed ? HashCS(value.Slice(start: MaxBytesHashed)) : 0; + } + + public static void HashUC(scoped ReadOnlySpan value, out long cs0, out long cs1) + { + cs0 = HashUC(value); + cs1 = value.Length > MaxBytesHashed ? HashUC(value.Slice(start: MaxBytesHashed)) : 0; + } + + public static void HashUC(scoped ReadOnlySpan value, out long cs0, out long cs1) + { + cs0 = HashUC(value); + cs1 = value.Length > MaxBytesHashed ? HashUC(value.Slice(start: MaxBytesHashed)) : 0; + } + + public static void Hash(scoped ReadOnlySpan value, out long cs0, out long uc0, out long cs1, out long uc1) + { + Hash(value, out cs0, out uc0); + if (value.Length > MaxBytesHashed) + { + Hash(value.Slice(start: MaxBytesHashed), out cs1, out uc1); + } + else + { + cs1 = uc1 = 0; + } + } + + public static void Hash(scoped ReadOnlySpan value, out long cs0, out long uc0, out long cs1, out long uc1) + { + Hash(value, out cs0, out uc0); + if (value.Length > MaxBytesHashed) + { + Hash(value.Slice(start: MaxBytesHashed), out cs1, out uc1); + } + else + { + cs1 = uc1 = 0; + } + } } diff --git a/tests/StackExchange.Redis.Benchmarks/EnumParseBenchmarks.cs b/tests/StackExchange.Redis.Benchmarks/EnumParseBenchmarks.cs index 04280c6fc..de6ae174e 100644 --- a/tests/StackExchange.Redis.Benchmarks/EnumParseBenchmarks.cs +++ b/tests/StackExchange.Redis.Benchmarks/EnumParseBenchmarks.cs @@ -20,6 +20,7 @@ public string[] Values() => "get", "expireat", "zremrangebyscore", + "GeoRadiusByMember", ]; private byte[] _bytes = [];