From f7368c351f1b9aa1515531395dd48ddb5389c1ba Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 23 Feb 2026 18:45:01 +0000 Subject: [PATCH 1/6] server WIP --- .../PhysicalConnection.Read.cs | 9 +- .../StackExchange.Redis.Server/RedisClient.cs | 26 ++++++ .../RedisRequest.cs | 90 ++++++++++++------- .../StackExchange.Redis.Server/RedisServer.cs | 8 +- toys/StackExchange.Redis.Server/RespServer.cs | 80 ++++++----------- .../StackExchange.Redis.Server.csproj | 1 + .../TypedRedisValue.cs | 2 +- 7 files changed, 124 insertions(+), 92 deletions(-) diff --git a/src/StackExchange.Redis/PhysicalConnection.Read.cs b/src/StackExchange.Redis/PhysicalConnection.Read.cs index 929181611..8aa654bdb 100644 --- a/src/StackExchange.Redis/PhysicalConnection.Read.cs +++ b/src/StackExchange.Redis/PhysicalConnection.Read.cs @@ -27,7 +27,7 @@ private async Task ReadAllAsync(CancellationToken cancellationToken) { var tail = _ioStream ?? Stream.Null; _readStatus = ReadStatus.Init; - RespScanState state = default; + _readState = default; _readBuffer = CycleBuffer.Create(); try { @@ -49,7 +49,7 @@ private async Task ReadAllAsync(CancellationToken cancellationToken) _readStatus = ReadStatus.TryParseResult; } // another formatter glitch - while (CommitAndParseFrames(ref state, read)); + while (CommitAndParseFrames(read)); _readStatus = ReadStatus.ProcessBufferComplete; @@ -77,6 +77,8 @@ private async Task ReadAllAsync(CancellationToken cancellationToken) private static byte[]? SharedNoLease; private CycleBuffer _readBuffer; + private RespScanState _readState = default; + private long GetReadCommittedLength() { try @@ -90,12 +92,13 @@ private long GetReadCommittedLength() } } - private bool CommitAndParseFrames(ref RespScanState state, int bytesRead) + private bool CommitAndParseFrames(int bytesRead) { if (bytesRead <= 0) { return false; } + ref RespScanState state = ref _readState; // avoid a ton of ldarg0 totalBytesReceived += bytesRead; #if PARSE_DETAIL diff --git a/toys/StackExchange.Redis.Server/RedisClient.cs b/toys/StackExchange.Redis.Server/RedisClient.cs index bfe27b042..02cf99efa 100644 --- a/toys/StackExchange.Redis.Server/RedisClient.cs +++ b/toys/StackExchange.Redis.Server/RedisClient.cs @@ -1,11 +1,35 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.IO.Pipelines; +using RESPite.Buffers; +using RESPite.Messages; namespace StackExchange.Redis.Server { public sealed class RedisClient : IDisposable { + private RespScanState _readState; + + public bool TryReadRequest(ReadOnlySequence data, out long consumed) + { + // skip past data we've already read + data = data.Slice(_readState.TotalBytes); + var status = RespFrameScanner.Default.TryRead(ref _readState, data); + consumed = _readState.TotalBytes; + switch (status) + { + case OperationStatus.Done: + _readState = default; // reset ready for the next frame + return true; + case OperationStatus.NeedMoreData: + consumed = 0; + return false; + default: + throw new InvalidOperationException($"Unexpected status: {status}"); + } + } + internal int SkipReplies { get; set; } internal bool ShouldSkipResponse() { @@ -49,6 +73,8 @@ public void Dispose() try { pipe.Output.Complete(); } catch { } if (pipe is IDisposable d) try { d.Dispose(); } catch { } } + + _readState = default; } } } diff --git a/toys/StackExchange.Redis.Server/RedisRequest.cs b/toys/StackExchange.Redis.Server/RedisRequest.cs index 36d133bab..a3c92e0e5 100644 --- a/toys/StackExchange.Redis.Server/RedisRequest.cs +++ b/toys/StackExchange.Redis.Server/RedisRequest.cs @@ -1,14 +1,13 @@ using System; +using System.Buffers; +using RESPite; +using RESPite.Messages; namespace StackExchange.Redis.Server { public readonly ref struct RedisRequest { - // why ref? don't *really* need it, but: these things are "in flight" - // based on an open RawResult (which is just the detokenized ReadOnlySequence) - // so: using "ref" makes it clear that you can't expect to store these and have - // them keep working - private readonly RawResult _inner; + private readonly RespReader _rootReader; public int Count { get; } @@ -22,43 +21,74 @@ public TypedRedisValue CommandNotFound() public TypedRedisValue UnknownSubcommandOrArgumentCount() => TypedRedisValue.Error($"ERR Unknown subcommand or wrong number of arguments for '{ToString()}'."); - public string GetString(int index) - => _inner[index].GetString(); + public string GetString(int index) => GetReader(index).ReadString(); - public bool IsString(int index, string value) // TODO: optimize - => string.Equals(value, _inner[index].GetString(), StringComparison.OrdinalIgnoreCase); + [Obsolete("Use IsString(int, ReadOnlySpan{byte}) instead.")] + public bool IsString(int index, string value) + => GetReader(index).Is(value); + + public bool IsString(int index, ReadOnlySpan value) + => GetReader(index).Is(value); public override int GetHashCode() => throw new NotSupportedException(); - internal RedisRequest(scoped in RawResult result) + + /// + /// Get a reader initialized at the start of the payload. + /// + public RespReader GetReader() => _rootReader; + + /// + /// Get a reader initialized at the start of the payload. + /// + private RespReader GetReader(int childIndex) { - _inner = result; - Count = result.ItemsCount; + if (childIndex < 0 || childIndex >= Count) Throw(); + var reader = GetReader(); + reader.MoveNextAggregate(); + for (int i = 0; i < childIndex; i++) + { + reader.MoveNextScalar(); + } + reader.MoveNextScalar(); + return reader; + + static void Throw() => throw new ArgumentOutOfRangeException(nameof(childIndex)); } - public RedisValue GetValue(int index) - => _inner[index].AsRedisValue(); + internal RedisRequest(scoped in RespReader reader) + { + _rootReader = reader; + var local = reader; + if (local.TryMoveNext(checkError: false) & local.IsAggregate) + { + Count = local.AggregateLength(); + } + } - public int GetInt32(int index) - => (int)_inner[index].AsRedisValue(); + internal RedisRequest(ReadOnlySpan payload) : this(new RespReader(payload)) { } + internal RedisRequest(in ReadOnlySequence payload) : this(new RespReader(payload)) { } - public long GetInt64(int index) => (long)_inner[index].AsRedisValue(); + public RedisValue GetValue(int index) => GetReader(index).ReadRedisValue(); - public RedisKey GetKey(int index) => _inner[index].AsRedisKey(); + public int GetInt32(int index) => GetReader(index).ReadInt32(); + + public long GetInt64(int index) => GetReader(index).ReadInt64(); + + public RedisKey GetKey(int index) => GetReader(index).ReadRedisKey(); internal RedisChannel GetChannel(int index, RedisChannel.RedisChannelOptions options) - => _inner[index].AsRedisChannel(null, options); + => throw new NotImplementedException(); - internal bool TryGetCommandBytes(int i, out CommandBytes command) - { - var payload = _inner[i].Payload; - if (payload.Length > CommandBytes.MaxLength) - { - command = default; - return false; - } + internal bool TryGetCommand(int i, out RedisCommand command) + => GetReader(i).TryRead(RedisCommandParser.TryParse, out command); + } - command = payload.IsEmpty ? default : new CommandBytes(payload); - return true; - } + internal static partial class RedisCommandParser + { + [AsciiHash(CaseSensitive = false)] + public static partial bool TryParse(ReadOnlySpan value, out RedisCommand command); + + [AsciiHash(CaseSensitive = false)] + public static partial bool TryParse(ReadOnlySpan value, out RedisCommand command); } } diff --git a/toys/StackExchange.Redis.Server/RedisServer.cs b/toys/StackExchange.Redis.Server/RedisServer.cs index 52728fd44..6dad839b5 100644 --- a/toys/StackExchange.Redis.Server/RedisServer.cs +++ b/toys/StackExchange.Redis.Server/RedisServer.cs @@ -102,9 +102,9 @@ protected virtual TypedRedisValue ClientGetname(RedisClient client, RedisRequest [RedisCommand(3, "client", "reply", LockFree = true)] protected virtual TypedRedisValue ClientReply(RedisClient client, RedisRequest request) { - if (request.IsString(2, "on")) client.SkipReplies = -1; // reply to nothing - else if (request.IsString(2, "off")) client.SkipReplies = 0; // reply to everything - else if (request.IsString(2, "skip")) client.SkipReplies = 2; // this one, and the next one + if (request.IsString(2, "on"u8)) client.SkipReplies = -1; // reply to nothing + else if (request.IsString(2, "off"u8)) client.SkipReplies = 0; // reply to everything + else if (request.IsString(2, "skip"u8)) client.SkipReplies = 2; // this one, and the next one else return TypedRedisValue.Error("ERR syntax error"); return TypedRedisValue.OK; } @@ -477,7 +477,7 @@ private TypedRedisValue SubscribeImpl(RedisClient client, RedisRequest request) { var reply = TypedRedisValue.Rent(3 * (request.Count - 1), out var span); int index = 0; - request.TryGetCommandBytes(0, out var cmd); + request.TryGetCommand(0, out var cmd); var cmdString = TypedRedisValue.BulkString(cmd.ToArray()); var mode = cmd[0] == (byte)'p' ? RedisChannel.RedisChannelOptions.Pattern : RedisChannel.RedisChannelOptions.None; for (int i = 1; i < request.Count; i++) diff --git a/toys/StackExchange.Redis.Server/RespServer.cs b/toys/StackExchange.Redis.Server/RespServer.cs index bab38c0ba..5ea0ae5bb 100644 --- a/toys/StackExchange.Redis.Server/RespServer.cs +++ b/toys/StackExchange.Redis.Server/RespServer.cs @@ -10,6 +10,8 @@ using System.Threading.Tasks; using Pipelines.Sockets.Unofficial; using Pipelines.Sockets.Unofficial.Arenas; +using RESPite.Buffers; +using RESPite.Messages; namespace StackExchange.Redis.Server { @@ -30,7 +32,7 @@ protected RespServer(TextWriter output = null) _commands = BuildCommands(this); } - private static Dictionary BuildCommands(RespServer server) + private static Dictionary BuildCommands(RespServer server) { static RedisCommandAttribute CheckSignatureAndGetAttribute(MethodInfo method) { @@ -46,7 +48,7 @@ static RedisCommandAttribute CheckSignatureAndGetAttribute(MethodInfo method) select new RespCommand(attrib, method, server) into cmd group cmd by cmd.Command; - var result = new Dictionary(); + var result = new Dictionary(); foreach (var grp in grouped) { RespCommand parent; @@ -59,7 +61,11 @@ static RedisCommandAttribute CheckSignatureAndGetAttribute(MethodInfo method) { parent = grp.Single(); } - result.Add(new CommandBytes(grp.Key), parent); + + if (RedisCommandParser.TryParse(grp.Key, out var command)) + { + result.Add(command, parent); + } } return result; } @@ -96,7 +102,7 @@ public RedisCommandAttribute( public int Arity { get; } public bool LockFree { get; set; } } - private readonly Dictionary _commands; + private readonly Dictionary _commands; private readonly struct RespCommand { @@ -243,7 +249,6 @@ protected void DoShutdown(ShutdownReason reason) public void Dispose() => Dispose(true); protected virtual void Dispose(bool disposing) { - _arena.Dispose(); DoShutdown(ShutdownReason.ServerDisposed); } @@ -259,17 +264,19 @@ public async Task RunClientAsync(IDuplexPipe pipe) var readResult = await pipe.Input.ReadAsync().ConfigureAwait(false); var buffer = readResult.Buffer; - bool makingProgress = false; - while (!client.Closed && await TryProcessRequestAsync(ref buffer, client, pipe.Output).ConfigureAwait(false)) + while (!client.Closed && client.TryReadRequest(buffer, out long consumed)) { - makingProgress = true; - } - pipe.Input.AdvanceTo(buffer.Start, buffer.End); + // process a completed request + RedisRequest request = new(buffer.Slice(0, consumed)); + var response = Execute(client, request); + await WriteResponseAsync(client, pipe.Output, response); - if (!makingProgress && readResult.IsCompleted) - { // nothing to do, and nothing more will be arriving - break; + // advance the buffer to account for the message we just read + buffer = buffer.Slice(consumed); } + + pipe.Input.AdvanceTo(buffer.Start, buffer.End); + if (readResult.IsCompleted) break; // EOF } } catch (ConnectionResetException) { } @@ -369,43 +376,6 @@ static void WritePrefix(IBufferWriter output, char prefix) await output.FlushAsync().ConfigureAwait(false); } - private static bool TryParseRequest(Arena arena, ref ReadOnlySequence buffer, out RedisRequest request) - { - var reader = new BufferReader(buffer); - var raw = PhysicalConnection.TryParseResult(false, arena, in buffer, ref reader, false, null, true); - if (raw.HasValue) - { - buffer = reader.SliceFromCurrent(); - request = new RedisRequest(raw); - return true; - } - request = default; - - return false; - } - - public ValueTask TryProcessRequestAsync(ref ReadOnlySequence buffer, RedisClient client, PipeWriter output) - { - static async ValueTask Awaited(ValueTask wwrite, TypedRedisValue rresponse) - { - await wwrite; - rresponse.Recycle(); - return true; - } - if (!buffer.IsEmpty && TryParseRequest(_arena, ref buffer, out var request)) - { - TypedRedisValue response; - try { response = Execute(client, request); } - finally { _arena.Reset(); } - - var write = WriteResponseAsync(client, output, response); - if (!write.IsCompletedSuccessfully) return Awaited(write, response); - response.Recycle(); - return new ValueTask(true); - } - return new ValueTask(false); - } - protected object ServerSyncLock => this; private long _totalCommandsProcesed, _totalErrorCount; @@ -416,13 +386,13 @@ public TypedRedisValue Execute(RedisClient client, RedisRequest request) { if (request.Count == 0) return default; // not a request - if (!request.TryGetCommandBytes(0, out var cmdBytes)) return request.CommandNotFound(); - if (cmdBytes.Length == 0) return default; // not a request + if (!request.TryGetCommand(0, out var rawCommand)) return request.CommandNotFound(); + Interlocked.Increment(ref _totalCommandsProcesed); try { TypedRedisValue result; - if (_commands.TryGetValue(cmdBytes, out var cmd)) + if (_commands.TryGetValue(rawCommand, out var cmd)) { if (cmd.HasSubCommands) { @@ -475,12 +445,14 @@ public TypedRedisValue Execute(RedisClient client, RedisRequest request) } } + /* internal static string ToLower(in RawResult value) { var val = value.GetString(); if (string.IsNullOrWhiteSpace(val)) return val; return val.ToLowerInvariant(); } + */ [RedisCommand(1, LockFree = true)] protected virtual TypedRedisValue Command(RedisClient client, RedisRequest request) @@ -498,7 +470,7 @@ protected virtual TypedRedisValue CommandInfo(RedisClient client, RedisRequest r var results = TypedRedisValue.Rent(request.Count - 2, out var span); for (int i = 2; i < request.Count; i++) { - span[i - 2] = request.TryGetCommandBytes(i, out var cmdBytes) + span[i - 2] = request.TryGetCommand(i, out var cmdBytes) && _commands.TryGetValue(cmdBytes, out var cmdInfo) ? CommandInfo(cmdInfo) : TypedRedisValue.NullArray; } diff --git a/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj b/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj index 9908e9088..3bcaace9b 100644 --- a/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj +++ b/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj @@ -14,5 +14,6 @@ + diff --git a/toys/StackExchange.Redis.Server/TypedRedisValue.cs b/toys/StackExchange.Redis.Server/TypedRedisValue.cs index a67ab8d5a..35e9b20ed 100644 --- a/toys/StackExchange.Redis.Server/TypedRedisValue.cs +++ b/toys/StackExchange.Redis.Server/TypedRedisValue.cs @@ -36,7 +36,7 @@ internal static TypedRedisValue Rent(int count, out Span span) /// /// Returns whether this value represents a null array. /// - public bool IsNullArray => Type == ResultType.Array && _value.DirectObject == null; + public bool IsNullArray => Type == ResultType.Array && _value.IsNull; private readonly RedisValue _value; From 9a8e60a910762f471860e7b653ab9e2cd75b3b1c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 23 Feb 2026 19:07:41 +0000 Subject: [PATCH 2/6] WIP server --- src/StackExchange.Redis/RedisValue.cs | 37 ++++++++++++++++- .../StackExchange.Redis.Server/RedisServer.cs | 33 ++++++++++----- .../TypedRedisValue.cs | 40 +++++++++++-------- 3 files changed, 83 insertions(+), 27 deletions(-) diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index 48e7dfa4c..b351d7b0f 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -3,6 +3,7 @@ using System.Buffers.Text; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -424,6 +425,7 @@ internal enum StorageType MemoryManager, ByteArray, String, + Unknown, } internal StorageType Type @@ -438,10 +440,43 @@ internal StorageType Type if (obj is byte[]) return StorageType.ByteArray; if (obj == Sentinel_UnsignedInteger) return StorageType.UInt64; if (obj is MemoryManager) return StorageType.MemoryManager; - throw new InvalidOperationException("Unknown type"); + return StorageType.Unknown; } } + // used in the toy server only! + internal static RedisValue CreateForeign(T value, int index, int length) where T : class + { + if (typeof(T) == typeof(string) || typeof(T) == typeof(byte[])) Throw(); + return new RedisValue(value, index, length); + static void Throw() => throw new InvalidOperationException(); + } + + private RedisValue(object obj, int index, int length) + { + Unsafe.SkipInit(out this); + _index = index; + _length = length; + _obj = obj; + } + + // used in the toy server only! + internal bool TryGetForeign([NotNullWhen(true)] out T? value, out int index, out int length) + where T : class + { + if (typeof(T) != typeof(string) && typeof(T) != typeof(byte[]) && _obj is T found) + { + index = _index; + length = _length; + value = found; + return true; + } + value = null; + index = 0; + length = 0; + return false; + } + /// /// Get the size of this value in bytes. /// diff --git a/toys/StackExchange.Redis.Server/RedisServer.cs b/toys/StackExchange.Redis.Server/RedisServer.cs index 6dad839b5..dd3881dc7 100644 --- a/toys/StackExchange.Redis.Server/RedisServer.cs +++ b/toys/StackExchange.Redis.Server/RedisServer.cs @@ -478,26 +478,39 @@ private TypedRedisValue SubscribeImpl(RedisClient client, RedisRequest request) var reply = TypedRedisValue.Rent(3 * (request.Count - 1), out var span); int index = 0; request.TryGetCommand(0, out var cmd); - var cmdString = TypedRedisValue.BulkString(cmd.ToArray()); - var mode = cmd[0] == (byte)'p' ? RedisChannel.RedisChannelOptions.Pattern : RedisChannel.RedisChannelOptions.None; + var mode = cmd switch + { + RedisCommand.PSUBSCRIBE or RedisCommand.PUNSUBSCRIBE => RedisChannel.RedisChannelOptions.Pattern, + RedisCommand.SSUBSCRIBE or RedisCommand.SSUBSCRIBE => RedisChannel.RedisChannelOptions.Sharded, + _ => RedisChannel.RedisChannelOptions.None, + }; + bool add = cmd is RedisCommand.SUBSCRIBE or RedisCommand.SSUBSCRIBE or RedisCommand.PSUBSCRIBE; + + var msgKind = cmd switch + { + RedisCommand.SUBSCRIBE => "subscribe", + RedisCommand.PSUBSCRIBE => "psubscribe", + RedisCommand.SSUBSCRIBE => "ssubscribe", + RedisCommand.UNSUBSCRIBE => "unsubscribe", + RedisCommand.PUNSUBSCRIBE => "punsubscribe", + RedisCommand.SUNSUBSCRIBE => "sunsubscribe", + _ => "???", + }; + for (int i = 1; i < request.Count; i++) { var channel = request.GetChannel(i, mode); int count; - if (s_Subscribe.Equals(cmd)) + if (add) { count = client.Subscribe(channel); } - else if (s_Unsubscribe.Equals(cmd)) - { - count = client.Unsubscribe(channel); - } else { - reply.Recycle(index); - return TypedRedisValue.Nil; + count = client.Unsubscribe(channel); } - span[index++] = cmdString; + + span[index++] = TypedRedisValue.BulkString(msgKind); span[index++] = TypedRedisValue.BulkString((byte[])channel); span[index++] = TypedRedisValue.Integer(count); } diff --git a/toys/StackExchange.Redis.Server/TypedRedisValue.cs b/toys/StackExchange.Redis.Server/TypedRedisValue.cs index 35e9b20ed..0ca036486 100644 --- a/toys/StackExchange.Redis.Server/TypedRedisValue.cs +++ b/toys/StackExchange.Redis.Server/TypedRedisValue.cs @@ -18,6 +18,7 @@ internal static TypedRedisValue Rent(int count, out Span span) span = default; return EmptyArray; } + var arr = ArrayPool.Shared.Rent(count); span = new Span(arr, 0, count); return new TypedRedisValue(arr, count); @@ -74,6 +75,7 @@ public static TypedRedisValue SimpleString(string value) /// The simple string OK. /// public static TypedRedisValue OK { get; } = SimpleString("OK"); + internal static TypedRedisValue Zero { get; } = Integer(0); internal static TypedRedisValue One { get; } = Integer(1); internal static TypedRedisValue NullArray { get; } = new TypedRedisValue((TypedRedisValue[])null, 0); @@ -86,22 +88,25 @@ public ReadOnlySpan Span { get { - if (Type != ResultType.Array) return default; - var arr = (TypedRedisValue[])_value.DirectObject; - if (arr == null) return default; - var length = (int)_value.DirectOverlappedBits64; - return new ReadOnlySpan(arr, 0, length); + if (_value.TryGetForeign(out var arr, out int index, out var length)) + { + return arr.AsSpan(index, length); + } + + return default; } } + public ArraySegment Segment { get { - if (Type != ResultType.Array) return default; - var arr = (TypedRedisValue[])_value.DirectObject; - if (arr == null) return default; - var length = (int)_value.DirectOverlappedBits64; - return new ArraySegment(arr, 0, length); + if (_value.TryGetForeign(out var arr, out int index, out var length)) + { + return new(arr, index, length); + } + + return default; } } @@ -150,24 +155,27 @@ private TypedRedisValue(TypedRedisValue[] oversizedItems, int count) if (oversizedItems == null) { if (count != 0) throw new ArgumentOutOfRangeException(nameof(count)); + oversizedItems = []; } else { if (count < 0 || count > oversizedItems.Length) throw new ArgumentOutOfRangeException(nameof(count)); - if (count == 0) oversizedItems = Array.Empty(); + if (count == 0) oversizedItems = []; } - _value = new RedisValue(oversizedItems, count); + + _value = RedisValue.CreateForeign(oversizedItems, 0, count); Type = ResultType.Array; } internal void Recycle(int limit = -1) { - if (_value.DirectObject is TypedRedisValue[] arr) + if (_value.TryGetForeign(out var arr, out var index, out var length)) { - if (limit < 0) limit = (int)_value.DirectOverlappedBits64; - for (int i = 0; i < limit; i++) + if (limit < 0) limit = length; + var span = arr.AsSpan(index, limit); + foreach (var el in span) { - arr[i].Recycle(); + el.Recycle(); } ArrayPool.Shared.Return(arr, clearArray: false); } From 432ec74ae809ee40a0c320fa85535fadcae58565 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 23 Feb 2026 19:13:07 +0000 Subject: [PATCH 3/6] more WIP server --- .../StackExchange.Redis.Server/RedisServer.cs | 10 ++++----- toys/StackExchange.Redis.Server/RespServer.cs | 21 ++++++++----------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/toys/StackExchange.Redis.Server/RedisServer.cs b/toys/StackExchange.Redis.Server/RedisServer.cs index dd3881dc7..b3be7b920 100644 --- a/toys/StackExchange.Redis.Server/RedisServer.cs +++ b/toys/StackExchange.Redis.Server/RedisServer.cs @@ -88,18 +88,18 @@ protected virtual TypedRedisValue Sismember(RedisClient client, RedisRequest req protected virtual bool Sismember(int database, RedisKey key, RedisValue value) => throw new NotSupportedException(); - [RedisCommand(3, "client", "setname", LockFree = true)] + [RedisCommand(3, RedisCommand.CLIENT, "setname", LockFree = true)] protected virtual TypedRedisValue ClientSetname(RedisClient client, RedisRequest request) { client.Name = request.GetString(2); return TypedRedisValue.OK; } - [RedisCommand(2, "client", "getname", LockFree = true)] + [RedisCommand(2, RedisCommand.CLIENT, "getname", LockFree = true)] protected virtual TypedRedisValue ClientGetname(RedisClient client, RedisRequest request) => TypedRedisValue.BulkString(client.Name); - [RedisCommand(3, "client", "reply", LockFree = true)] + [RedisCommand(3, RedisCommand.CLIENT, "reply", LockFree = true)] protected virtual TypedRedisValue ClientReply(RedisClient client, RedisRequest request) { if (request.IsString(2, "on"u8)) client.SkipReplies = -1; // reply to nothing @@ -212,7 +212,7 @@ internal int CountMatch(string pattern) return count; } } - [RedisCommand(3, "config", "get", LockFree = true)] + [RedisCommand(3, RedisCommand.CONFIG, "get", LockFree = true)] protected virtual TypedRedisValue Config(RedisClient client, RedisRequest request) { var pattern = request.GetString(2); @@ -405,7 +405,7 @@ StringBuilder AddHeader() break; } } - [RedisCommand(2, "memory", "purge")] + [RedisCommand(2, RedisCommand.MEMORY, "purge")] protected virtual TypedRedisValue MemoryPurge(RedisClient client, RedisRequest request) { GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); diff --git a/toys/StackExchange.Redis.Server/RespServer.cs b/toys/StackExchange.Redis.Server/RespServer.cs index 5ea0ae5bb..401713e42 100644 --- a/toys/StackExchange.Redis.Server/RespServer.cs +++ b/toys/StackExchange.Redis.Server/RespServer.cs @@ -62,9 +62,9 @@ static RedisCommandAttribute CheckSignatureAndGetAttribute(MethodInfo method) parent = grp.Single(); } - if (RedisCommandParser.TryParse(grp.Key, out var command)) + if (grp.Key != RedisCommand.UNKNOWN) { - result.Add(command, parent); + result.Add(grp.Key, parent); } } return result; @@ -86,9 +86,9 @@ protected virtual void AppendStats(StringBuilder sb) => [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] protected sealed class RedisCommandAttribute : Attribute { - public RedisCommandAttribute( + internal RedisCommandAttribute( int arity, - string command = null, + RedisCommand command = RedisCommand.UNKNOWN, string subcommand = null) { Command = command; @@ -97,7 +97,7 @@ public RedisCommandAttribute( MaxArgs = Arity > 0 ? Arity : int.MaxValue; } public int MaxArgs { get; set; } - public string Command { get; } + internal RedisCommand Command { get; } public string SubCommand { get; } public int Arity { get; } public bool LockFree { get; set; } @@ -109,16 +109,14 @@ private readonly struct RespCommand public RespCommand(RedisCommandAttribute attrib, MethodInfo method, RespServer server) { _operation = (RespOperation)Delegate.CreateDelegate(typeof(RespOperation), server, method); - Command = (string.IsNullOrWhiteSpace(attrib.Command) ? method.Name : attrib.Command).Trim().ToLowerInvariant(); - CommandBytes = new CommandBytes(Command); + Command = attrib.Command; SubCommand = attrib.SubCommand?.Trim()?.ToLowerInvariant(); Arity = attrib.Arity; MaxArgs = attrib.MaxArgs; LockFree = attrib.LockFree; _subcommands = null; } - private CommandBytes CommandBytes { get; } - public string Command { get; } + public RedisCommand Command { get; } public string SubCommand { get; } public bool IsSubCommand => !string.IsNullOrEmpty(SubCommand); public int Arity { get; } @@ -137,7 +135,6 @@ private RespCommand(in RespCommand parent, RespCommand[] subs) if (subs == null || subs.Length == 0) throw new InvalidOperationException("Cannot add empty sub-commands"); Command = parent.Command; - CommandBytes = parent.CommandBytes; SubCommand = parent.SubCommand; Arity = parent.Arity; MaxArgs = parent.MaxArgs; @@ -464,7 +461,7 @@ protected virtual TypedRedisValue Command(RedisClient client, RedisRequest reque return results; } - [RedisCommand(-2, "command", "info", LockFree = true)] + [RedisCommand(-2, RedisCommand.COMMAND, "info", LockFree = true)] protected virtual TypedRedisValue CommandInfo(RedisClient client, RedisRequest request) { var results = TypedRedisValue.Rent(request.Count - 2, out var span); @@ -480,7 +477,7 @@ protected virtual TypedRedisValue CommandInfo(RedisClient client, RedisRequest r private TypedRedisValue CommandInfo(RespCommand command) { var arr = TypedRedisValue.Rent(6, out var span); - span[0] = TypedRedisValue.BulkString(command.Command); + span[0] = TypedRedisValue.BulkString(command.Command.ToString()); span[1] = TypedRedisValue.Integer(command.NetArity()); span[2] = TypedRedisValue.EmptyArray; span[3] = TypedRedisValue.Zero; From a6765dcec2abef725223e5587d8b97c6705dabae Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 24 Feb 2026 08:35:07 +0000 Subject: [PATCH 4/6] make ascii-hash comparable --- src/RESPite/PublicAPI/PublicAPI.Unshipped.txt | 9 ++- src/RESPite/Shared/AsciiHash.Comparers.cs | 37 +++++++++ src/RESPite/Shared/AsciiHash.cs | 75 ++++++++++++++----- .../RedisRequest.cs | 49 ++++++++---- .../StackExchange.Redis.Server/RedisServer.cs | 23 ++++-- toys/StackExchange.Redis.Server/RespServer.cs | 46 ++++++------ .../StackExchange.Redis.Server.csproj | 2 +- 7 files changed, 178 insertions(+), 63 deletions(-) create mode 100644 src/RESPite/Shared/AsciiHash.Comparers.cs diff --git a/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt b/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt index 23ec4b348..31b82cc03 100644 --- a/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt @@ -3,7 +3,9 @@ [SER004]const RESPite.Buffers.CycleBuffer.GetFullPagesOnly = -1 -> int [SER004]RESPite.AsciiHash [SER004]RESPite.AsciiHash.AsciiHash() -> void -[SER004]RESPite.AsciiHash.AsciiHash(System.ReadOnlyMemory value) -> 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.IsCI(long hash, System.ReadOnlySpan value) -> bool @@ -11,6 +13,7 @@ [SER004]RESPite.AsciiHash.IsCS(long hash, System.ReadOnlySpan value) -> bool [SER004]RESPite.AsciiHash.IsCS(System.ReadOnlySpan value) -> bool [SER004]RESPite.AsciiHash.Length.get -> int +[SER004]RESPite.AsciiHash.Span.get -> System.ReadOnlySpan [SER004]RESPite.AsciiHashAttribute [SER004]RESPite.AsciiHashAttribute.AsciiHashAttribute(string! token = "") -> void [SER004]RESPite.AsciiHashAttribute.CaseSensitive.get -> bool @@ -46,6 +49,8 @@ [SER004]RESPite.Messages.RespReader.ReadArray(ref TState state, RESPite.Messages.RespReader.Projection! projection, bool scalar = false) -> TResult[]? [SER004]RESPite.Messages.RespReader.ReadPastArray(ref TState state, RESPite.Messages.RespReader.Projection! projection, bool scalar = false) -> TResult[]? [SER004]RESPite.Messages.RespReader.ScalarParser +[SER004]static RESPite.AsciiHash.CaseInsensitiveEqualityComparer.get -> System.Collections.Generic.IEqualityComparer! +[SER004]static RESPite.AsciiHash.CaseSensitiveEqualityComparer.get -> System.Collections.Generic.IEqualityComparer! [SER004]static RESPite.AsciiHash.EqualsCI(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool [SER004]static RESPite.AsciiHash.EqualsCI(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool [SER004]static RESPite.AsciiHash.EqualsCS(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool @@ -61,6 +66,8 @@ [SER004]static RESPite.AsciiHash.SequenceEqualsCI(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool [SER004]static RESPite.AsciiHash.SequenceEqualsCS(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool [SER004]static RESPite.AsciiHash.SequenceEqualsCS(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool +[SER004]static RESPite.AsciiHash.ToLower(System.Span span) -> void +[SER004]static RESPite.AsciiHash.ToUpper(System.Span span) -> void [SER004]static RESPite.Buffers.CycleBuffer.Create(System.Buffers.MemoryPool? pool = null, int pageSize = 8192) -> RESPite.Buffers.CycleBuffer [SER004]const RESPite.Messages.RespScanState.MinBytes = 3 -> int [SER004]override RESPite.Messages.RespScanState.Equals(object? obj) -> bool diff --git a/src/RESPite/Shared/AsciiHash.Comparers.cs b/src/RESPite/Shared/AsciiHash.Comparers.cs new file mode 100644 index 000000000..de86009fc --- /dev/null +++ b/src/RESPite/Shared/AsciiHash.Comparers.cs @@ -0,0 +1,37 @@ +namespace RESPite; + +public readonly partial struct AsciiHash +{ + public static IEqualityComparer CaseSensitiveEqualityComparer => CaseSensitiveComparer.Instance; + public static IEqualityComparer CaseInsensitiveEqualityComparer => CaseInsensitiveComparer.Instance; + + private sealed class CaseSensitiveComparer : IEqualityComparer + { + private CaseSensitiveComparer() { } + public static readonly CaseSensitiveComparer Instance = new(); + + public bool Equals(AsciiHash x, AsciiHash y) + { + var len = x.Length; + return (len == y.Length & x._hashCS == y._hashCS) + && (len <= MaxBytesHashIsEqualityCS || x.Span.SequenceEqual(y.Span)); + } + + public int GetHashCode(AsciiHash obj) => obj._hashCS.GetHashCode(); + } + + private sealed class CaseInsensitiveComparer : IEqualityComparer + { + private CaseInsensitiveComparer() { } + public static readonly CaseInsensitiveComparer Instance = new(); + + public bool Equals(AsciiHash x, AsciiHash y) + { + var len = x.Length; + return (len == y.Length & x._hashLC == y._hashLC) + && (len <= MaxBytesHashIsEqualityCS || SequenceEqualsCI(x.Span, y.Span)); + } + + public int GetHashCode(AsciiHash obj) => obj._hashLC.GetHashCode(); + } +} diff --git a/src/RESPite/Shared/AsciiHash.cs b/src/RESPite/Shared/AsciiHash.cs index 571f4a239..d2f4d7e19 100644 --- a/src/RESPite/Shared/AsciiHash.cs +++ b/src/RESPite/Shared/AsciiHash.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +using System.Text; namespace RESPite; @@ -34,26 +35,66 @@ public sealed class AsciiHashAttribute(string token = "") : Attribute } [Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] -public readonly struct AsciiHash +public readonly partial struct AsciiHash { - private readonly long _hashCI; - private readonly long _hashCS; - private readonly ReadOnlyMemory _value; - public int Length => _value.Length; + // ReSharper disable InconsistentNaming + private readonly long _hashCI, _hashCS, _hashLC; + // ReSharper restore InconsistentNaming + private readonly int _index, _length; + private readonly byte[] _arr; + + public int Length => _length; /// /// The optimal buffer length (with padding) to use for this value. /// public int BufferLength => (Length + 1 + 7) & ~7; // an extra byte, then round up to word-size - public AsciiHash(ReadOnlySpan value) : this((ReadOnlyMemory)value.ToArray()) { } + public ReadOnlySpan Span => new(_arr ?? [], _index, _length); + + public AsciiHash(ReadOnlySpan value) : this(value.ToArray(), 0, value.Length) { } + public AsciiHash(string value) : this(Encoding.ASCII.GetBytes(value)) { } + + public AsciiHash(byte[] arr) : this(arr, 0, -1) { } - public AsciiHash(ReadOnlyMemory value) + public AsciiHash(byte[] arr, int index, int length) { - _value = value; - var span = value.Span; - _hashCI = HashCI(span); - _hashCS = HashCS(span); + _arr = arr ?? []; + _index = index; + _length = length < 0 ? (_arr.Length - index) : length; + + var span = new ReadOnlySpan(_arr, _index, _length); + Hash(span, out _hashCS, out _hashCI); + + // pre-compute the lower-case hash + Span buffer = stackalloc byte[8]; + BinaryPrimitives.WriteInt64LittleEndian(buffer, _hashCS); + ToLower(buffer); + _hashLC = BinaryPrimitives.ReadInt64LittleEndian(buffer); + } + + /// + /// In-place ASCII upper-case conversion. + /// + public static void ToUpper(Span span) + { + foreach (ref var b in span) + { + if (b >= 'a' && b <= 'z') + b = (byte)(b & ~0x20); + } + } + + /// + /// In-place ASCII lower-case conversion. + /// + public static void ToLower(Span span) + { + foreach (ref var b in span) + { + if (b >= 'a' && b <= 'z') + b |= (byte)(b & ~0x20); + } } private const long CaseMask = ~0x2020202020202020; @@ -62,19 +103,19 @@ public AsciiHash(ReadOnlyMemory 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); + var len = _length; + if (hash != _hashCS | value.Length != len) return false; + return len <= MaxBytesHashIsEqualityCS || EqualsCS(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; + var len = _length; + if (hash != _hashCI | value.Length != len) return false; if (len <= MaxBytesHashIsEqualityCS && HashCS(value) == _hashCS) return true; - return EqualsCI(_value.Span, value); + return EqualsCI(Span, value); } public static long HashCS(in ReadOnlySequence value) diff --git a/toys/StackExchange.Redis.Server/RedisRequest.cs b/toys/StackExchange.Redis.Server/RedisRequest.cs index a3c92e0e5..9584bc353 100644 --- a/toys/StackExchange.Redis.Server/RedisRequest.cs +++ b/toys/StackExchange.Redis.Server/RedisRequest.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Diagnostics; using RESPite; using RESPite.Messages; @@ -55,7 +56,7 @@ private RespReader GetReader(int childIndex) static void Throw() => throw new ArgumentOutOfRangeException(nameof(childIndex)); } - internal RedisRequest(scoped in RespReader reader) + internal RedisRequest(scoped in RespReader reader, ref byte[] commandLease) { _rootReader = reader; var local = reader; @@ -63,10 +64,40 @@ internal RedisRequest(scoped in RespReader reader) { Count = local.AggregateLength(); } + + if (Count == 0) + { + Command = s_EmptyCommand; + } + else + { + local.MoveNextScalar(); + var len = local.ScalarLength(); + if (len > commandLease.Length) + { + ArrayPool.Shared.Return(commandLease); + commandLease = ArrayPool.Shared.Rent(len); + } + var readBytes = local.CopyTo(commandLease); + Debug.Assert(readBytes == len); + AsciiHash.ToLower(commandLease.AsSpan(0, readBytes)); + // note we retain the lease array in the Command, this is intentional + Command = new(commandLease, 0, readBytes); + } + } + + internal static byte[] GetLease() => ArrayPool.Shared.Rent(16); + internal static void ReleaseLease(ref byte[] commandLease) + { + ArrayPool.Shared.Return(commandLease); + commandLease = []; } - internal RedisRequest(ReadOnlySpan payload) : this(new RespReader(payload)) { } - internal RedisRequest(in ReadOnlySequence payload) : this(new RespReader(payload)) { } + private static readonly AsciiHash s_EmptyCommand = new(Array.Empty()); + public readonly AsciiHash Command; + + internal RedisRequest(ReadOnlySpan payload, ref byte[] commandLease) : this(new RespReader(payload), ref commandLease) { } + internal RedisRequest(in ReadOnlySequence payload, ref byte[] commandLease) : this(new RespReader(payload), ref commandLease) { } public RedisValue GetValue(int index) => GetReader(index).ReadRedisValue(); @@ -78,17 +109,5 @@ internal RedisRequest(in ReadOnlySequence payload) : this(new RespReader(p internal RedisChannel GetChannel(int index, RedisChannel.RedisChannelOptions options) => throw new NotImplementedException(); - - internal bool TryGetCommand(int i, out RedisCommand command) - => GetReader(i).TryRead(RedisCommandParser.TryParse, out command); - } - - internal static partial class RedisCommandParser - { - [AsciiHash(CaseSensitive = false)] - public static partial bool TryParse(ReadOnlySpan value, out RedisCommand command); - - [AsciiHash(CaseSensitive = false)] - public static partial bool TryParse(ReadOnlySpan value, out RedisCommand command); } } diff --git a/toys/StackExchange.Redis.Server/RedisServer.cs b/toys/StackExchange.Redis.Server/RedisServer.cs index b3be7b920..e38177e8a 100644 --- a/toys/StackExchange.Redis.Server/RedisServer.cs +++ b/toys/StackExchange.Redis.Server/RedisServer.cs @@ -3,6 +3,8 @@ using System.Diagnostics; using System.IO; using System.Text; +using RESPite; +using StackExchange.Redis; namespace StackExchange.Redis.Server { @@ -88,18 +90,18 @@ protected virtual TypedRedisValue Sismember(RedisClient client, RedisRequest req protected virtual bool Sismember(int database, RedisKey key, RedisValue value) => throw new NotSupportedException(); - [RedisCommand(3, RedisCommand.CLIENT, "setname", LockFree = true)] + [RedisCommand(3, nameof(RedisCommand.CLIENT), "setname", LockFree = true)] protected virtual TypedRedisValue ClientSetname(RedisClient client, RedisRequest request) { client.Name = request.GetString(2); return TypedRedisValue.OK; } - [RedisCommand(2, RedisCommand.CLIENT, "getname", LockFree = true)] + [RedisCommand(2, nameof(RedisCommand.CLIENT), "getname", LockFree = true)] protected virtual TypedRedisValue ClientGetname(RedisClient client, RedisRequest request) => TypedRedisValue.BulkString(client.Name); - [RedisCommand(3, RedisCommand.CLIENT, "reply", LockFree = true)] + [RedisCommand(3, nameof(RedisCommand.CLIENT), "reply", LockFree = true)] protected virtual TypedRedisValue ClientReply(RedisClient client, RedisRequest request) { if (request.IsString(2, "on"u8)) client.SkipReplies = -1; // reply to nothing @@ -212,7 +214,7 @@ internal int CountMatch(string pattern) return count; } } - [RedisCommand(3, RedisCommand.CONFIG, "get", LockFree = true)] + [RedisCommand(3, nameof(RedisCommand.CONFIG), "get", LockFree = true)] protected virtual TypedRedisValue Config(RedisClient client, RedisRequest request) { var pattern = request.GetString(2); @@ -405,7 +407,7 @@ StringBuilder AddHeader() break; } } - [RedisCommand(2, RedisCommand.MEMORY, "purge")] + [RedisCommand(2, nameof(RedisCommand.MEMORY), "purge")] protected virtual TypedRedisValue MemoryPurge(RedisClient client, RedisRequest request) { GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); @@ -476,8 +478,8 @@ protected virtual TypedRedisValue Unsubscribe(RedisClient client, RedisRequest r private TypedRedisValue SubscribeImpl(RedisClient client, RedisRequest request) { var reply = TypedRedisValue.Rent(3 * (request.Count - 1), out var span); - int index = 0; - request.TryGetCommand(0, out var cmd); + + _ = RedisCommandParser.TryParse(request.Command.Span, out var cmd); var mode = cmd switch { RedisCommand.PSUBSCRIBE or RedisCommand.PUNSUBSCRIBE => RedisChannel.RedisChannelOptions.Pattern, @@ -497,6 +499,7 @@ private TypedRedisValue SubscribeImpl(RedisClient client, RedisRequest request) _ => "???", }; + int index = 0; for (int i = 1; i < request.Count; i++) { var channel = request.GetChannel(i, mode); @@ -557,4 +560,10 @@ 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); + } } diff --git a/toys/StackExchange.Redis.Server/RespServer.cs b/toys/StackExchange.Redis.Server/RespServer.cs index 401713e42..641c4749a 100644 --- a/toys/StackExchange.Redis.Server/RespServer.cs +++ b/toys/StackExchange.Redis.Server/RespServer.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Pipelines.Sockets.Unofficial; using Pipelines.Sockets.Unofficial.Arenas; +using RESPite; using RESPite.Buffers; using RESPite.Messages; @@ -32,7 +33,7 @@ protected RespServer(TextWriter output = null) _commands = BuildCommands(this); } - private static Dictionary BuildCommands(RespServer server) + private static Dictionary BuildCommands(RespServer server) { static RedisCommandAttribute CheckSignatureAndGetAttribute(MethodInfo method) { @@ -42,13 +43,16 @@ static RedisCommandAttribute CheckSignatureAndGetAttribute(MethodInfo method) return null; return (RedisCommandAttribute)Attribute.GetCustomAttribute(method, typeof(RedisCommandAttribute)); } - var grouped = from method in server.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - let attrib = CheckSignatureAndGetAttribute(method) - where attrib != null - select new RespCommand(attrib, method, server) into cmd - group cmd by cmd.Command; - var result = new Dictionary(); + var grouped = ( + from method in server.GetType() + .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + let attrib = CheckSignatureAndGetAttribute(method) + where attrib != null + select new RespCommand(attrib, method, server)) + .GroupBy(x => new AsciiHash(x.Command.ToLowerInvariant()), AsciiHash.CaseSensitiveEqualityComparer); + + var result = new Dictionary(AsciiHash.CaseSensitiveEqualityComparer); foreach (var grp in grouped) { RespCommand parent; @@ -62,10 +66,7 @@ static RedisCommandAttribute CheckSignatureAndGetAttribute(MethodInfo method) parent = grp.Single(); } - if (grp.Key != RedisCommand.UNKNOWN) - { - result.Add(grp.Key, parent); - } + result.Add(grp.Key, parent); } return result; } @@ -86,9 +87,9 @@ protected virtual void AppendStats(StringBuilder sb) => [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] protected sealed class RedisCommandAttribute : Attribute { - internal RedisCommandAttribute( + public RedisCommandAttribute( int arity, - RedisCommand command = RedisCommand.UNKNOWN, + string command = null, string subcommand = null) { Command = command; @@ -97,12 +98,12 @@ internal RedisCommandAttribute( MaxArgs = Arity > 0 ? Arity : int.MaxValue; } public int MaxArgs { get; set; } - internal RedisCommand Command { get; } + public string Command { get; } public string SubCommand { get; } public int Arity { get; } public bool LockFree { get; set; } } - private readonly Dictionary _commands; + private readonly Dictionary _commands; private readonly struct RespCommand { @@ -116,7 +117,7 @@ public RespCommand(RedisCommandAttribute attrib, MethodInfo method, RespServer s LockFree = attrib.LockFree; _subcommands = null; } - public RedisCommand Command { get; } + public string Command { get; } public string SubCommand { get; } public bool IsSubCommand => !string.IsNullOrEmpty(SubCommand); public int Arity { get; } @@ -253,6 +254,7 @@ public async Task RunClientAsync(IDuplexPipe pipe) { Exception fault = null; RedisClient client = null; + byte[] commandLease = RedisRequest.GetLease(); try { client = AddClient(); @@ -264,7 +266,7 @@ public async Task RunClientAsync(IDuplexPipe pipe) while (!client.Closed && client.TryReadRequest(buffer, out long consumed)) { // process a completed request - RedisRequest request = new(buffer.Slice(0, consumed)); + RedisRequest request = new(buffer.Slice(0, consumed), ref commandLease); var response = Execute(client, request); await WriteResponseAsync(client, pipe.Output, response); @@ -289,6 +291,7 @@ public async Task RunClientAsync(IDuplexPipe pipe) } finally { + RedisRequest.ReleaseLease(ref commandLease); RemoveClient(client); try { pipe.Input.Complete(fault); } catch { } try { pipe.Output.Complete(fault); } catch { } @@ -383,13 +386,13 @@ public TypedRedisValue Execute(RedisClient client, RedisRequest request) { if (request.Count == 0) return default; // not a request - if (!request.TryGetCommand(0, out var rawCommand)) return request.CommandNotFound(); + if (request.Command.Length == 0) return request.CommandNotFound(); Interlocked.Increment(ref _totalCommandsProcesed); try { TypedRedisValue result; - if (_commands.TryGetValue(rawCommand, out var cmd)) + if (_commands.TryGetValue(request.Command, out var cmd)) { if (cmd.HasSubCommands) { @@ -461,14 +464,13 @@ protected virtual TypedRedisValue Command(RedisClient client, RedisRequest reque return results; } - [RedisCommand(-2, RedisCommand.COMMAND, "info", LockFree = true)] + [RedisCommand(-2, nameof(RedisCommand.COMMAND), "info", LockFree = true)] protected virtual TypedRedisValue CommandInfo(RedisClient client, RedisRequest request) { var results = TypedRedisValue.Rent(request.Count - 2, out var span); for (int i = 2; i < request.Count; i++) { - span[i - 2] = request.TryGetCommand(i, out var cmdBytes) - && _commands.TryGetValue(cmdBytes, out var cmdInfo) + span[i - 2] = _commands.TryGetValue(request.Command, out var cmdInfo) ? CommandInfo(cmdInfo) : TypedRedisValue.NullArray; } return results; diff --git a/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj b/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj index 3bcaace9b..bb61c6616 100644 --- a/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj +++ b/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + netstandard2.0;net8.0 Basic redis server based on StackExchange.Redis StackExchange.Redis StackExchange.Redis.Server From 23db1ea179f51c86dee55d96b07766bd3ff918bd Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 24 Feb 2026 08:41:11 +0000 Subject: [PATCH 5/6] make AsciiHash equatable --- src/RESPite/PublicAPI/PublicAPI.Unshipped.txt | 4 ++++ src/RESPite/Shared/AsciiHash.cs | 20 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt b/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt index 31b82cc03..0c3bf87be 100644 --- a/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt @@ -1,6 +1,9 @@ #nullable enable [SER004]const RESPite.Buffers.CycleBuffer.GetAnything = 0 -> int [SER004]const RESPite.Buffers.CycleBuffer.GetFullPagesOnly = -1 -> int +[SER004]override RESPite.AsciiHash.Equals(object? other) -> bool +[SER004]override RESPite.AsciiHash.GetHashCode() -> int +[SER004]override RESPite.AsciiHash.ToString() -> string! [SER004]RESPite.AsciiHash [SER004]RESPite.AsciiHash.AsciiHash() -> void [SER004]RESPite.AsciiHash.AsciiHash(byte[]! arr) -> void @@ -8,6 +11,7 @@ [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 [SER004]RESPite.AsciiHash.IsCI(long hash, System.ReadOnlySpan value) -> bool [SER004]RESPite.AsciiHash.IsCI(System.ReadOnlySpan value) -> bool [SER004]RESPite.AsciiHash.IsCS(long hash, System.ReadOnlySpan value) -> bool diff --git a/src/RESPite/Shared/AsciiHash.cs b/src/RESPite/Shared/AsciiHash.cs index d2f4d7e19..7d3f32ca3 100644 --- a/src/RESPite/Shared/AsciiHash.cs +++ b/src/RESPite/Shared/AsciiHash.cs @@ -35,7 +35,7 @@ public sealed class AsciiHashAttribute(string token = "") : Attribute } [Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] -public readonly partial struct AsciiHash +public readonly partial struct AsciiHash : IEquatable { // ReSharper disable InconsistentNaming private readonly long _hashCI, _hashCS, _hashLC; @@ -55,6 +55,24 @@ public readonly partial struct AsciiHash public AsciiHash(ReadOnlySpan value) : this(value.ToArray(), 0, value.Length) { } public AsciiHash(string value) : this(Encoding.ASCII.GetBytes(value)) { } + /// + public override int GetHashCode() => _hashCS.GetHashCode(); + + /// + public override string ToString() => _length == 0 ? "" : Encoding.ASCII.GetString(_arr, _index, _length); + + /// + public override bool Equals(object? other) => other is AsciiHash hash && Equals(hash); + + /// + public bool Equals(in AsciiHash other) + { + return (_length == other.Length & _hashCS == other._hashCS) + && (_length <= MaxBytesHashIsEqualityCS || Span.SequenceEqual(other.Span)); + } + + bool IEquatable.Equals(AsciiHash other) => Equals(other); + public AsciiHash(byte[] arr) : this(arr, 0, -1) { } public AsciiHash(byte[] arr, int index, int length) From 8b3e6faf66c2b4a823eef1e05df977c3bff04ff2 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 24 Feb 2026 11:02:22 +0000 Subject: [PATCH 6/6] optimize case-insensitive performance --- eng/StackExchange.Redis.Build/AsciiHash.md | 22 +- .../AsciiHashGenerator.cs | 64 ++--- src/RESPite/PublicAPI/PublicAPI.Unshipped.txt | 11 +- src/RESPite/Shared/AsciiHash.Comparers.cs | 8 +- src/RESPite/Shared/AsciiHash.Instance.cs | 72 ++++++ src/RESPite/Shared/AsciiHash.cs | 219 ++++-------------- .../Configuration/LoggingTunnel.cs | 10 +- .../HotKeys.ResultProcessor.cs | 44 ++-- src/StackExchange.Redis/KeyNotification.cs | 12 +- .../KeyNotificationTypeAsciiHash.cs | 112 ++++----- .../PhysicalConnection.Read.cs | 22 +- .../ResultProcessor.VectorSets.cs | 26 +-- src/StackExchange.Redis/ResultProcessor.cs | 20 +- .../AsciiHashBenchmarks.cs | 4 + .../AsciiHashSwitch.cs | 4 +- .../StackExchange.Redis.Benchmarks/Program.cs | 2 +- ...sciiHashTests.cs => AsciiHashUnitTests.cs} | 197 ++++++++-------- .../RedisRequest.cs | 2 +- toys/StackExchange.Redis.Server/RespServer.cs | 2 +- 19 files changed, 381 insertions(+), 472 deletions(-) create mode 100644 src/RESPite/Shared/AsciiHash.Instance.cs rename tests/StackExchange.Redis.Tests/{AsciiHashTests.cs => AsciiHashUnitTests.cs} (69%) diff --git a/eng/StackExchange.Redis.Build/AsciiHash.md b/eng/StackExchange.Redis.Build/AsciiHash.md index 921745e9d..4a76ded62 100644 --- a/eng/StackExchange.Redis.Build/AsciiHash.md +++ b/eng/StackExchange.Redis.Build/AsciiHash.md @@ -28,14 +28,15 @@ static partial class bin { public const int Length = 3; public const long HashCS = ... - public const long HashCI = ... + public const long HashUC = ... public static ReadOnlySpan U8 => @"bin"u8; public static string Text => @"bin"; - public static bool IsCI(long hash, in RawResult value) => ... - public static bool IsCS(long hash, in ReadOnlySpan value) => ... + public static bool IsCS(in ReadOnlySpan value, long cs) => ... + public static bool IsCI(in RawResult value, long uc) => ... + } ``` -The `CS` and `CI` are case-sensitive and case-insensitive tools, respectively. +The `CS` and `UC` are case-sensitive and case-insensitive (using upper-case) tools, respectively. (this API is strictly an internal implementation detail, and can change at any time) @@ -46,10 +47,10 @@ var key = ... var hash = key.HashCS(); switch (key.Length) { - case bin.Length when bin.Is(hash, key): + case bin.Length when bin.Is(key, hash): // handle bin break; - case f32.Length when f32.Is(hash, key): + case f32.Length when f32.Is(key, hash): // handle f32 break; } @@ -57,7 +58,7 @@ switch (key.Length) The switch on the `Length` is optional, but recommended - these low values can often be implemented (by the compiler) as a simple jump-table, which is very fast. However, switching on the hash itself is also valid. All hash matches -must also perform a sequence equality check - the `Is(hash, value)` convenience method validates both hash and equality. +must also perform a sequence equality check - the `Is(value, hash)` convenience method validates both hash and equality. Note that `switch` requires `const` values, hence why we use generated *types* rather than partial-properties that emit an instance with the known values. Also, the `"..."u8` syntax emits a span which is awkward to store, but @@ -81,6 +82,13 @@ Now, `bin.Hash` can be supplied to a caller that takes an `AsciiHash` instance ( which then has *instance* methods for case-sensitive and case-insensitive matching; the instance already knows the target hash and payload values. +The `AsciiHash` returned implements `IEquatable` implementing case-sensitive equality; there are +also independent case-sensitive and case-insensitive comparers available via the static +`CaseSensitiveEqualityComparer` and `CaseInsensitiveEqualityComparer` properties respectively. + +Comparison values can be constructed on the fly on top of transient buffers using the constructors **that take +arrays**. Note that the other constructors may allocate on a per-usage basis. + ## Enum parsing (part 1) When identifying multiple values, an `enum` may be more convenient. Consider: diff --git a/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs b/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs index 664040bb1..eae78126e 100644 --- a/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs +++ b/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs @@ -490,7 +490,7 @@ private void BuildEnumParsers( } else { - NewLine().Append("global::RESPite.AsciiHash.Hash(").Append(method.From.Name).Append(", out var hashCS, out var hashCI);"); + NewLine().Append("global::RESPite.AsciiHash.Hash(").Append(method.From.Name).Append(", out var hashCS, out var hashUC);"); } if (string.IsNullOrEmpty(method.CaseSensitive.Name)) @@ -544,7 +544,7 @@ void Write(bool caseSensitive) .ThenBy(x => x.ParseText)) { var len = member.ParseText.Length; - AsciiHash.Hash(member.ParseText, out var hashCS, out var hashCI); + AsciiHash.Hash(member.ParseText, out var hashCS, out var hashUC); bool valueCaseSensitive = caseSensitive || !HasCaseSensitiveCharacters(member.ParseText); @@ -552,60 +552,28 @@ void Write(bool caseSensitive) if (valueCaseSensitive) { line.Append(" when hashCS is ").Append(hashCS); - if (len > AsciiHash.MaxBytesHashIsEqualityCS) - { - line.Append(" && "); - WriteValueTest(member.ParseText, true); - } } else { - // optimize for "all_lower" or "ALL_UPPER" matches; "Mixed_Match" comes last - var ucText = member.ParseText.ToUpperInvariant(); - var lcText = member.ParseText.ToLowerInvariant(); - long hashUC = AsciiHash.HashCS(ucText), hashLC = AsciiHash.HashCS(lcText); - - if (len <= AsciiHash.MaxBytesHashIsEqualityCS) - { - // note we know the lc and uc hash must be different - line.Append(" when (hashCS is ").Append(hashUC).Append(" or ").Append(hashLC) - .Append(") || (hashCI is ").Append(hashCI).Append(" && "); - WriteValueTest(member.ParseText, false); - line.Append(")"); - } - else if (hashLC == hashCS && hashUC == hashCS) - { - // there are alphas, but not in the hashed portion - line.Append(" when hashCS is ").Append(hashLC).Append(" && "); - WriteValueTest(member.ParseText, false); - } - else - { - line.Append(" when (hashCS is ").Append(hashLC).Append(" && "); - WriteValueTest(lcText, true); - line.Append(") || (hashCS is ").Append(hashUC).Append(" && "); - WriteValueTest(ucText, true); - line.Append(") || (hashCI is ").Append(hashCI).Append(" && "); - WriteValueTest(member.ParseText, false); - line.Append(")"); - } + line.Append(" when hashUC is ").Append(hashUC); } - line.Append(" => ").Append(method.To.Type).Append(".").Append(member.EnumMember).Append(","); - - void WriteValueTest(string value, bool testCS) + if (len > AsciiHash.MaxBytesHashed) { + line.Append(" && "); var csValue = SyntaxFactory .LiteralExpression( SyntaxKind.StringLiteralExpression, - SyntaxFactory.Literal(value)) + SyntaxFactory.Literal(member.ParseText)) .ToFullString(); line.Append("global::RESPite.AsciiHash.") - .Append(testCS ? nameof(AsciiHash.SequenceEqualsCS) : nameof(AsciiHash.SequenceEqualsCI)) + .Append(valueCaseSensitive ? nameof(AsciiHash.SequenceEqualsCS) : nameof(AsciiHash.SequenceEqualsCI)) .Append("(").Append(method.From.Name).Append(", ").Append(csValue); if (method.From.IsBytes) line.Append("u8"); line.Append(")"); } + + line.Append(" => ").Append(method.To.Type).Append(".").Append(member.EnumMember).Append(","); } NewLine().Append("_ => (").Append(method.To.Type).Append(")").Append(method.DefaultValue) @@ -717,29 +685,29 @@ private static void BuildTypeImplementations( .LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(literal.Value)) .ToFullString(); - AsciiHash.Hash(literal.Value, out var hashCS, out var hashCI); + AsciiHash.Hash(literal.Value, out var hashCS, out var hashUC); NewLine().Append("static partial class ").Append(literal.Name); NewLine().Append("{"); indent++; NewLine().Append("public const int Length = ").Append(literal.Value.Length).Append(';'); NewLine().Append("public const long HashCS = ").Append(hashCS).Append(';'); - NewLine().Append("public const long HashCI = ").Append(hashCI).Append(';'); + NewLine().Append("public const long HashUC = ").Append(hashUC).Append(';'); NewLine().Append("public static ReadOnlySpan U8 => ").Append(csValue).Append("u8;"); NewLine().Append("public const string Text = ").Append(csValue).Append(';'); - if (literal.Value.Length <= AsciiHash.MaxBytesHashIsEqualityCS) + if (literal.Value.Length <= AsciiHash.MaxBytesHashed) { // the case-sensitive hash enforces all the values NewLine().Append( - "public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS & value.Length == Length;"); + "public static bool IsCS(ReadOnlySpan value, long cs) => cs == HashCS & value.Length == Length;"); NewLine().Append( - "public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && (global::RESPite.AsciiHash.HashCS(value) == HashCS || global::RESPite.AsciiHash.EqualsCI(value, U8));"); + "public static bool IsCI(ReadOnlySpan value, long uc) => uc == HashUC & value.Length == Length;"); } else { NewLine().Append( - "public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8);"); + "public static bool IsCS(ReadOnlySpan value, long cs) => cs == HashCS && value.SequenceEqual(U8);"); NewLine().Append( - "public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8);"); + "public static bool IsCI(ReadOnlySpan value, long uc) => uc == HashUC && global::RESPite.AsciiHash.SequenceEqualsCI(value, U8);"); } indent--; diff --git a/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt b/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt index 0c3bf87be..9173b3c85 100644 --- a/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt @@ -12,9 +12,7 @@ [SER004]RESPite.AsciiHash.AsciiHash(System.ReadOnlySpan value) -> void [SER004]RESPite.AsciiHash.BufferLength.get -> int [SER004]RESPite.AsciiHash.Equals(in RESPite.AsciiHash other) -> bool -[SER004]RESPite.AsciiHash.IsCI(long hash, System.ReadOnlySpan value) -> bool [SER004]RESPite.AsciiHash.IsCI(System.ReadOnlySpan value) -> bool -[SER004]RESPite.AsciiHash.IsCS(long hash, System.ReadOnlySpan value) -> bool [SER004]RESPite.AsciiHash.IsCS(System.ReadOnlySpan value) -> bool [SER004]RESPite.AsciiHash.Length.get -> int [SER004]RESPite.AsciiHash.Span.get -> System.ReadOnlySpan @@ -59,13 +57,12 @@ [SER004]static RESPite.AsciiHash.EqualsCI(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool [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 ci) -> void -[SER004]static RESPite.AsciiHash.Hash(scoped System.ReadOnlySpan value, out long cs, out long ci) -> void -[SER004]static RESPite.AsciiHash.HashCI(scoped System.ReadOnlySpan value) -> long -[SER004]static RESPite.AsciiHash.HashCI(scoped System.ReadOnlySpan value) -> long -[SER004]static RESPite.AsciiHash.HashCS(in System.Buffers.ReadOnlySequence value) -> long +[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 cs, out long uc) -> void [SER004]static RESPite.AsciiHash.HashCS(scoped System.ReadOnlySpan value) -> long [SER004]static RESPite.AsciiHash.HashCS(scoped System.ReadOnlySpan value) -> long +[SER004]static RESPite.AsciiHash.HashUC(scoped System.ReadOnlySpan value) -> long +[SER004]static RESPite.AsciiHash.HashUC(scoped System.ReadOnlySpan value) -> long [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.Comparers.cs b/src/RESPite/Shared/AsciiHash.Comparers.cs index de86009fc..7b69a15a4 100644 --- a/src/RESPite/Shared/AsciiHash.Comparers.cs +++ b/src/RESPite/Shared/AsciiHash.Comparers.cs @@ -14,7 +14,7 @@ public bool Equals(AsciiHash x, AsciiHash y) { var len = x.Length; return (len == y.Length & x._hashCS == y._hashCS) - && (len <= MaxBytesHashIsEqualityCS || x.Span.SequenceEqual(y.Span)); + && (len <= MaxBytesHashed || x.Span.SequenceEqual(y.Span)); } public int GetHashCode(AsciiHash obj) => obj._hashCS.GetHashCode(); @@ -28,10 +28,10 @@ private CaseInsensitiveComparer() { } public bool Equals(AsciiHash x, AsciiHash y) { var len = x.Length; - return (len == y.Length & x._hashLC == y._hashLC) - && (len <= MaxBytesHashIsEqualityCS || SequenceEqualsCI(x.Span, y.Span)); + return (len == y.Length & x._hashUC == y._hashUC) + && (len <= MaxBytesHashed || SequenceEqualsCI(x.Span, y.Span)); } - public int GetHashCode(AsciiHash obj) => obj._hashLC.GetHashCode(); + public int GetHashCode(AsciiHash obj) => obj._hashUC.GetHashCode(); } } diff --git a/src/RESPite/Shared/AsciiHash.Instance.cs b/src/RESPite/Shared/AsciiHash.Instance.cs new file mode 100644 index 000000000..704c78ee6 --- /dev/null +++ b/src/RESPite/Shared/AsciiHash.Instance.cs @@ -0,0 +1,72 @@ +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace RESPite; + +public readonly partial struct AsciiHash : IEquatable +{ + // ReSharper disable InconsistentNaming + private readonly long _hashCS, _hashUC; + // ReSharper restore InconsistentNaming + private readonly int _index, _length; + private readonly byte[] _arr; + + public int Length => _length; + + /// + /// The optimal buffer length (with padding) to use for this value. + /// + public int BufferLength => (Length + 1 + 7) & ~7; // an extra byte, then round up to word-size + + public ReadOnlySpan Span => new(_arr ?? [], _index, _length); + + public AsciiHash(ReadOnlySpan value) : this(value.ToArray(), 0, value.Length) { } + public AsciiHash(string value) : this(Encoding.ASCII.GetBytes(value)) { } + + /// + public override int GetHashCode() => _hashCS.GetHashCode(); + + /// + public override string ToString() => _length == 0 ? "" : Encoding.ASCII.GetString(_arr, _index, _length); + + /// + public override bool Equals(object? other) => other is AsciiHash hash && Equals(hash); + + /// + public bool Equals(in AsciiHash other) + { + return (_length == other.Length & _hashCS == other._hashCS) + && (_length <= MaxBytesHashed || Span.SequenceEqual(other.Span)); + } + + bool IEquatable.Equals(AsciiHash other) => Equals(other); + + public AsciiHash(byte[] arr) : this(arr, 0, -1) { } + + public AsciiHash(byte[] arr, int index, int length) + { + _arr = arr ?? []; + _index = index; + _length = length < 0 ? (_arr.Length - index) : length; + + var span = new ReadOnlySpan(_arr, _index, _length); + Hash(span, out _hashCS, out _hashUC); + } + + public bool IsCS(ReadOnlySpan value) + { + var cs = HashCS(value); + var len = _length; + if (cs != _hashCS | value.Length != len) return false; + return len <= MaxBytesHashed || Span.SequenceEqual(value); + } + + public bool IsCI(ReadOnlySpan value) + { + var uc = HashUC(value); + var len = _length; + if (uc != _hashUC | value.Length != len) return false; + return len <= MaxBytesHashed || SequenceEqualsCI(Span, value); + } +} diff --git a/src/RESPite/Shared/AsciiHash.cs b/src/RESPite/Shared/AsciiHash.cs index 7d3f32ca3..8d4646a5c 100644 --- a/src/RESPite/Shared/AsciiHash.cs +++ b/src/RESPite/Shared/AsciiHash.cs @@ -2,8 +2,8 @@ using System.Buffers.Binary; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Text; namespace RESPite; @@ -34,63 +34,12 @@ public sealed class AsciiHashAttribute(string token = "") : Attribute public bool CaseSensitive { get; set; } = true; } +/// +/// Instance members are in AsciiHash.Instance.cs. +/// [Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] -public readonly partial struct AsciiHash : IEquatable +public readonly partial struct AsciiHash { - // ReSharper disable InconsistentNaming - private readonly long _hashCI, _hashCS, _hashLC; - // ReSharper restore InconsistentNaming - private readonly int _index, _length; - private readonly byte[] _arr; - - public int Length => _length; - - /// - /// The optimal buffer length (with padding) to use for this value. - /// - public int BufferLength => (Length + 1 + 7) & ~7; // an extra byte, then round up to word-size - - public ReadOnlySpan Span => new(_arr ?? [], _index, _length); - - public AsciiHash(ReadOnlySpan value) : this(value.ToArray(), 0, value.Length) { } - public AsciiHash(string value) : this(Encoding.ASCII.GetBytes(value)) { } - - /// - public override int GetHashCode() => _hashCS.GetHashCode(); - - /// - public override string ToString() => _length == 0 ? "" : Encoding.ASCII.GetString(_arr, _index, _length); - - /// - public override bool Equals(object? other) => other is AsciiHash hash && Equals(hash); - - /// - public bool Equals(in AsciiHash other) - { - return (_length == other.Length & _hashCS == other._hashCS) - && (_length <= MaxBytesHashIsEqualityCS || Span.SequenceEqual(other.Span)); - } - - bool IEquatable.Equals(AsciiHash other) => Equals(other); - - public AsciiHash(byte[] arr) : this(arr, 0, -1) { } - - public AsciiHash(byte[] arr, int index, int length) - { - _arr = arr ?? []; - _index = index; - _length = length < 0 ? (_arr.Length - index) : length; - - var span = new ReadOnlySpan(_arr, _index, _length); - Hash(span, out _hashCS, out _hashCI); - - // pre-compute the lower-case hash - Span buffer = stackalloc byte[8]; - BinaryPrimitives.WriteInt64LittleEndian(buffer, _hashCS); - ToLower(buffer); - _hashLC = BinaryPrimitives.ReadInt64LittleEndian(buffer); - } - /// /// In-place ASCII upper-case conversion. /// @@ -115,103 +64,25 @@ public static void ToLower(Span span) } } - private const long CaseMask = ~0x2020202020202020; - - public bool IsCS(ReadOnlySpan value) => IsCS(HashCS(value), value); - - public bool IsCS(long hash, ReadOnlySpan value) - { - var len = _length; - if (hash != _hashCS | value.Length != len) return false; - return len <= MaxBytesHashIsEqualityCS || EqualsCS(Span, value); - } - - public bool IsCI(ReadOnlySpan value) => IsCI(HashCI(value), value); - - public bool IsCI(long hash, ReadOnlySpan value) - { - var len = _length; - if (hash != _hashCI | value.Length != len) return false; - if (len <= MaxBytesHashIsEqualityCS && HashCS(value) == _hashCS) return true; - return EqualsCI(Span, value); - } - - public static long HashCS(in 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 >= MaxBytesHashed | value.IsSingleSegment - ? HashCS(first) : SlowHashCS(value); - - static long SlowHashCS(in ReadOnlySequence value) - { - Span buffer = stackalloc byte[MaxBytesHashed]; - var len = value.Length; - if (len <= MaxBytesHashed) - { - value.CopyTo(buffer); - buffer = buffer.Slice(0, (int)len); - } - else - { - value.Slice(0, MaxBytesHashed).CopyTo(buffer); - } - - return HashCS(buffer); - } - } - - internal const int MaxBytesHashIsEqualityCS = sizeof(long), MaxBytesHashed = sizeof(long); + internal const int 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); + return len <= MaxBytesHashed ? HashCS(first) == HashCS(second) : first.SequenceEqual(second); } public static bool SequenceEqualsCS(ReadOnlySpan first, ReadOnlySpan second) => first.SequenceEqual(second); - public static unsafe bool EqualsCI(ReadOnlySpan first, ReadOnlySpan second) + public static 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 = 0b0101_1111; - 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; - } - } + // for very short values, the UC hash performs CI equality + return len <= MaxBytesHashed ? HashUC(first) == HashUC(second) : SequenceEqualsCI(first, second); } public static unsafe bool SequenceEqualsCI(ReadOnlySpan first, ReadOnlySpan second) @@ -254,47 +125,18 @@ 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); + return len <= MaxBytesHashed ? HashCS(first) == HashCS(second) : first.SequenceEqual(second); } public static bool SequenceEqualsCS(ReadOnlySpan first, ReadOnlySpan second) => first.SequenceEqual(second); - public static unsafe bool EqualsCI(ReadOnlySpan first, ReadOnlySpan second) + public static 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 (char* firstPtr = &MemoryMarshal.GetReference(first)) - { - fixed (char* secondPtr = &MemoryMarshal.GetReference(second)) - { - const int CS_MASK = 0b0101_1111; - for (int i = 0; i < len; i++) - { - int x = (byte)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 != (byte)secondPtr[i]) - { - // non-alpha mismatch - return false; - } - } - - return true; - } - } + return len <= MaxBytesHashed ? HashUC(first) == HashUC(second) : SequenceEqualsCI(first, second); } public static unsafe bool SequenceEqualsCI(ReadOnlySpan first, ReadOnlySpan second) @@ -332,20 +174,41 @@ public static unsafe bool SequenceEqualsCI(ReadOnlySpan first, ReadOnlySpa } } - public static void Hash(scoped ReadOnlySpan value, out long cs, out long ci) + public static void Hash(scoped ReadOnlySpan value, out long cs, out long uc) { cs = HashCS(value); - ci = cs & CaseMask; + uc = ToUC(cs); } - public static void Hash(scoped ReadOnlySpan value, out long cs, out long ci) + public static void Hash(scoped ReadOnlySpan value, out long cs, out long uc) { cs = HashCS(value); - ci = cs & CaseMask; + uc = ToUC(cs); } - public static long HashCI(scoped ReadOnlySpan value) - => HashCS(value) & CaseMask; + public static long HashUC(scoped ReadOnlySpan value) => ToUC(HashCS(value)); + + public static long HashUC(scoped ReadOnlySpan value) => ToUC(HashCS(value)); + + internal static long ToUC(long hashCS) + { + const long LC_MASK = 0x2020_2020_2020_2020; + // check whether there are any possible lower-case letters; + // this would be anything with the 0x20 bit set + if ((hashCS & LC_MASK) == 0) return hashCS; + + // Something looks possibly lower-case; we can't just mask it off, + // because there are other non-alpha characters in that range. +#if NET || NETSTANDARD2_1_OR_GREATER + ToUpper(MemoryMarshal.CreateSpan(ref Unsafe.As(ref hashCS), sizeof(long))); + return hashCS; +#else + Span buffer = stackalloc byte[8]; + BinaryPrimitives.WriteInt64LittleEndian(buffer, hashCS); + ToUpper(buffer); + return BinaryPrimitives.ReadInt64LittleEndian(buffer); +#endif + } public static long HashCS(scoped ReadOnlySpan value) { @@ -378,6 +241,4 @@ public static long HashCS(scoped ReadOnlySpan value) } return (long)tally; } - - public static long HashCI(scoped ReadOnlySpan value) => HashCS(value) & CaseMask; } diff --git a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs index 996159d5b..9cc8008ea 100644 --- a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs +++ b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs @@ -84,12 +84,12 @@ private static bool IsArrayOutOfBand(in RespReader source) ? tmp : StackCopyLengthChecked(in reader, stackalloc byte[MAX_TYPE_LEN]); - var hash = AsciiHash.HashCS(span); - switch (hash) + var cs = AsciiHash.HashCS(span); + switch (cs) { - 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: + case PushMessage.HashCS when PushMessage.IsCS(span, cs) & len >= 3: + case PushPMessage.HashCS when PushPMessage.IsCS(span, cs) & len >= 4: + case PushSMessage.HashCS when PushSMessage.IsCS(span, cs) & len >= 3: return true; } } diff --git a/src/StackExchange.Redis/HotKeys.ResultProcessor.cs b/src/StackExchange.Redis/HotKeys.ResultProcessor.cs index fd4754406..e3b434785 100644 --- a/src/StackExchange.Redis/HotKeys.ResultProcessor.cs +++ b/src/StackExchange.Redis/HotKeys.ResultProcessor.cs @@ -53,21 +53,21 @@ private HotKeysResult(ref RespReader reader) continue; } - var hash = AsciiHash.HashCS(keyBytes); + var hashCS = AsciiHash.HashCS(keyBytes); // Move to value if (!reader.TryMoveNext()) break; long i64; - switch (hash) + switch (hashCS) { - case tracking_active.HashCS when tracking_active.IsCS(hash, keyBytes): + case tracking_active.HashCS when tracking_active.IsCS(keyBytes, hashCS): TrackingActive = reader.ReadBoolean(); break; - case sample_ratio.HashCS when sample_ratio.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): + case sample_ratio.HashCS when sample_ratio.IsCS(keyBytes, hashCS) && reader.TryReadInt64(out i64): SampleRatio = i64; break; - case selected_slots.HashCS when selected_slots.IsCS(hash, keyBytes) && reader.IsAggregate: + case selected_slots.HashCS when selected_slots.IsCS(keyBytes, hashCS) && reader.IsAggregate: var slotRanges = reader.ReadPastArray( static (ref RespReader slotReader) => { @@ -100,55 +100,55 @@ private HotKeysResult(ref RespReader reader) _selectedSlots = slotRanges ?? []; } break; - case all_commands_all_slots_us.HashCS when all_commands_all_slots_us.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): + case all_commands_all_slots_us.HashCS when all_commands_all_slots_us.IsCS(keyBytes, hashCS) && reader.TryReadInt64(out i64): AllCommandsAllSlotsMicroseconds = i64; break; - case all_commands_selected_slots_us.HashCS when all_commands_selected_slots_us.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): + case all_commands_selected_slots_us.HashCS when all_commands_selected_slots_us.IsCS(keyBytes, hashCS) && reader.TryReadInt64(out i64): AllCommandSelectedSlotsMicroseconds = i64; break; - 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): + case sampled_command_selected_slots_us.HashCS when sampled_command_selected_slots_us.IsCS(keyBytes, hashCS) && reader.TryReadInt64(out i64): + case sampled_commands_selected_slots_us.HashCS when sampled_commands_selected_slots_us.IsCS(keyBytes, hashCS) && reader.TryReadInt64(out i64): SampledCommandsSelectedSlotsMicroseconds = i64; break; - case net_bytes_all_commands_all_slots.HashCS when net_bytes_all_commands_all_slots.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): + case net_bytes_all_commands_all_slots.HashCS when net_bytes_all_commands_all_slots.IsCS(keyBytes, hashCS) && reader.TryReadInt64(out i64): AllCommandsAllSlotsNetworkBytes = i64; break; - case net_bytes_all_commands_selected_slots.HashCS when net_bytes_all_commands_selected_slots.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): + case net_bytes_all_commands_selected_slots.HashCS when net_bytes_all_commands_selected_slots.IsCS(keyBytes, hashCS) && reader.TryReadInt64(out i64): NetworkBytesAllCommandsSelectedSlotsRaw = i64; break; - case net_bytes_sampled_commands_selected_slots.HashCS when net_bytes_sampled_commands_selected_slots.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): + case net_bytes_sampled_commands_selected_slots.HashCS when net_bytes_sampled_commands_selected_slots.IsCS(keyBytes, hashCS) && reader.TryReadInt64(out i64): NetworkBytesSampledCommandsSelectedSlotsRaw = i64; break; - case collection_start_time_unix_ms.HashCS when collection_start_time_unix_ms.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): + case collection_start_time_unix_ms.HashCS when collection_start_time_unix_ms.IsCS(keyBytes, hashCS) && reader.TryReadInt64(out i64): CollectionStartTimeUnixMilliseconds = i64; break; - case collection_duration_ms.HashCS when collection_duration_ms.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): + case collection_duration_ms.HashCS when collection_duration_ms.IsCS(keyBytes, hashCS) && 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.HashCS when collection_duration_us.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): + case collection_duration_us.HashCS when collection_duration_us.IsCS(keyBytes, hashCS) && reader.TryReadInt64(out i64): CollectionDurationMicroseconds = i64; break; - case total_cpu_time_sys_ms.HashCS when total_cpu_time_sys_ms.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): + case total_cpu_time_sys_ms.HashCS when total_cpu_time_sys_ms.IsCS(keyBytes, hashCS) && 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.HashCS when total_cpu_time_sys_us.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): + case total_cpu_time_sys_us.HashCS when total_cpu_time_sys_us.IsCS(keyBytes, hashCS) && reader.TryReadInt64(out i64): metrics |= HotKeysMetrics.Cpu; TotalCpuTimeSystemMicroseconds = i64; break; - case total_cpu_time_user_ms.HashCS when total_cpu_time_user_ms.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): + case total_cpu_time_user_ms.HashCS when total_cpu_time_user_ms.IsCS(keyBytes, hashCS) && 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.HashCS when total_cpu_time_user_us.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): + case total_cpu_time_user_us.HashCS when total_cpu_time_user_us.IsCS(keyBytes, hashCS) && reader.TryReadInt64(out i64): metrics |= HotKeysMetrics.Cpu; TotalCpuTimeUserMicroseconds = i64; break; - case total_net_bytes.HashCS when total_net_bytes.IsCS(hash, keyBytes) && reader.TryReadInt64(out i64): + case total_net_bytes.HashCS when total_net_bytes.IsCS(keyBytes, hashCS) && reader.TryReadInt64(out i64): metrics |= HotKeysMetrics.Network; TotalNetworkBytesRaw = i64; break; - case by_cpu_time_us.HashCS when by_cpu_time_us.IsCS(hash, keyBytes) && reader.IsAggregate: + case by_cpu_time_us.HashCS when by_cpu_time_us.IsCS(keyBytes, hashCS) && reader.IsAggregate: metrics |= HotKeysMetrics.Cpu; int cpuLen = reader.AggregateLength() / 2; var cpuTime = new MetricKeyCpu[cpuLen]; @@ -164,7 +164,7 @@ private HotKeysResult(ref RespReader reader) } _cpuByKey = cpuTime; break; - case by_net_bytes.HashCS when by_net_bytes.IsCS(hash, keyBytes) && reader.IsAggregate: + case by_net_bytes.HashCS when by_net_bytes.IsCS(keyBytes, hashCS) && reader.IsAggregate: metrics |= HotKeysMetrics.Network; int netLen = reader.AggregateLength() / 2; var netBytes = new MetricKeyBytes[netLen]; diff --git a/src/StackExchange.Redis/KeyNotification.cs b/src/StackExchange.Redis/KeyNotification.cs index 71675aeb7..beea79dd6 100644 --- a/src/StackExchange.Redis/KeyNotification.cs +++ b/src/StackExchange.Redis/KeyNotification.cs @@ -37,11 +37,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 = AsciiHash.HashCS(prefix); - switch (hash) + var hashCS = AsciiHash.HashCS(prefix); + switch (hashCS) { - case KeySpacePrefix.HashCS when KeySpacePrefix.IsCS(hash, prefix): - case KeyEventPrefix.HashCS when KeyEventPrefix.IsCS(hash, prefix): + case KeySpacePrefix.HashCS when KeySpacePrefix.IsCS(prefix, hashCS): + case KeyEventPrefix.HashCS when KeyEventPrefix.IsCS(prefix, hashCS): // 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 +442,7 @@ public bool IsKeySpace get { var span = _channel.Span; - return span.Length >= KeySpacePrefix.Length + MinSuffixBytes && KeySpacePrefix.IsCS(AsciiHash.HashCS(span), span.Slice(0, KeySpacePrefix.Length)); + return span.Length >= KeySpacePrefix.Length + MinSuffixBytes && KeySpacePrefix.IsCS(span.Slice(0, KeySpacePrefix.Length), AsciiHash.HashCS(span)); } } @@ -454,7 +454,7 @@ public bool IsKeyEvent get { var span = _channel.Span; - return span.Length >= KeyEventPrefix.Length + MinSuffixBytes && KeyEventPrefix.IsCS(AsciiHash.HashCS(span), span.Slice(0, KeyEventPrefix.Length)); + return span.Length >= KeyEventPrefix.Length + MinSuffixBytes && KeyEventPrefix.IsCS(span.Slice(0, KeyEventPrefix.Length), AsciiHash.HashCS(span)); } } diff --git a/src/StackExchange.Redis/KeyNotificationTypeAsciiHash.cs b/src/StackExchange.Redis/KeyNotificationTypeAsciiHash.cs index 997c66e62..ea8a06659 100644 --- a/src/StackExchange.Redis/KeyNotificationTypeAsciiHash.cs +++ b/src/StackExchange.Redis/KeyNotificationTypeAsciiHash.cs @@ -13,63 +13,63 @@ internal static partial class KeyNotificationTypeAsciiHash public static KeyNotificationType Parse(ReadOnlySpan value) { - var hash = AsciiHash.HashCS(value); - return hash switch + var hashCS = AsciiHash.HashCS(value); + return hashCS switch { - 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, + append.HashCS when append.IsCS(value, hashCS) => KeyNotificationType.Append, + copy.HashCS when copy.IsCS(value, hashCS) => KeyNotificationType.Copy, + del.HashCS when del.IsCS(value, hashCS) => KeyNotificationType.Del, + expire.HashCS when expire.IsCS(value, hashCS) => KeyNotificationType.Expire, + hdel.HashCS when hdel.IsCS(value, hashCS) => KeyNotificationType.HDel, + hexpired.HashCS when hexpired.IsCS(value, hashCS) => KeyNotificationType.HExpired, + hincrbyfloat.HashCS when hincrbyfloat.IsCS(value, hashCS) => KeyNotificationType.HIncrByFloat, + hincrby.HashCS when hincrby.IsCS(value, hashCS) => KeyNotificationType.HIncrBy, + hpersist.HashCS when hpersist.IsCS(value, hashCS) => KeyNotificationType.HPersist, + hset.HashCS when hset.IsCS(value, hashCS) => KeyNotificationType.HSet, + incrbyfloat.HashCS when incrbyfloat.IsCS(value, hashCS) => KeyNotificationType.IncrByFloat, + incrby.HashCS when incrby.IsCS(value, hashCS) => KeyNotificationType.IncrBy, + linsert.HashCS when linsert.IsCS(value, hashCS) => KeyNotificationType.LInsert, + lpop.HashCS when lpop.IsCS(value, hashCS) => KeyNotificationType.LPop, + lpush.HashCS when lpush.IsCS(value, hashCS) => KeyNotificationType.LPush, + lrem.HashCS when lrem.IsCS(value, hashCS) => KeyNotificationType.LRem, + lset.HashCS when lset.IsCS(value, hashCS) => KeyNotificationType.LSet, + ltrim.HashCS when ltrim.IsCS(value, hashCS) => KeyNotificationType.LTrim, + move_from.HashCS when move_from.IsCS(value, hashCS) => KeyNotificationType.MoveFrom, + move_to.HashCS when move_to.IsCS(value, hashCS) => KeyNotificationType.MoveTo, + persist.HashCS when persist.IsCS(value, hashCS) => KeyNotificationType.Persist, + rename_from.HashCS when rename_from.IsCS(value, hashCS) => KeyNotificationType.RenameFrom, + rename_to.HashCS when rename_to.IsCS(value, hashCS) => KeyNotificationType.RenameTo, + restore.HashCS when restore.IsCS(value, hashCS) => KeyNotificationType.Restore, + rpop.HashCS when rpop.IsCS(value, hashCS) => KeyNotificationType.RPop, + rpush.HashCS when rpush.IsCS(value, hashCS) => KeyNotificationType.RPush, + sadd.HashCS when sadd.IsCS(value, hashCS) => KeyNotificationType.SAdd, + set.HashCS when set.IsCS(value, hashCS) => KeyNotificationType.Set, + setrange.HashCS when setrange.IsCS(value, hashCS) => KeyNotificationType.SetRange, + sortstore.HashCS when sortstore.IsCS(value, hashCS) => KeyNotificationType.SortStore, + srem.HashCS when srem.IsCS(value, hashCS) => KeyNotificationType.SRem, + spop.HashCS when spop.IsCS(value, hashCS) => KeyNotificationType.SPop, + xadd.HashCS when xadd.IsCS(value, hashCS) => KeyNotificationType.XAdd, + xdel.HashCS when xdel.IsCS(value, hashCS) => KeyNotificationType.XDel, + xgroup_createconsumer.HashCS when xgroup_createconsumer.IsCS(value, hashCS) => KeyNotificationType.XGroupCreateConsumer, + xgroup_create.HashCS when xgroup_create.IsCS(value, hashCS) => KeyNotificationType.XGroupCreate, + xgroup_delconsumer.HashCS when xgroup_delconsumer.IsCS(value, hashCS) => KeyNotificationType.XGroupDelConsumer, + xgroup_destroy.HashCS when xgroup_destroy.IsCS(value, hashCS) => KeyNotificationType.XGroupDestroy, + xgroup_setid.HashCS when xgroup_setid.IsCS(value, hashCS) => KeyNotificationType.XGroupSetId, + xsetid.HashCS when xsetid.IsCS(value, hashCS) => KeyNotificationType.XSetId, + xtrim.HashCS when xtrim.IsCS(value, hashCS) => KeyNotificationType.XTrim, + zadd.HashCS when zadd.IsCS(value, hashCS) => KeyNotificationType.ZAdd, + zdiffstore.HashCS when zdiffstore.IsCS(value, hashCS) => KeyNotificationType.ZDiffStore, + zinterstore.HashCS when zinterstore.IsCS(value, hashCS) => KeyNotificationType.ZInterStore, + zunionstore.HashCS when zunionstore.IsCS(value, hashCS) => KeyNotificationType.ZUnionStore, + zincr.HashCS when zincr.IsCS(value, hashCS) => KeyNotificationType.ZIncr, + zrembyrank.HashCS when zrembyrank.IsCS(value, hashCS) => KeyNotificationType.ZRemByRank, + zrembyscore.HashCS when zrembyscore.IsCS(value, hashCS) => KeyNotificationType.ZRemByScore, + zrem.HashCS when zrem.IsCS(value, hashCS) => KeyNotificationType.ZRem, + expired.HashCS when expired.IsCS(value, hashCS) => KeyNotificationType.Expired, + evicted.HashCS when evicted.IsCS(value, hashCS) => KeyNotificationType.Evicted, + _new.HashCS when _new.IsCS(value, hashCS) => KeyNotificationType.New, + overwritten.HashCS when overwritten.IsCS(value, hashCS) => KeyNotificationType.Overwritten, + type_changed.HashCS when type_changed.IsCS(value, hashCS) => KeyNotificationType.TypeChanged, _ => KeyNotificationType.Unknown, }; } diff --git a/src/StackExchange.Redis/PhysicalConnection.Read.cs b/src/StackExchange.Redis/PhysicalConnection.Read.cs index 8aa654bdb..84671b716 100644 --- a/src/StackExchange.Redis/PhysicalConnection.Read.cs +++ b/src/StackExchange.Redis/PhysicalConnection.Read.cs @@ -352,41 +352,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 = AsciiHash.HashCS(span); + var hashCS = AsciiHash.HashCS(span); RedisChannel.RedisChannelOptions channelOptions = RedisChannel.RedisChannelOptions.None; PushKind kind; - switch (hash) + switch (hashCS) { - case PushMessage.HashCS when PushMessage.IsCS(hash, span) & len >= 3: + case PushMessage.HashCS when PushMessage.IsCS(span, hashCS) & len >= 3: kind = PushKind.Message; break; - case PushPMessage.HashCS when PushPMessage.IsCS(hash, span) & len >= 4: + case PushPMessage.HashCS when PushPMessage.IsCS(span, hashCS) & len >= 4: channelOptions = RedisChannel.RedisChannelOptions.Pattern; kind = PushKind.PMessage; break; - case PushSMessage.HashCS when PushSMessage.IsCS(hash, span) & len >= 3: + case PushSMessage.HashCS when PushSMessage.IsCS(span, hashCS) & len >= 3: channelOptions = RedisChannel.RedisChannelOptions.Sharded; kind = PushKind.SMessage; break; - case PushSubscribe.HashCS when PushSubscribe.IsCS(hash, span): + case PushSubscribe.HashCS when PushSubscribe.IsCS(span, hashCS): kind = PushKind.Subscribe; break; - case PushPSubscribe.HashCS when PushPSubscribe.IsCS(hash, span): + case PushPSubscribe.HashCS when PushPSubscribe.IsCS(span, hashCS): channelOptions = RedisChannel.RedisChannelOptions.Pattern; kind = PushKind.PSubscribe; break; - case PushSSubscribe.HashCS when PushSSubscribe.IsCS(hash, span): + case PushSSubscribe.HashCS when PushSSubscribe.IsCS(span, hashCS): channelOptions = RedisChannel.RedisChannelOptions.Sharded; kind = PushKind.SSubscribe; break; - case PushUnsubscribe.HashCS when PushUnsubscribe.IsCS(hash, span): + case PushUnsubscribe.HashCS when PushUnsubscribe.IsCS(span, hashCS): kind = PushKind.Unsubscribe; break; - case PushPUnsubscribe.HashCS when PushPUnsubscribe.IsCS(hash, span): + case PushPUnsubscribe.HashCS when PushPUnsubscribe.IsCS(span, hashCS): channelOptions = RedisChannel.RedisChannelOptions.Pattern; kind = PushKind.PUnsubscribe; break; - case PushSUnsubscribe.HashCS when PushSUnsubscribe.IsCS(hash, span): + case PushSUnsubscribe.HashCS when PushSUnsubscribe.IsCS(span, hashCS): channelOptions = RedisChannel.RedisChannelOptions.Sharded; kind = PushKind.SUnsubscribe; break; diff --git a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs index 7721ad678..8fd2a0d55 100644 --- a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs +++ b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs @@ -96,36 +96,36 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes continue; } - var hash = AsciiHash.HashCS(testBytes); // this still contains the key, even though we've advanced - switch (hash) + var hashCS = AsciiHash.HashCS(testBytes); // this still contains the key, even though we've advanced + switch (hashCS) { - case size.HashCS when size.IsCS(hash, testBytes) && reader.TryReadInt64(out var i64): + case size.HashCS when size.IsCS(testBytes, hashCS) && reader.TryReadInt64(out var i64): resultSize = i64; break; - case vset_uid.HashCS when vset_uid.IsCS(hash, testBytes) && reader.TryReadInt64(out var i64): + case vset_uid.HashCS when vset_uid.IsCS(testBytes, hashCS) && reader.TryReadInt64(out var i64): vsetUid = i64; break; - case max_level.HashCS when max_level.IsCS(hash, testBytes) && reader.TryReadInt64(out var i64): + case max_level.HashCS when max_level.IsCS(testBytes, hashCS) && reader.TryReadInt64(out var i64): maxLevel = checked((int)i64); break; - case vector_dim.HashCS when vector_dim.IsCS(hash, testBytes) && reader.TryReadInt64(out var i64): + case vector_dim.HashCS when vector_dim.IsCS(testBytes, hashCS) && reader.TryReadInt64(out var i64): vectorDim = checked((int)i64); break; - case quant_type.HashCS when quant_type.IsCS(hash, testBytes): + case quant_type.HashCS when quant_type.IsCS(testBytes, hashCS): len = reader.ScalarLength(); testBytes = (len > stackBuffer.Length | reader.IsNull) ? default : reader.TryGetSpan(out tmp) ? tmp : reader.Buffer(stackBuffer); - hash = AsciiHash.HashCS(testBytes); - switch (hash) + hashCS = AsciiHash.HashCS(testBytes); + switch (hashCS) { - case bin.HashCS when bin.IsCS(hash, testBytes): + case bin.HashCS when bin.IsCS(testBytes, hashCS): quantType = VectorSetQuantization.Binary; break; - case f32.HashCS when f32.IsCS(hash, testBytes): + case f32.HashCS when f32.IsCS(testBytes, hashCS): quantType = VectorSetQuantization.None; break; - case int8.HashCS when int8.IsCS(hash, testBytes): + case int8.HashCS when int8.IsCS(testBytes, hashCS): quantType = VectorSetQuantization.Int8; break; default: @@ -134,7 +134,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes break; } break; - case hnsw_max_node_uid.HashCS when hnsw_max_node_uid.IsCS(hash, testBytes) && reader.TryReadInt64(out var i64): + case hnsw_max_node_uid.HashCS when hnsw_max_node_uid.IsCS(testBytes, hashCS) && reader.TryReadInt64(out var i64): hnswMaxNodeUid = i64; break; } diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index ed2ddabf0..71559da10 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -1706,16 +1706,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 = AsciiHash.HashCS(span); - return hash switch - { - 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, + var hashCS = AsciiHash.HashCS(span); + return hashCS switch + { + redistype_string.HashCS when redistype_string.IsCS(span, hashCS) => Redis.RedisType.String, + redistype_list.HashCS when redistype_list.IsCS(span, hashCS) => Redis.RedisType.List, + redistype_set.HashCS when redistype_set.IsCS(span, hashCS) => Redis.RedisType.Set, + redistype_zset.HashCS when redistype_zset.IsCS(span, hashCS) => Redis.RedisType.SortedSet, + redistype_hash.HashCS when redistype_hash.IsCS(span, hashCS) => Redis.RedisType.Hash, + redistype_stream.HashCS when redistype_stream.IsCS(span, hashCS) => Redis.RedisType.Stream, + redistype_vectorset.HashCS when redistype_vectorset.IsCS(span, hashCS) => Redis.RedisType.VectorSet, _ => Redis.RedisType.Unknown, }; } diff --git a/tests/StackExchange.Redis.Benchmarks/AsciiHashBenchmarks.cs b/tests/StackExchange.Redis.Benchmarks/AsciiHashBenchmarks.cs index 11264d0fc..57677f705 100644 --- a/tests/StackExchange.Redis.Benchmarks/AsciiHashBenchmarks.cs +++ b/tests/StackExchange.Redis.Benchmarks/AsciiHashBenchmarks.cs @@ -55,8 +55,10 @@ public void Setup() Assert(AsciiHash.HashCS(bytes), nameof(AsciiHash.HashCS) + ":byte"); Assert(AsciiHash.HashCS(_sourceString.AsSpan()), nameof(AsciiHash.HashCS) + ":char"); + /* Assert(AsciiHash.HashCS(SingleSegmentBytes), nameof(AsciiHash.HashCS) + " (single segment)"); Assert(AsciiHash.HashCS(_sourceMultiSegmentBytes), nameof(AsciiHash.HashCS) + " (multi segment)"); + */ void Assert(long actual, string name) { @@ -117,6 +119,7 @@ public long HashCS_C() return hash; } + /* // [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] public long Hash64_SingleSegment() { @@ -142,4 +145,5 @@ public long Hash64_MultiSegment() return hash; } + */ } diff --git a/tests/StackExchange.Redis.Benchmarks/AsciiHashSwitch.cs b/tests/StackExchange.Redis.Benchmarks/AsciiHashSwitch.cs index 016f67e70..2409362ce 100644 --- a/tests/StackExchange.Redis.Benchmarks/AsciiHashSwitch.cs +++ b/tests/StackExchange.Redis.Benchmarks/AsciiHashSwitch.cs @@ -178,7 +178,7 @@ by_net_bytes.Length when by_net_bytes.IsCS(hash, span) => Field.by_net_bytes, public Field SwitchOnHash_CI() { ReadOnlySpan span = _bytes; - var hash = AsciiHash.HashCI(span); + var hash = AsciiHash.HashUC(span); return hash switch { key.HashCI when key.IsCI(hash, span) => Field.key, @@ -213,7 +213,7 @@ by_net_bytes.HashCI when by_net_bytes.IsCI(hash, span) => Field.by_net_bytes, public Field SwitchOnLength_CI() { ReadOnlySpan span = _bytes; - var hash = AsciiHash.HashCI(span); + var hash = AsciiHash.HashUC(span); return span.Length switch { key.Length when key.IsCI(hash, span) => Field.key, diff --git a/tests/StackExchange.Redis.Benchmarks/Program.cs b/tests/StackExchange.Redis.Benchmarks/Program.cs index fd6b8d881..c5c12a657 100644 --- a/tests/StackExchange.Redis.Benchmarks/Program.cs +++ b/tests/StackExchange.Redis.Benchmarks/Program.cs @@ -25,7 +25,7 @@ private static void Main(string[] args) { Console.WriteLine(cmd); } - _ = Console.ReadLine(); + var obj = new AsciiHashBenchmarks(); foreach (var size in obj.Sizes) { diff --git a/tests/StackExchange.Redis.Tests/AsciiHashTests.cs b/tests/StackExchange.Redis.Tests/AsciiHashUnitTests.cs similarity index 69% rename from tests/StackExchange.Redis.Tests/AsciiHashTests.cs rename to tests/StackExchange.Redis.Tests/AsciiHashUnitTests.cs index 1330b29ba..e6c3aedb5 100644 --- a/tests/StackExchange.Redis.Tests/AsciiHashTests.cs +++ b/tests/StackExchange.Redis.Tests/AsciiHashUnitTests.cs @@ -10,7 +10,7 @@ // ReSharper disable IdentifierTypo namespace StackExchange.Redis.Tests; -public partial class AsciiHashTests(ITestOutputHelper log) +public partial class AsciiHashUnitTests(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 @@ -33,7 +33,6 @@ public partial class AsciiHashTests(ITestOutputHelper log) [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 @@ -62,12 +61,12 @@ public void AsciiHashIs_Short() ReadOnlySpan value = "abc"u8; var hash = AsciiHash.HashCS(value); Assert.Equal(abc.HashCS, hash); - Assert.True(abc.IsCS(hash, value)); + Assert.True(abc.IsCS(value, hash)); value = "abz"u8; hash = AsciiHash.HashCS(value); Assert.NotEqual(abc.HashCS, hash); - Assert.False(abc.IsCS(hash, value)); + Assert.False(abc.IsCS(value, hash)); } [Fact] @@ -76,12 +75,12 @@ public void AsciiHashIs_Long() ReadOnlySpan value = "abcdefghijklmnopqrst"u8; var hash = AsciiHash.HashCS(value); Assert.Equal(abcdefghijklmnopqrst.HashCS, hash); - Assert.True(abcdefghijklmnopqrst.IsCS(hash, value)); + Assert.True(abcdefghijklmnopqrst.IsCS(value, hash)); value = "abcdefghijklmnopqrsz"u8; hash = AsciiHash.HashCS(value); Assert.Equal(abcdefghijklmnopqrst.HashCS, hash); // hash collision, fine - Assert.False(abcdefghijklmnopqrst.IsCS(hash, value)); + Assert.False(abcdefghijklmnopqrst.IsCS(value, hash)); } // Test case-sensitive and case-insensitive equality for various lengths @@ -136,8 +135,8 @@ public void CaseInsensitiveEquality(string text) var lower = Encoding.UTF8.GetBytes(text); var upper = Encoding.UTF8.GetBytes(text.ToUpperInvariant()); - var hashLowerCI = AsciiHash.HashCI(lower); - var hashUpperCI = AsciiHash.HashCI(upper); + var hashLowerUC = AsciiHash.HashUC(lower); + var hashUpperUC = AsciiHash.HashUC(upper); // Case-insensitive: same case should match Assert.True(AsciiHash.EqualsCI(lower, lower), "CI: lower == lower"); @@ -148,7 +147,7 @@ public void CaseInsensitiveEquality(string text) Assert.True(AsciiHash.EqualsCI(upper, lower), "CI: upper == lower"); // CI hashes should be the same for different cases - Assert.Equal(hashLowerCI, hashUpperCI); + Assert.Equal(hashLowerUC, hashUpperUC); } [Theory] @@ -178,48 +177,48 @@ public void GeneratedTypes_CaseSensitive(string text) switch (text) { case "a": - Assert.True(a.IsCS(hashLowerCS, lower)); - Assert.False(a.IsCS(hashUpperCS, upper)); + Assert.True(a.IsCS(lower, hashLowerCS)); + Assert.False(a.IsCS(lower, hashUpperCS)); break; case "ab": - Assert.True(ab.IsCS(hashLowerCS, lower)); - Assert.False(ab.IsCS(hashUpperCS, upper)); + Assert.True(ab.IsCS(lower, hashLowerCS)); + Assert.False(ab.IsCS(lower, hashUpperCS)); break; case "abc": - Assert.True(abc.IsCS(hashLowerCS, lower)); - Assert.False(abc.IsCS(hashUpperCS, upper)); + Assert.True(abc.IsCS(lower, hashLowerCS)); + Assert.False(abc.IsCS(lower, hashUpperCS)); break; case "abcd": - Assert.True(abcd.IsCS(hashLowerCS, lower)); - Assert.False(abcd.IsCS(hashUpperCS, upper)); + Assert.True(abcd.IsCS(lower, hashLowerCS)); + Assert.False(abcd.IsCS(lower, hashUpperCS)); break; case "abcde": - Assert.True(abcde.IsCS(hashLowerCS, lower)); - Assert.False(abcde.IsCS(hashUpperCS, upper)); + Assert.True(abcde.IsCS(lower, hashLowerCS)); + Assert.False(abcde.IsCS(lower, hashUpperCS)); break; case "abcdef": - Assert.True(abcdef.IsCS(hashLowerCS, lower)); - Assert.False(abcdef.IsCS(hashUpperCS, upper)); + Assert.True(abcdef.IsCS(lower, hashLowerCS)); + Assert.False(abcdef.IsCS(lower, hashUpperCS)); break; case "abcdefg": - Assert.True(abcdefg.IsCS(hashLowerCS, lower)); - Assert.False(abcdefg.IsCS(hashUpperCS, upper)); + Assert.True(abcdefg.IsCS(lower, hashLowerCS)); + Assert.False(abcdefg.IsCS(lower, hashUpperCS)); break; case "abcdefgh": - Assert.True(abcdefgh.IsCS(hashLowerCS, lower)); - Assert.False(abcdefgh.IsCS(hashUpperCS, upper)); + Assert.True(abcdefgh.IsCS(lower, hashLowerCS)); + Assert.False(abcdefgh.IsCS(lower, hashUpperCS)); break; case "abcdefghijklmnopqrst": - Assert.True(abcdefghijklmnopqrst.IsCS(hashLowerCS, lower)); - Assert.False(abcdefghijklmnopqrst.IsCS(hashUpperCS, upper)); + Assert.True(abcdefghijklmnopqrst.IsCS(lower, hashLowerCS)); + Assert.False(abcdefghijklmnopqrst.IsCS(lower, hashUpperCS)); break; case "foo-bar": - Assert.True(foo_bar_hyphen.IsCS(hashLowerCS, lower)); - Assert.False(foo_bar_hyphen.IsCS(hashUpperCS, upper)); + Assert.True(foo_bar_hyphen.IsCS(lower, hashLowerCS)); + Assert.False(foo_bar_hyphen.IsCS(lower, hashUpperCS)); break; case "foo_bar": - Assert.True(foo_bar_underscore.IsCS(hashLowerCS, lower)); - Assert.False(foo_bar_underscore.IsCS(hashUpperCS, upper)); + Assert.True(foo_bar_underscore.IsCS(lower, hashLowerCS)); + Assert.False(foo_bar_underscore.IsCS(lower, hashUpperCS)); break; } } @@ -244,55 +243,55 @@ public void GeneratedTypes_CaseInsensitive(string text) var lower = Encoding.UTF8.GetBytes(text); var upper = Encoding.UTF8.GetBytes(text.ToUpperInvariant()); - var hashLowerCI = AsciiHash.HashCI(lower); - var hashUpperCI = AsciiHash.HashCI(upper); + var hashLowerUC = AsciiHash.HashUC(lower); + var hashUpperUC = AsciiHash.HashUC(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)); + Assert.True(a.IsCI(lower, hashLowerUC)); + Assert.True(a.IsCI(upper, hashUpperUC)); break; case "ab": - Assert.True(ab.IsCI(hashLowerCI, lower)); - Assert.True(ab.IsCI(hashUpperCI, upper)); + Assert.True(ab.IsCI(lower, hashLowerUC)); + Assert.True(ab.IsCI(upper, hashUpperUC)); break; case "abc": - Assert.True(abc.IsCI(hashLowerCI, lower)); - Assert.True(abc.IsCI(hashUpperCI, upper)); + Assert.True(abc.IsCI(lower, hashLowerUC)); + Assert.True(abc.IsCI(upper, hashUpperUC)); break; case "abcd": - Assert.True(abcd.IsCI(hashLowerCI, lower)); - Assert.True(abcd.IsCI(hashUpperCI, upper)); + Assert.True(abcd.IsCI(lower, hashLowerUC)); + Assert.True(abcd.IsCI(upper, hashUpperUC)); break; case "abcde": - Assert.True(abcde.IsCI(hashLowerCI, lower)); - Assert.True(abcde.IsCI(hashUpperCI, upper)); + Assert.True(abcde.IsCI(lower, hashLowerUC)); + Assert.True(abcde.IsCI(upper, hashUpperUC)); break; case "abcdef": - Assert.True(abcdef.IsCI(hashLowerCI, lower)); - Assert.True(abcdef.IsCI(hashUpperCI, upper)); + Assert.True(abcdef.IsCI(lower, hashLowerUC)); + Assert.True(abcdef.IsCI(upper, hashUpperUC)); break; case "abcdefg": - Assert.True(abcdefg.IsCI(hashLowerCI, lower)); - Assert.True(abcdefg.IsCI(hashUpperCI, upper)); + Assert.True(abcdefg.IsCI(lower, hashLowerUC)); + Assert.True(abcdefg.IsCI(upper, hashUpperUC)); break; case "abcdefgh": - Assert.True(abcdefgh.IsCI(hashLowerCI, lower)); - Assert.True(abcdefgh.IsCI(hashUpperCI, upper)); + Assert.True(abcdefgh.IsCI(lower, hashLowerUC)); + Assert.True(abcdefgh.IsCI(upper, hashUpperUC)); break; case "abcdefghijklmnopqrst": - Assert.True(abcdefghijklmnopqrst.IsCI(hashLowerCI, lower)); - Assert.True(abcdefghijklmnopqrst.IsCI(hashUpperCI, upper)); + Assert.True(abcdefghijklmnopqrst.IsCI(lower, hashLowerUC)); + Assert.True(abcdefghijklmnopqrst.IsCI(upper, hashUpperUC)); break; case "foo-bar": - Assert.True(foo_bar_hyphen.IsCI(hashLowerCI, lower)); - Assert.True(foo_bar_hyphen.IsCI(hashUpperCI, upper)); + Assert.True(foo_bar_hyphen.IsCI(lower, hashLowerUC)); + Assert.True(foo_bar_hyphen.IsCI(upper, hashUpperUC)); break; case "foo_bar": - Assert.True(foo_bar_underscore.IsCI(hashLowerCI, lower)); - Assert.True(foo_bar_underscore.IsCI(hashUpperCI, upper)); + Assert.True(foo_bar_underscore.IsCI(lower, hashLowerUC)); + Assert.True(foo_bar_underscore.IsCI(upper, hashUpperUC)); break; } } @@ -304,10 +303,10 @@ public void GeneratedType_a_CaseSensitivity() ReadOnlySpan lower = "a"u8; ReadOnlySpan upper = "A"u8; - Assert.True(a.IsCS(AsciiHash.HashCS(lower), lower)); - Assert.False(a.IsCS(AsciiHash.HashCS(upper), upper)); - Assert.True(a.IsCI(AsciiHash.HashCI(lower), lower)); - Assert.True(a.IsCI(AsciiHash.HashCI(upper), upper)); + Assert.True(a.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(a.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(a.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(a.IsCI(upper, AsciiHash.HashUC(upper))); } [Fact] @@ -316,10 +315,10 @@ public void GeneratedType_ab_CaseSensitivity() ReadOnlySpan lower = "ab"u8; ReadOnlySpan upper = "AB"u8; - Assert.True(ab.IsCS(AsciiHash.HashCS(lower), lower)); - Assert.False(ab.IsCS(AsciiHash.HashCS(upper), upper)); - Assert.True(ab.IsCI(AsciiHash.HashCI(lower), lower)); - Assert.True(ab.IsCI(AsciiHash.HashCI(upper), upper)); + Assert.True(ab.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(ab.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(ab.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(ab.IsCI(upper, AsciiHash.HashUC(upper))); } [Fact] @@ -328,10 +327,10 @@ public void GeneratedType_abc_CaseSensitivity() ReadOnlySpan lower = "abc"u8; ReadOnlySpan upper = "ABC"u8; - Assert.True(abc.IsCS(AsciiHash.HashCS(lower), lower)); - Assert.False(abc.IsCS(AsciiHash.HashCS(upper), upper)); - Assert.True(abc.IsCI(AsciiHash.HashCI(lower), lower)); - Assert.True(abc.IsCI(AsciiHash.HashCI(upper), upper)); + Assert.True(abc.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(abc.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(abc.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(abc.IsCI(upper, AsciiHash.HashUC(upper))); } [Fact] @@ -340,10 +339,10 @@ public void GeneratedType_abcd_CaseSensitivity() ReadOnlySpan lower = "abcd"u8; ReadOnlySpan upper = "ABCD"u8; - Assert.True(abcd.IsCS(AsciiHash.HashCS(lower), lower)); - Assert.False(abcd.IsCS(AsciiHash.HashCS(upper), upper)); - Assert.True(abcd.IsCI(AsciiHash.HashCI(lower), lower)); - Assert.True(abcd.IsCI(AsciiHash.HashCI(upper), upper)); + Assert.True(abcd.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(abcd.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(abcd.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(abcd.IsCI(upper, AsciiHash.HashUC(upper))); } [Fact] @@ -352,10 +351,10 @@ public void GeneratedType_abcde_CaseSensitivity() ReadOnlySpan lower = "abcde"u8; ReadOnlySpan upper = "ABCDE"u8; - Assert.True(abcde.IsCS(AsciiHash.HashCS(lower), lower)); - Assert.False(abcde.IsCS(AsciiHash.HashCS(upper), upper)); - Assert.True(abcde.IsCI(AsciiHash.HashCI(lower), lower)); - Assert.True(abcde.IsCI(AsciiHash.HashCI(upper), upper)); + Assert.True(abcde.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(abcde.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(abcde.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(abcde.IsCI(upper, AsciiHash.HashUC(upper))); } [Fact] @@ -364,10 +363,10 @@ public void GeneratedType_abcdef_CaseSensitivity() ReadOnlySpan lower = "abcdef"u8; ReadOnlySpan upper = "ABCDEF"u8; - Assert.True(abcdef.IsCS(AsciiHash.HashCS(lower), lower)); - Assert.False(abcdef.IsCS(AsciiHash.HashCS(upper), upper)); - Assert.True(abcdef.IsCI(AsciiHash.HashCI(lower), lower)); - Assert.True(abcdef.IsCI(AsciiHash.HashCI(upper), upper)); + Assert.True(abcdef.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(abcdef.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(abcdef.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(abcdef.IsCI(upper, AsciiHash.HashUC(upper))); } [Fact] @@ -376,10 +375,10 @@ public void GeneratedType_abcdefg_CaseSensitivity() ReadOnlySpan lower = "abcdefg"u8; ReadOnlySpan upper = "ABCDEFG"u8; - Assert.True(abcdefg.IsCS(AsciiHash.HashCS(lower), lower)); - Assert.False(abcdefg.IsCS(AsciiHash.HashCS(upper), upper)); - Assert.True(abcdefg.IsCI(AsciiHash.HashCI(lower), lower)); - Assert.True(abcdefg.IsCI(AsciiHash.HashCI(upper), upper)); + Assert.True(abcdefg.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(abcdefg.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(abcdefg.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(abcdefg.IsCI(upper, AsciiHash.HashUC(upper))); } [Fact] @@ -388,10 +387,10 @@ public void GeneratedType_abcdefgh_CaseSensitivity() ReadOnlySpan lower = "abcdefgh"u8; ReadOnlySpan upper = "ABCDEFGH"u8; - Assert.True(abcdefgh.IsCS(AsciiHash.HashCS(lower), lower)); - Assert.False(abcdefgh.IsCS(AsciiHash.HashCS(upper), upper)); - Assert.True(abcdefgh.IsCI(AsciiHash.HashCI(lower), lower)); - Assert.True(abcdefgh.IsCI(AsciiHash.HashCI(upper), upper)); + Assert.True(abcdefgh.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(abcdefgh.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(abcdefgh.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(abcdefgh.IsCI(upper, AsciiHash.HashUC(upper))); } [Fact] @@ -400,10 +399,10 @@ public void GeneratedType_abcdefghijklmnopqrst_CaseSensitivity() ReadOnlySpan lower = "abcdefghijklmnopqrst"u8; ReadOnlySpan upper = "ABCDEFGHIJKLMNOPQRST"u8; - Assert.True(abcdefghijklmnopqrst.IsCS(AsciiHash.HashCS(lower), lower)); - Assert.False(abcdefghijklmnopqrst.IsCS(AsciiHash.HashCS(upper), upper)); - Assert.True(abcdefghijklmnopqrst.IsCI(AsciiHash.HashCI(lower), lower)); - Assert.True(abcdefghijklmnopqrst.IsCI(AsciiHash.HashCI(upper), upper)); + Assert.True(abcdefghijklmnopqrst.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(abcdefghijklmnopqrst.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(abcdefghijklmnopqrst.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(abcdefghijklmnopqrst.IsCI(upper, AsciiHash.HashUC(upper))); } [Fact] @@ -413,10 +412,10 @@ public void GeneratedType_foo_bar_CaseSensitivity() ReadOnlySpan lower = "foo-bar"u8; ReadOnlySpan upper = "FOO-BAR"u8; - Assert.True(foo_bar.IsCS(AsciiHash.HashCS(lower), lower)); - Assert.False(foo_bar.IsCS(AsciiHash.HashCS(upper), upper)); - Assert.True(foo_bar.IsCI(AsciiHash.HashCI(lower), lower)); - Assert.True(foo_bar.IsCI(AsciiHash.HashCI(upper), upper)); + Assert.True(foo_bar.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(foo_bar.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(foo_bar.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(foo_bar.IsCI(upper, AsciiHash.HashUC(upper))); } [Fact] @@ -426,10 +425,10 @@ public void GeneratedType_foo_bar_hyphen_CaseSensitivity() ReadOnlySpan lower = "foo-bar"u8; ReadOnlySpan upper = "FOO-BAR"u8; - Assert.True(foo_bar_hyphen.IsCS(AsciiHash.HashCS(lower), lower)); - Assert.False(foo_bar_hyphen.IsCS(AsciiHash.HashCS(upper), upper)); - Assert.True(foo_bar_hyphen.IsCI(AsciiHash.HashCI(lower), lower)); - Assert.True(foo_bar_hyphen.IsCI(AsciiHash.HashCI(upper), upper)); + Assert.True(foo_bar_hyphen.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(foo_bar_hyphen.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(foo_bar_hyphen.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(foo_bar_hyphen.IsCI(upper, AsciiHash.HashUC(upper))); } [Fact] diff --git a/toys/StackExchange.Redis.Server/RedisRequest.cs b/toys/StackExchange.Redis.Server/RedisRequest.cs index 9584bc353..eb213591d 100644 --- a/toys/StackExchange.Redis.Server/RedisRequest.cs +++ b/toys/StackExchange.Redis.Server/RedisRequest.cs @@ -80,7 +80,7 @@ internal RedisRequest(scoped in RespReader reader, ref byte[] commandLease) } var readBytes = local.CopyTo(commandLease); Debug.Assert(readBytes == len); - AsciiHash.ToLower(commandLease.AsSpan(0, readBytes)); + AsciiHash.ToUpper(commandLease.AsSpan(0, readBytes)); // note we retain the lease array in the Command, this is intentional Command = new(commandLease, 0, readBytes); } diff --git a/toys/StackExchange.Redis.Server/RespServer.cs b/toys/StackExchange.Redis.Server/RespServer.cs index 641c4749a..420dc83ba 100644 --- a/toys/StackExchange.Redis.Server/RespServer.cs +++ b/toys/StackExchange.Redis.Server/RespServer.cs @@ -50,7 +50,7 @@ from method in server.GetType() let attrib = CheckSignatureAndGetAttribute(method) where attrib != null select new RespCommand(attrib, method, server)) - .GroupBy(x => new AsciiHash(x.Command.ToLowerInvariant()), AsciiHash.CaseSensitiveEqualityComparer); + .GroupBy(x => new AsciiHash(x.Command.ToUpperInvariant()), AsciiHash.CaseSensitiveEqualityComparer); var result = new Dictionary(AsciiHash.CaseSensitiveEqualityComparer); foreach (var grp in grouped)