From df0e2ba144105cc626fdd49a2cbf9f579fe939bb Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 29 Aug 2025 10:54:52 +0100 Subject: [PATCH 001/108] RESPite, squashed --- Directory.Build.props | 10 + Directory.Packages.props | 6 + StackExchange.Redis.sln | 44 + docs/ReleaseNotes.md | 6 +- .../RespCommandGenerator.cs | 735 ++++++++ .../RespCommandGenerator.md | 18 + .../StackExchange.Redis.Build.csproj | 14 + global.json | 2 +- src/Directory.Build.props | 5 +- src/RESP.Core/AmbientBufferWriter.cs | 93 + src/RESP.Core/BatchConnection.cs | 229 +++ src/RESP.Core/Builder.cs | 33 + src/RESP.Core/CustomNetworkStream.cs | 297 +++ src/RESP.Core/CycleBuffer.Simple.cs | 114 ++ src/RESP.Core/CycleBuffer.cs | 707 ++++++++ src/RESP.Core/DebugCounters.cs | 183 ++ src/RESP.Core/DirectWriteConnection.cs | 722 ++++++++ src/RESP.Core/FrameScanInfo.cs | 34 + src/RESP.Core/Global.cs | 4 + src/RESP.Core/IRespConnection.cs | 23 + src/RESP.Core/IRespReader.cs | 14 + src/RESP.Core/Message.cs | 211 +++ src/RESP.Core/PipelinedConnection.cs | 218 +++ src/RESP.Core/PublicAPI/PublicAPI.Shipped.txt | 1 + .../PublicAPI/PublicAPI.Unshipped.txt | 191 ++ .../PublicAPI/net5.0/PublicAPI.Shipped.txt | 1 + .../PublicAPI/net5.0/PublicAPI.Unshipped.txt | 2 + .../PublicAPI/net7.0/PublicAPI.Shipped.txt | 1 + .../PublicAPI/net7.0/PublicAPI.Unshipped.txt | 2 + .../PublicAPI/net8.0/PublicAPI.Shipped.txt | 1 + .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 2 + src/RESP.Core/README.md | 3 + src/RESP.Core/RESP.Core.csproj | 64 + src/RESP.Core/Raw.cs | 139 ++ src/RESP.Core/RespAttributeReader.cs | 74 + src/RESP.Core/RespCommandAttribute.cs | 36 + src/RESP.Core/RespConnectionExtensions.cs | 287 +++ src/RESP.Core/RespConnectionPool.cs | 189 ++ src/RESP.Core/RespConstants.cs | 52 + src/RESP.Core/RespException.cs | 10 + src/RESP.Core/RespFormatters.cs | 61 + src/RESP.Core/RespFrameScanner.cs | 193 ++ src/RESP.Core/RespParsers.cs | 177 ++ src/RESP.Core/RespPayload.cs | 671 +++++++ src/RESP.Core/RespPrefix.cs | 97 + .../RespReader.AggregateEnumerator.cs | 196 ++ src/RESP.Core/RespReader.Debug.cs | 33 + src/RESP.Core/RespReader.ScalarEnumerator.cs | 107 ++ src/RESP.Core/RespReader.Span.cs | 85 + src/RESP.Core/RespReader.Utils.cs | 318 ++++ src/RESP.Core/RespReader.cs | 1599 +++++++++++++++++ src/RESP.Core/RespReaderExtensions.cs | 142 ++ src/RESP.Core/RespReaders.cs | 341 ++++ src/RESP.Core/RespScanState.cs | 161 ++ src/RESP.Core/RespWriter.cs | 913 ++++++++++ src/RESP.Core/ResponseReader.cs | 54 + src/RESP.Core/Void.cs | 7 + src/RESPite.Redis/Alt/DownlevelExtensions.cs | 16 + src/RESPite.Redis/Formatters.cs | 9 + src/RESPite.Redis/KeyStringArrayFormatter.cs | 18 + src/RESPite.Redis/RESPite.Redis.csproj | 23 + src/RESPite.Redis/RedisExtensions.cs | 22 + src/RESPite.Redis/RedisKeys.cs | 16 + src/RESPite.Redis/RedisStrings.cs | 99 + .../RESPite.StackExchange.Redis.csproj | 20 + src/RESPite/Alt/DownlevelExtensions.cs | 10 + src/RESPite/Connections/BatchConnection.cs | 219 +++ src/RESPite/IRespBatch.cs | 7 + src/RESPite/IRespConnection.cs | 19 + src/RESPite/Internal/ActivationHelper.cs | 115 ++ src/RESPite/Internal/CycleBuffer.cs | 705 ++++++++ src/RESPite/Internal/DebugCounters.cs | 182 ++ src/RESPite/Internal/IRespInlineParser.cs | 5 + src/RESPite/Internal/IRespMessage.cs | 18 + src/RESPite/Internal/Raw.cs | 138 ++ src/RESPite/Internal/RespConstants.cs | 51 + src/RESPite/Internal/RespMessageBase.cs | 436 +++++ src/RESPite/Internal/RespMessageBase_Typed.cs | 30 + .../RespMessageBase_Typed_Stateful.cs | 33 + .../Internal/RespOperationExtensions.cs | 18 + src/RESPite/Internal/StreamConnection.cs | 717 ++++++++ src/RESPite/Messages/IRespMetadataParser.cs | 10 + src/RESPite/Messages/IRespParser_Typed.cs | 13 + .../Messages/IRespParser_Typed_Stateful.cs | 12 + src/RESPite/Messages/RespAttributeReader.cs | 68 + src/RESPite/Messages/RespFrameScanner.cs | 193 ++ src/RESPite/Messages/RespPrefix.cs | 97 + .../RespReader.AggregateEnumerator.cs | 193 ++ src/RESPite/Messages/RespReader.Debug.cs | 33 + .../Messages/RespReader.ScalarEnumerator.cs | 105 ++ src/RESPite/Messages/RespReader.Span.cs | 84 + src/RESPite/Messages/RespReader.Utils.cs | 317 ++++ src/RESPite/Messages/RespReader.cs | 1598 ++++++++++++++++ src/RESPite/Messages/RespScanState.cs | 160 ++ src/RESPite/RESPite.csproj | 60 + src/RESPite/RespCommandMap.cs | 25 + src/RESPite/RespConfiguration.cs | 89 + src/RESPite/RespContext.cs | 92 + src/RESPite/RespException.cs | 8 + src/RESPite/RespOperation.cs | 137 ++ src/RESPite/RespOperationT.cs | 136 ++ .../ConfigurationOptions.cs | 8 +- .../ConnectionMultiplexer.Debug.cs | 2 +- .../ConnectionMultiplexer.cs | 26 +- src/StackExchange.Redis/ExceptionFactory.cs | 2 +- src/StackExchange.Redis/FrameworkShims.cs | 144 +- .../Interfaces/IConnectionMultiplexer.cs | 600 +++---- src/StackExchange.Redis/Interfaces/IServer.cs | 22 +- src/StackExchange.Redis/PhysicalBridge.cs | 2 +- src/StackExchange.Redis/PhysicalConnection.cs | 14 +- .../Profiling/ProfilingSession.cs | 2 +- .../PublicAPI/PublicAPI.Shipped.txt | 6 +- src/StackExchange.Redis/RedisDatabase.cs | 11 +- src/StackExchange.Redis/RedisServer.cs | 18 +- src/StackExchange.Redis/ServerEndPoint.cs | 8 +- .../StackExchange.Redis.csproj | 2 +- tests/BasicTest/BasicTest.csproj | 7 +- tests/BasicTest/BenchmarkBase.cs | 539 ++++++ tests/BasicTest/CustomConfig.cs | 30 + tests/BasicTest/Issue898.cs | 79 + tests/BasicTest/NewCoreBenchmark.cs | 338 ++++ tests/BasicTest/OldCoreBenchmark.cs | 190 ++ tests/BasicTest/Program.cs | 311 +--- tests/BasicTest/RedisBenchmarks.cs | 460 +++++ tests/BasicTest/RespBenchmark.md | 352 ++++ tests/BasicTest/SlowConfig.cs | 11 + .../BasicTestBaseline.csproj | 3 +- .../RESP.Core.Tests/BasicIntegrationTests.cs | 97 + tests/RESP.Core.Tests/BufferTests.cs | 182 ++ tests/RESP.Core.Tests/ConnectionFixture.cs | 20 + tests/RESP.Core.Tests/IntegrationTestBase.cs | 22 + tests/RESP.Core.Tests/RESP.Core.Tests.csproj | 24 + .../RedisStringsIntegrationTests.cs | 41 + tests/RedisConfigs/.docker/Redis/Dockerfile | 2 +- .../FailoverTests.cs | 6 +- .../GetServerTests.cs | 150 -- .../Helpers/SharedConnectionFixture.cs | 2 +- tests/StackExchange.Redis.Tests/KeyTests.cs | 4 +- .../StackExchange.Redis.Tests/LoggerTests.cs | 10 +- .../StackExchange.Redis.Tests/PubSubTests.cs | 40 +- tests/StackExchange.Redis.Tests/SSLTests.cs | 2 + .../StackExchange.Redis.Tests/StreamTests.cs | 22 - .../SyncContextTests.cs | 4 +- 143 files changed, 19240 insertions(+), 863 deletions(-) create mode 100644 eng/StackExchange.Redis.Build/RespCommandGenerator.cs create mode 100644 eng/StackExchange.Redis.Build/RespCommandGenerator.md create mode 100644 eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj create mode 100644 src/RESP.Core/AmbientBufferWriter.cs create mode 100644 src/RESP.Core/BatchConnection.cs create mode 100644 src/RESP.Core/Builder.cs create mode 100644 src/RESP.Core/CustomNetworkStream.cs create mode 100644 src/RESP.Core/CycleBuffer.Simple.cs create mode 100644 src/RESP.Core/CycleBuffer.cs create mode 100644 src/RESP.Core/DebugCounters.cs create mode 100644 src/RESP.Core/DirectWriteConnection.cs create mode 100644 src/RESP.Core/FrameScanInfo.cs create mode 100644 src/RESP.Core/Global.cs create mode 100644 src/RESP.Core/IRespConnection.cs create mode 100644 src/RESP.Core/IRespReader.cs create mode 100644 src/RESP.Core/Message.cs create mode 100644 src/RESP.Core/PipelinedConnection.cs create mode 100644 src/RESP.Core/PublicAPI/PublicAPI.Shipped.txt create mode 100644 src/RESP.Core/PublicAPI/PublicAPI.Unshipped.txt create mode 100644 src/RESP.Core/PublicAPI/net5.0/PublicAPI.Shipped.txt create mode 100644 src/RESP.Core/PublicAPI/net5.0/PublicAPI.Unshipped.txt create mode 100644 src/RESP.Core/PublicAPI/net7.0/PublicAPI.Shipped.txt create mode 100644 src/RESP.Core/PublicAPI/net7.0/PublicAPI.Unshipped.txt create mode 100644 src/RESP.Core/PublicAPI/net8.0/PublicAPI.Shipped.txt create mode 100644 src/RESP.Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt create mode 100644 src/RESP.Core/README.md create mode 100644 src/RESP.Core/RESP.Core.csproj create mode 100644 src/RESP.Core/Raw.cs create mode 100644 src/RESP.Core/RespAttributeReader.cs create mode 100644 src/RESP.Core/RespCommandAttribute.cs create mode 100644 src/RESP.Core/RespConnectionExtensions.cs create mode 100644 src/RESP.Core/RespConnectionPool.cs create mode 100644 src/RESP.Core/RespConstants.cs create mode 100644 src/RESP.Core/RespException.cs create mode 100644 src/RESP.Core/RespFormatters.cs create mode 100644 src/RESP.Core/RespFrameScanner.cs create mode 100644 src/RESP.Core/RespParsers.cs create mode 100644 src/RESP.Core/RespPayload.cs create mode 100644 src/RESP.Core/RespPrefix.cs create mode 100644 src/RESP.Core/RespReader.AggregateEnumerator.cs create mode 100644 src/RESP.Core/RespReader.Debug.cs create mode 100644 src/RESP.Core/RespReader.ScalarEnumerator.cs create mode 100644 src/RESP.Core/RespReader.Span.cs create mode 100644 src/RESP.Core/RespReader.Utils.cs create mode 100644 src/RESP.Core/RespReader.cs create mode 100644 src/RESP.Core/RespReaderExtensions.cs create mode 100644 src/RESP.Core/RespReaders.cs create mode 100644 src/RESP.Core/RespScanState.cs create mode 100644 src/RESP.Core/RespWriter.cs create mode 100644 src/RESP.Core/ResponseReader.cs create mode 100644 src/RESP.Core/Void.cs create mode 100644 src/RESPite.Redis/Alt/DownlevelExtensions.cs create mode 100644 src/RESPite.Redis/Formatters.cs create mode 100644 src/RESPite.Redis/KeyStringArrayFormatter.cs create mode 100644 src/RESPite.Redis/RESPite.Redis.csproj create mode 100644 src/RESPite.Redis/RedisExtensions.cs create mode 100644 src/RESPite.Redis/RedisKeys.cs create mode 100644 src/RESPite.Redis/RedisStrings.cs create mode 100644 src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj create mode 100644 src/RESPite/Alt/DownlevelExtensions.cs create mode 100644 src/RESPite/Connections/BatchConnection.cs create mode 100644 src/RESPite/IRespBatch.cs create mode 100644 src/RESPite/IRespConnection.cs create mode 100644 src/RESPite/Internal/ActivationHelper.cs create mode 100644 src/RESPite/Internal/CycleBuffer.cs create mode 100644 src/RESPite/Internal/DebugCounters.cs create mode 100644 src/RESPite/Internal/IRespInlineParser.cs create mode 100644 src/RESPite/Internal/IRespMessage.cs create mode 100644 src/RESPite/Internal/Raw.cs create mode 100644 src/RESPite/Internal/RespConstants.cs create mode 100644 src/RESPite/Internal/RespMessageBase.cs create mode 100644 src/RESPite/Internal/RespMessageBase_Typed.cs create mode 100644 src/RESPite/Internal/RespMessageBase_Typed_Stateful.cs create mode 100644 src/RESPite/Internal/RespOperationExtensions.cs create mode 100644 src/RESPite/Internal/StreamConnection.cs create mode 100644 src/RESPite/Messages/IRespMetadataParser.cs create mode 100644 src/RESPite/Messages/IRespParser_Typed.cs create mode 100644 src/RESPite/Messages/IRespParser_Typed_Stateful.cs create mode 100644 src/RESPite/Messages/RespAttributeReader.cs create mode 100644 src/RESPite/Messages/RespFrameScanner.cs create mode 100644 src/RESPite/Messages/RespPrefix.cs create mode 100644 src/RESPite/Messages/RespReader.AggregateEnumerator.cs create mode 100644 src/RESPite/Messages/RespReader.Debug.cs create mode 100644 src/RESPite/Messages/RespReader.ScalarEnumerator.cs create mode 100644 src/RESPite/Messages/RespReader.Span.cs create mode 100644 src/RESPite/Messages/RespReader.Utils.cs create mode 100644 src/RESPite/Messages/RespReader.cs create mode 100644 src/RESPite/Messages/RespScanState.cs create mode 100644 src/RESPite/RESPite.csproj create mode 100644 src/RESPite/RespCommandMap.cs create mode 100644 src/RESPite/RespConfiguration.cs create mode 100644 src/RESPite/RespContext.cs create mode 100644 src/RESPite/RespException.cs create mode 100644 src/RESPite/RespOperation.cs create mode 100644 src/RESPite/RespOperationT.cs create mode 100644 tests/BasicTest/BenchmarkBase.cs create mode 100644 tests/BasicTest/CustomConfig.cs create mode 100644 tests/BasicTest/Issue898.cs create mode 100644 tests/BasicTest/NewCoreBenchmark.cs create mode 100644 tests/BasicTest/OldCoreBenchmark.cs create mode 100644 tests/BasicTest/RedisBenchmarks.cs create mode 100644 tests/BasicTest/RespBenchmark.md create mode 100644 tests/BasicTest/SlowConfig.cs create mode 100644 tests/RESP.Core.Tests/BasicIntegrationTests.cs create mode 100644 tests/RESP.Core.Tests/BufferTests.cs create mode 100644 tests/RESP.Core.Tests/ConnectionFixture.cs create mode 100644 tests/RESP.Core.Tests/IntegrationTestBase.cs create mode 100644 tests/RESP.Core.Tests/RESP.Core.Tests.csproj create mode 100644 tests/RESP.Core.Tests/RedisStringsIntegrationTests.cs delete mode 100644 tests/StackExchange.Redis.Tests/GetServerTests.cs diff --git a/Directory.Build.props b/Directory.Build.props index 9f512d5e9..35291c5d8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,8 @@ + + true + 2.0.0 2014 - $([System.DateTime]::Now.Year) Stack Exchange, Inc. true @@ -26,8 +29,15 @@ true false true + true + false + true 00240000048000009400000006020000002400005253413100040000010001007791a689e9d8950b44a9a8886baad2ea180e7a8a854f158c9b98345ca5009cdd2362c84f368f1c3658c132b3c0f74e44ff16aeb2e5b353b6e0fe02f923a050470caeac2bde47a2238a9c7125ed7dab14f486a5a64558df96640933b9f2b6db188fc4a820f96dce963b662fa8864adbff38e5b4542343f162ecdc6dad16912fff + + preview + $(DefineConstants);PREVIEW_LANGVER + true true diff --git a/Directory.Packages.props b/Directory.Packages.props index 79c404dc2..6d0a8b05f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,13 @@ + + + + + + diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index 8f772ae42..2ade781b0 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -122,6 +122,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "docs", "docs\docs.csproj", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackExchange.Redis.Benchmarks", "tests\StackExchange.Redis.Benchmarks\StackExchange.Redis.Benchmarks.csproj", "{59889284-FFEE-82E7-94CB-3B43E87DA6CF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RESP.Core", "src\RESP.Core\RESP.Core.csproj", "{E50EEB8B-6B3F-4C8C-A5C6-C37FB87C01E2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RESP.Core.Tests", "tests\RESP.Core.Tests\RESP.Core.Tests.csproj", "{7063E2D3-C591-4604-A5DD-32D4A1678A58}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{C0132984-68D1-4A97-8F8C-AD4E2EECC583}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackExchange.Redis.Build", "eng\StackExchange.Redis.Build\StackExchange.Redis.Build.csproj", "{B0055B76-4685-4ECF-A904-88EE4E6FC8F0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RESPite", "src\RESPite\RESPite.csproj", "{F8762EE5-3461-4F6B-8C24-C876B6D9E637}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RESPite.Redis", "src\RESPite.Redis\RESPite.Redis.csproj", "{3A92C2E7-3033-4FDF-8DDC-5DF43D290537}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RESPite.StackExchange.Redis", "src\RESPite.StackExchange.Redis\RESPite.StackExchange.Redis.csproj", "{A5580114-C236-494E-851C-A21E3DB86FC8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -180,6 +194,30 @@ Global {59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Debug|Any CPU.Build.0 = Debug|Any CPU {59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Release|Any CPU.ActiveCfg = Release|Any CPU {59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Release|Any CPU.Build.0 = Release|Any CPU + {E50EEB8B-6B3F-4C8C-A5C6-C37FB87C01E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E50EEB8B-6B3F-4C8C-A5C6-C37FB87C01E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E50EEB8B-6B3F-4C8C-A5C6-C37FB87C01E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E50EEB8B-6B3F-4C8C-A5C6-C37FB87C01E2}.Release|Any CPU.Build.0 = Release|Any CPU + {7063E2D3-C591-4604-A5DD-32D4A1678A58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7063E2D3-C591-4604-A5DD-32D4A1678A58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7063E2D3-C591-4604-A5DD-32D4A1678A58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7063E2D3-C591-4604-A5DD-32D4A1678A58}.Release|Any CPU.Build.0 = Release|Any CPU + {B0055B76-4685-4ECF-A904-88EE4E6FC8F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0055B76-4685-4ECF-A904-88EE4E6FC8F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0055B76-4685-4ECF-A904-88EE4E6FC8F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0055B76-4685-4ECF-A904-88EE4E6FC8F0}.Release|Any CPU.Build.0 = Release|Any CPU + {F8762EE5-3461-4F6B-8C24-C876B6D9E637}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8762EE5-3461-4F6B-8C24-C876B6D9E637}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8762EE5-3461-4F6B-8C24-C876B6D9E637}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8762EE5-3461-4F6B-8C24-C876B6D9E637}.Release|Any CPU.Build.0 = Release|Any CPU + {3A92C2E7-3033-4FDF-8DDC-5DF43D290537}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A92C2E7-3033-4FDF-8DDC-5DF43D290537}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A92C2E7-3033-4FDF-8DDC-5DF43D290537}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A92C2E7-3033-4FDF-8DDC-5DF43D290537}.Release|Any CPU.Build.0 = Release|Any CPU + {A5580114-C236-494E-851C-A21E3DB86FC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5580114-C236-494E-851C-A21E3DB86FC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5580114-C236-494E-851C-A21E3DB86FC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5580114-C236-494E-851C-A21E3DB86FC8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -202,6 +240,12 @@ Global {A0F89B8B-32A3-4C28-8F1B-ADE343F16137} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} {69A0ACF2-DF1F-4F49-B554-F732DCA938A3} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} {59889284-FFEE-82E7-94CB-3B43E87DA6CF} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} + {E50EEB8B-6B3F-4C8C-A5C6-C37FB87C01E2} = {00CA0876-DA9F-44E8-B0DC-A88716BF347A} + {7063E2D3-C591-4604-A5DD-32D4A1678A58} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} + {B0055B76-4685-4ECF-A904-88EE4E6FC8F0} = {C0132984-68D1-4A97-8F8C-AD4E2EECC583} + {F8762EE5-3461-4F6B-8C24-C876B6D9E637} = {00CA0876-DA9F-44E8-B0DC-A88716BF347A} + {3A92C2E7-3033-4FDF-8DDC-5DF43D290537} = {00CA0876-DA9F-44E8-B0DC-A88716BF347A} + {A5580114-C236-494E-851C-A21E3DB86FC8} = {00CA0876-DA9F-44E8-B0DC-A88716BF347A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {193AA352-6748-47C1-A5FC-C9AA6B5F000B} diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 4621190d2..3a50f1ef3 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -6,17 +6,13 @@ Current package versions: | ------------ | ----------------- | ----- | | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | -## Unreleased - -## 2.9.11 +## Unreleased (2.9.xxx) - Add `HGETDEL`, `HGETEX` and `HSETEX` support ([#2863 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2863)) - Fix key-prefix omission in `SetIntersectionLength` and `SortedSet{Combine[WithScores]|IntersectionLength}` ([#2863 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2863)) - Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2638)) - Add support for XPENDING Idle time filter ([#2822 by david-brink-talogy](https://github.com/StackExchange/StackExchange.Redis/pull/2822)) - Improve `double` formatting performance on net8+ ([#2928 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2928)) -- Add `GetServer(RedisKey, ...)` API ([#2936 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2936)) -- Fix error constructing `StreamAdd` message ([#2941 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2941)) ## 2.8.58 diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs new file mode 100644 index 000000000..d32e96153 --- /dev/null +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -0,0 +1,735 @@ +using System.Buffers; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Reflection; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace StackExchange.Redis.Build; + +[Generator(LanguageNames.CSharp)] +public class RespCommandGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var literals = context.SyntaxProvider + .CreateSyntaxProvider(Predicate, Transform) + .Where(pair => pair.MethodName is { Length: > 0 }) + .Collect(); + + context.RegisterSourceOutput(literals, Generate); + } + + private bool Predicate(SyntaxNode node, CancellationToken cancellationToken) + { + // looking for [FastHash] partial static class Foo { } + if (node is MethodDeclarationSyntax decl + && decl.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + foreach (var attribList in decl.AttributeLists) + { + foreach (var attrib in attribList.Attributes) + { + if (attrib.Name.ToString() is "RespCommandAttribute" or "RespCommand") return true; + } + } + } + + return false; + } + + private static string GetFullName(ITypeSymbol type) => + type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + private static string GetName(ITypeSymbol type) + { + if (type.ContainingType is null) return type.Name; + var stack = new Stack(); + while (true) + { + stack.Push(type.Name); + if (type.ContainingType is null) break; + type = type.ContainingType; + } + + var sb = new StringBuilder(stack.Pop()); + while (stack.Count != 0) + { + sb.Append('.').Append(stack.Pop()); + } + + return sb.ToString(); + } + + private (string Namespace, string TypeName, string ReturnType, string MethodName, string Command, + ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> Parameters, string + TypeModifiers, string + MethodModifiers, string Context, string? Formatter, string? Parser) Transform( + GeneratorSyntaxContext ctx, + CancellationToken cancellationToken) + { + // extract the name and value (defaults to name, but can be overridden via attribute) and the location + if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not IMethodSymbol method) return default; + if (!(method.IsPartialDefinition && method.PartialImplementationPart is null)) return default; + + string returnType; + if (method.ReturnsVoid) + { + returnType = ""; + } + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + else if (method.ReturnType is null) + { + return default; + } + else + { + returnType = GetFullName(method.ReturnType); + } + + string ns = "", parentType = ""; + if (method.ContainingType is { } containingType) + { + parentType = GetName(containingType); + ns = containingType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); + } + else if (method.ContainingNamespace is { } containingNamespace) + { + ns = containingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); + } + + string value = method.Name.ToLowerInvariant(); + string? formatter = null, parser = null; + foreach (var attrib in method.GetAttributes()) + { + if (attrib.AttributeClass?.Name == "RespCommandAttribute") + { + if (attrib.ConstructorArguments.Length == 1) + { + if (attrib.ConstructorArguments[0].Value?.ToString() is { Length: > 0 } val) + { + value = val; + break; + } + + foreach (var tuple in attrib.NamedArguments) + { + switch (tuple.Key) + { + case "Formatter": + formatter = tuple.Value.Value?.ToString(); + break; + case "Parser": + parser = tuple.Value.Value?.ToString(); + break; + } + } + } + } + } + + var parameters = + ImmutableArray.CreateBuilder<(string Type, string Name, string Modifiers, ParameterFlags Flags)>( + method.Parameters.Length); + + // get context from the available fields + string? context = null; + + foreach (var param in method.Parameters) + { + if (IsRespContext(param.Type)) + { + context = param.Name; + break; + } + } + + foreach (var member in method.ContainingType.GetMembers()) + { + if (member is IFieldSymbol { IsStatic: false } field && IsRespContext(field.Type)) + { + context = field.Name; + break; + } + } + + if (context is null) + { + // get context from primary constructor (actually, we look at all constructors, + // and just hope that the one that matches: works!) + foreach (var ctor in method.ContainingType.Constructors) + { + if (ctor.IsStatic) continue; + foreach (var param in ctor.Parameters) + { + if (IsRespContext(param.Type)) + { + context = param.Name; + break; + } + } + + if (context is not null) break; + } + } + + if (context is null) + { + // last ditch, get context from properties + foreach (var member in method.ContainingType.GetMembers()) + { + if (member is IPropertySymbol { IsStatic: false } prop + && IsRespContext(prop.Type) && prop.GetMethod is not null) + { + context = prop.Name; + break; + } + } + } + + static bool Ignore(ITypeSymbol symbol) => IsRespContext(symbol); // CT etc? + + foreach (var param in method.Parameters) + { + var flags = ParameterFlags.Parameter; + if (IsKey(param)) flags |= ParameterFlags.Key; + if (!Ignore(param.Type)) + { + flags |= ParameterFlags.Data; + } + + string modifiers = param.RefKind switch + { + RefKind.None => "", + RefKind.In => "in ", + RefKind.Out => "out ", + RefKind.Ref => "ref ", + _ => "", + }; + + if (param.Ordinal == 0 && method.IsExtensionMethod) + { + modifiers = "this " + modifiers; + } + + parameters.Add((GetFullName(param.Type), param.Name, modifiers, flags)); + } + + static bool IsRespContext(ITypeSymbol type) + => type is INamedTypeSymbol + { + Name: "RespContext", + ContainingNamespace: { Name: "Resp", ContainingNamespace.IsGlobalNamespace: true } + }; + + var syntax = (MethodDeclarationSyntax)ctx.Node; + return (ns, parentType, returnType, method.Name, value, parameters.ToImmutable(), + TypeModifiers(method.ContainingType), syntax.Modifiers.ToString(), context ?? "", formatter, parser); + + static string TypeModifiers(ITypeSymbol type) + { + foreach (var symbol in type.DeclaringSyntaxReferences) + { + var syntax = symbol.GetSyntax(); + if (syntax is TypeDeclarationSyntax typeDeclaration) + { + var mods = typeDeclaration.Modifiers.ToString(); + return syntax switch + { + InterfaceDeclarationSyntax => $"{mods} interface", + StructDeclarationSyntax => $"{mods} struct", + _ => $"{mods} class", + }; + } + } + + return "class"; // wut? + } + } + + private bool IsKey(IParameterSymbol param) + { + if (param.Name.EndsWith("key", StringComparison.InvariantCultureIgnoreCase)) + { + return true; + } + + foreach (var attrib in param.GetAttributes()) + { + if (attrib.AttributeClass?.Name == "KeyAttribute") return true; + } + + return false; + } + + private string GetVersion() + { + var asm = GetType().Assembly; + if (asm.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false).FirstOrDefault() is + AssemblyFileVersionAttribute { Version: { Length: > 0 } } version) + { + return version.Version; + } + + return asm.GetName().Version?.ToString() ?? "??"; + } + + private void Generate( + SourceProductionContext ctx, + ImmutableArray<(string Namespace, string TypeName, string ReturnType, string MethodName, string Command, + ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> Parameters, string + TypeModifiers, + string + MethodModifiers, string Context, string? Formatter, string? Parser)> methods) + { + if (methods.IsDefaultOrEmpty) return; + + var sb = new StringBuilder("// ") + .AppendLine().Append("// ").Append(GetType().Name).Append(" v").Append(GetVersion()).AppendLine(); + + bool first; + int indent = 0; + + // find the unique param types, so we can build helpers + Dictionary, (string Name, + bool Shared)> + formatters = + new(FormatterComparer.Default); + static bool IsThis(string modifier) => modifier.StartsWith("this "); + + foreach (var method in methods) + { + if (method.Formatter is not null || DataParameterCount(method.Parameters) < 2) + { + continue; // consumer should add their own extension method for the target type + } + + var key = method.Parameters; + if (!formatters.TryGetValue(key, out var existing)) + { + formatters.Add(key, ($"__RespFormatter_{formatters.Count}", false)); + } + else if (!existing.Shared) + { + formatters[key] = (existing.Name, true); // mark shared + } + } + + StringBuilder NewLine() => sb.AppendLine().Append(' ', Math.Max(indent * 4, 0)); + NewLine().Append("using System;"); + NewLine().Append("using System.Threading.Tasks;"); + foreach (var grp in methods.GroupBy(l => (l.Namespace, l.TypeName, l.TypeModifiers))) + { + NewLine(); + int braces = 0; + if (!string.IsNullOrWhiteSpace(grp.Key.Namespace)) + { + NewLine().Append("namespace ").Append(grp.Key.Namespace); + NewLine().Append("{"); + indent++; + braces++; + } + + if (!string.IsNullOrWhiteSpace(grp.Key.TypeName)) + { + if (grp.Key.TypeName.Contains('.')) // nested types + { + var toks = grp.Key.TypeName.Split('.'); + for (var i = 0; i < toks.Length; i++) + { + var part = toks[i]; + if (i == toks.Length - 1) + { + NewLine().Append(grp.Key.TypeModifiers).Append(' ').Append(part); + } + else + { + NewLine().Append("partial class ").Append(part); + } + + NewLine().Append("{"); + indent++; + braces++; + } + } + else + { + NewLine().Append(grp.Key.TypeModifiers).Append(' ').Append(grp.Key.TypeName); + NewLine().Append("{"); + indent++; + braces++; + } + } + + foreach (var method in grp) + { + bool isSharedFormatter = false; + string? formatter = method.Formatter + ?? InbuiltFormatter(method.Parameters); + if (formatter is null && formatters.TryGetValue(method.Parameters, out var tmp)) + { + formatter = $"{tmp.Name}.Default"; + isSharedFormatter = tmp.Shared; + } + + // perform string escaping on the generated value (this includes the quotes, note) + var csValue = SyntaxFactory + .LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(method.Command)) + .ToFullString(); + + WriteMethod(false); + WriteMethod(true); + + void WriteMethod(bool asAsync) + { + sb = NewLine().Append(asAsync ? RemovePartial(method.MethodModifiers) : method.MethodModifiers) + .Append(' '); + if (asAsync) + { + sb.Append("ValueTask"); + if (!string.IsNullOrWhiteSpace(method.ReturnType)) + { + sb.Append('<').Append(method.ReturnType).Append('>'); + } + } + else + { + sb.Append(string.IsNullOrEmpty(method.ReturnType) ? "void" : method.ReturnType); + } + + sb.Append(' ').Append(method.MethodName).Append(asAsync ? "Async" : "").Append("("); + first = true; + foreach (var param in method.Parameters) + { + if ((param.Flags & ParameterFlags.Parameter) == 0) continue; + if (!first) sb.Append(", "); + first = false; + + sb.Append(param.Modifiers).Append(param.Type).Append(' ').Append(param.Name); + } + + var dataParameters = DataParameterCount(method.Parameters); + sb.Append(")"); + indent++; + + var parser = method.Parser ?? InbuiltParser(method.ReturnType); + bool useDirectCall = method.Context is { Length: > 0 } & formatter is { Length: > 0 } & + parser is { Length: > 0 }; + + if (string.IsNullOrWhiteSpace(method.Context)) + { + NewLine().Append("=> throw new NotSupportedException(\"No RespContext available\");"); + useDirectCall = false; + } + else if (!(useDirectCall & asAsync)) + { + sb = NewLine(); + if (useDirectCall) sb.Append("// "); + sb.Append("=> ").Append(method.Context).Append(".Command(").Append(csValue).Append("u8"); + if (dataParameters != 0) + { + sb.Append(", "); + WriteTuple(method.Parameters, sb, TupleMode.Values); + + if (!string.IsNullOrWhiteSpace(formatter)) + { + sb.Append(", ").Append(formatter); + } + } + + sb.Append(asAsync ? ").AsValueTask" : ").Wait"); + if (!string.IsNullOrWhiteSpace(method.ReturnType)) + { + sb.Append('<').Append(method.ReturnType).Append('>'); + } + + sb.Append("(").Append(parser).Append(");"); + } + + if (useDirectCall) // avoid the intermediate step when possible + { + sb = NewLine().Append("=> global::Resp.Message.Send").Append(asAsync ? "Async" : "") + .Append('<'); + WriteTuple( + method.Parameters, + sb, + isSharedFormatter ? TupleMode.SyntheticNames : TupleMode.NamedTuple); + sb.Append(", ").Append(method.ReturnType).Append(">(").Append(method.Context).Append(", ") + .Append(csValue).Append("u8").Append(", "); + WriteTuple(method.Parameters, sb, TupleMode.Values); + sb.Append(", ").Append(formatter).Append(", ").Append(parser).Append(");"); + } + + indent--; + NewLine(); + } + } + + // handle any closing braces + while (braces-- > 0) + { + indent--; + NewLine().Append("}"); + } + + NewLine(); + } + + foreach (var tuple in formatters) + { + var parameters = tuple.Key; + var name = tuple.Value.Name; + var names = tuple.Value.Shared ? TupleMode.SyntheticNames : TupleMode.NamedTuple; + + NewLine(); + sb = NewLine().Append("sealed file class ").Append(name).Append(" : Resp.IRespFormatter<"); + WriteTuple(parameters, sb, names); + sb.Append('>'); + NewLine().Append("{"); + indent++; + NewLine().Append("public static readonly ").Append(name).Append(" Default = new();"); + NewLine(); + + sb = NewLine() + .Append("public void Format(scoped ReadOnlySpan command, ref Resp.RespWriter writer, in "); + WriteTuple(parameters, sb, names); + sb.Append(" request)"); + NewLine().Append("{"); + indent++; + var count = DataParameterCount(parameters); + sb = NewLine().Append("writer.WriteCommand(command, ").Append(count); + sb.Append(");"); + if (count == 1) + { + NewLine().Append("writer.WriteBulkString(request);"); + } + else + { + int index = 0; + foreach (var parameter in parameters) + { + if ((parameter.Flags & ParameterFlags.DataParameter) == ParameterFlags.DataParameter) + { + sb = NewLine().Append("writer.") + .Append((parameter.Flags & ParameterFlags.Key) == 0 ? "WriteBulkString" : "WriteKey") + .Append("(request."); + if (names == TupleMode.SyntheticNames) + { + sb.Append("Arg").Append(index); + } + else + { + sb.Append(parameter.Name); + } + + sb.Append(");"); + index++; + } + } + + Debug.Assert(index == count, "wrote all parameters"); + } + + indent--; + NewLine().Append("}"); + indent--; + NewLine().Append("}"); + } + + NewLine(); + ctx.AddSource(GetType().Name + ".generated.cs", sb.ToString()); + + static void WriteTuple( + ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> parameters, + StringBuilder sb, + TupleMode mode) + { + var count = DataParameterCount(parameters); + if (count == 0) return; + if (count < 2) + { + var p = FirstDataParameter(parameters); + if (IsThis(p.Modifiers)) + { + p = parameters[1]; + } + + sb.Append(mode == TupleMode.Values ? p.Name : p.Type); + return; + } + + sb.Append('('); + int index = 0; + foreach (var param in parameters) + { + if ((param.Flags & ParameterFlags.DataParameter) != ParameterFlags.DataParameter) + { + continue; // note don't increase index + } + + if (index != 0) sb.Append(", "); + + switch (mode) + { + case TupleMode.Values: + sb.Append(param.Name); + break; + case TupleMode.AnonTuple: + sb.Append(param.Type); + break; + case TupleMode.NamedTuple: + sb.Append(param.Type).Append(' ').Append(param.Name); + break; + case TupleMode.SyntheticNames: + sb.Append(param.Type).Append(" Arg").Append(index); + break; + } + + index++; + } + + sb.Append(')'); + } + } + + private static string? InbuiltFormatter( + ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> parameters) + { + if (DataParameterCount(parameters) == 1) + { + var p = FirstDataParameter(parameters); + return InbuiltFormatter(p.Type, (p.Flags & ParameterFlags.Key) != 0); + } + + return null; + } + + private static (string Type, string Name, string Modifiers, ParameterFlags Flags) FirstDataParameter( + ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> parameters) + { + if (!parameters.IsDefaultOrEmpty) + { + foreach (var parameter in parameters) + { + if ((parameter.Flags & ParameterFlags.DataParameter) == ParameterFlags.DataParameter) + { + return parameter; + } + } + } + + return Array.Empty<(string Type, string Name, string Modifiers, ParameterFlags Flags)>().First(); + } + + private static int DataParameterCount( + ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> parameters) + { + if (parameters.IsDefaultOrEmpty) return 0; + int count = 0; + foreach (var parameter in parameters) + { + if ((parameter.Flags & ParameterFlags.DataParameter) == ParameterFlags.DataParameter) + { + count++; + } + } + + return count; + } + + private static string? InbuiltFormatter(string type, bool isKey) => type switch + { + "string" => isKey ? "Resp.RespFormatters.Key.String" : "Resp.RespFormatters.Value.String", + "byte[]" => isKey ? "Resp.RespFormatters.Key.ByteArray" : "Resp.RespFormatters.Value.ByteArray", + "int" => "Resp.RespFormatters.Int32", + "long" => "Resp.RespFormatters.Int64", + "float" => "Resp.RespFormatters.Single", + "double" => "Resp.RespFormatters.Double", + _ => null, + }; + + private static string? InbuiltParser(string type, bool explicitSuccess = false) => type switch + { + "" when explicitSuccess => "Resp.RespParsers.Success", + "string" => "Resp.RespParsers.String", + "int" => "Resp.RespParsers.Int32", + "long" => "Resp.RespParsers.Int64", + "float" => "Resp.RespParsers.Single", + "double" => "Resp.RespParsers.Double", + "int?" => "Resp.RespParsers.NullableInt32", + "long?" => "Resp.RespParsers.NullableInt64", + "float?" => "Resp.RespParsers.NullableSingle", + "double?" => "Resp.RespParsers.NullableDouble", + "global::Resp.ResponseSummary" => "Resp.ResponseSummary.Parser", + _ => null, + }; + + private enum TupleMode + { + AnonTuple, + NamedTuple, + Values, + SyntheticNames, + } + + private static string RemovePartial(string modifiers) + { + if (string.IsNullOrWhiteSpace(modifiers) || !modifiers.Contains("partial")) return modifiers; + if (modifiers == "partial") return ""; + if (modifiers.StartsWith("partial ")) return modifiers.Substring(8); + if (modifiers.EndsWith(" partial")) return modifiers.Substring(0, modifiers.Length - 8); + return modifiers.Replace(" partial ", " "); + } + + [Flags] + private enum ParameterFlags + { + None = 0, + Parameter = 1 << 0, + Data = 1 << 1, + DataParameter = Data | Parameter, + Key = 1 << 2, + Literal = 1 << 3, + } + + // compares whether a formatter can be shared, which depends on the key index and types (not names) + private sealed class + FormatterComparer + : IEqualityComparer> + { + private FormatterComparer() { } + public static readonly FormatterComparer Default = new(); + + public bool Equals( + ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> x, + ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> y) + { + if (x.Length != y.Length) return false; + for (int i = 0; i < x.Length; i++) + { + var px = x[i]; + var py = y[i]; + if (px.Type != py.Type || px.Flags != py.Flags) return false; + // literals need to match by name too + if ((px.Flags & ParameterFlags.Literal) != 0 + && px.Name != py.Name) return false; + } + + return true; + } + + public int GetHashCode( + ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> obj) + { + var hash = obj.Length; + foreach (var p in obj) + { + hash ^= p.Type.GetHashCode() ^ (int)p.Flags; + } + + return hash; + } + } +} diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.md b/eng/StackExchange.Redis.Build/RespCommandGenerator.md new file mode 100644 index 000000000..1a3ec5fbc --- /dev/null +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.md @@ -0,0 +1,18 @@ +# RespCommandGenerator + +Emit basic RESP command bodies. + +The purpose of this generator is to interpret inputs like: + +``` c# +[RespCommand] // optional: include explicit command text +public int void Foo(string key, int delta, double x); +``` + +and implement the relevant sync and async core logic, including +implementing a custom `IRespFormatter<(string, int, double)>`. Note that +the formatter can be reused between commands, so the names are not used internally. + +Note that parameters named `key` are detected automatically for sharding purposes; +when this is not suitable,`[Key]` can be used instead to denote a parameter to use +for sharding - for example `partial void Rename([Key] string fromKey, string toKey)`. \ No newline at end of file diff --git a/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj new file mode 100644 index 000000000..c7b4f6506 --- /dev/null +++ b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.0 + enable + enable + true + + + + + + + diff --git a/global.json b/global.json index 35e954767..f00fd8fcc 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "allowPrerelease": false + "allowPrerelease": true } } \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 29eadff61..5fb9cc5c5 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -6,8 +6,11 @@ false - + + + + diff --git a/src/RESP.Core/AmbientBufferWriter.cs b/src/RESP.Core/AmbientBufferWriter.cs new file mode 100644 index 000000000..bc64abc75 --- /dev/null +++ b/src/RESP.Core/AmbientBufferWriter.cs @@ -0,0 +1,93 @@ +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Resp; + +internal sealed class AmbientBufferWriter : IBufferWriter +{ + [ThreadStatic] + private static AmbientBufferWriter? _threadStaticInstance; + + public static AmbientBufferWriter Get(int estimatedSize) + { + var obj = _threadStaticInstance ??= new AmbientBufferWriter(); + obj.Init(estimatedSize); + return obj; + } + + private byte[] _buffer = []; + private int _committed; + + private void Init(int size) + { + _committed = 0; + if (size < 0) size = 0; + if (_buffer.Length < size) + { + DemandCapacity(size); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DemandCapacity(int size) + { + const int MIN_BUFFER = 32; + size = Math.Max(size, MIN_BUFFER); + + if (_committed + size > _buffer.Length) + { + GrowBy(size); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowBy(int length) + { + var newSize = Math.Max(_committed + length, checked((_buffer.Length * 3) / 2)); + byte[] newBuffer = ArrayPool.Shared.Rent(newSize), oldBuffer = _buffer; + if (_committed != 0) + { + new ReadOnlySpan(oldBuffer, 0, _committed).CopyTo(newBuffer); + } + + _buffer = newBuffer; + ArrayPool.Shared.Return(oldBuffer); + } + + internal byte[] Detach(out int length) + { + length = _committed; + if (length == 0) return []; + var result = _buffer; + _buffer = []; + _committed = 0; + return result; + } + + public void Advance(int count) + { + var capacity = _buffer.Length - _committed; + if (count < 0 || count > capacity) Throw(); + { + _committed += count; + } + + static void Throw() => throw new ArgumentOutOfRangeException(nameof(count)); + } + + public Memory GetMemory(int sizeHint = 0) + { + DemandCapacity(sizeHint); + return new(_buffer, _committed, _buffer.Length - _committed); + } + + public Span GetSpan(int sizeHint = 0) + { + DemandCapacity(sizeHint); + return new(_buffer, _committed, _buffer.Length - _committed); + } + + internal void Reset() => _committed = 0; +} diff --git a/src/RESP.Core/BatchConnection.cs b/src/RESP.Core/BatchConnection.cs new file mode 100644 index 000000000..29ce1dfef --- /dev/null +++ b/src/RESP.Core/BatchConnection.cs @@ -0,0 +1,229 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace Resp; + +public interface IBatchConnection : IRespConnection +{ + Task FlushAsync(); + void Flush(); +} + +internal sealed class BatchConnection : IBatchConnection +{ + private bool _isDisposed; + private readonly List _unsent; + private readonly IRespConnection _tail; + private readonly RespContext _context; + + public BatchConnection(in RespContext context, int sizeHint) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - an abundance of caution + var tail = context.Connection; + if (tail is not { CanWrite: true }) ThrowNonWritable(); + if (tail is BatchConnection) ThrowBatch(); + + _unsent = sizeHint <= 0 ? [] : new List(sizeHint); + _tail = tail!; + _context = context.WithConnection(this); + static void ThrowBatch() => throw new ArgumentException("Nested batches are not supported", nameof(tail)); + + static void ThrowNonWritable() => + throw new ArgumentException("A writable connection is required", nameof(tail)); + } + + public void Dispose() + { + lock (_unsent) + { + /* everyone else checks disposal inside the lock, so: + once we've set this, we can be sure that no more + items will be added */ + _isDisposed = true; + } +#if NET5_0_OR_GREATER + var span = CollectionsMarshal.AsSpan(_unsent); + foreach (var message in span) + { + message.TrySetException(new ObjectDisposedException(ToString())); + } +#else + foreach (var message in _unsent) + { + message.TrySetException(new ObjectDisposedException(ToString())); + } +#endif + _unsent.Clear(); + } + + public ValueTask DisposeAsync() + { + Dispose(); + return default; + } + + public RespConfiguration Configuration => _tail.Configuration; + public bool CanWrite => _tail.CanWrite; + + public int Outstanding + { + get + { + lock (_unsent) + { + return _unsent.Count; + } + } + } + + public ref readonly RespContext Context => ref _context; + + private const string SyncMessage = "Batch connections do not support synchronous sends"; + public void Send(IRespMessage message) => throw new NotSupportedException(SyncMessage); + + public void Send(ReadOnlySpan messages) => throw new NotSupportedException(SyncMessage); + + private void ThrowIfDisposed() + { + if (_isDisposed) Throw(); + static void Throw() => throw new ObjectDisposedException(nameof(BatchConnection)); + } + + public Task SendAsync(IRespMessage message) + { + lock (_unsent) + { + ThrowIfDisposed(); + _unsent.Add(message); + } + + return Task.CompletedTask; + } + + public Task SendAsync(ReadOnlyMemory messages) + { + if (messages.Length != 0) + { + lock (_unsent) + { + ThrowIfDisposed(); +#if NET8_0_OR_GREATER + _unsent.AddRange(messages.Span); // internally optimized +#else + // two-step; first ensure capacity, then add in loop +#if NET6_0_OR_GREATER + _unsent.EnsureCapacity(_unsent.Count + messages.Length); +#else + var required = _unsent.Count + messages.Length; + if (_unsent.Capacity < required) + { + const int maxLength = 0X7FFFFFC7; // not directly available on down-level runtimes :( + var newCapacity = _unsent.Capacity * 2; // try doubling + if ((uint)newCapacity > maxLength) newCapacity = maxLength; // account for max + if (newCapacity < required) newCapacity = required; // in case doubling wasn't enough + _unsent.Capacity = newCapacity; + } +#endif + foreach (var message in messages.Span) + { + _unsent.Add(message); + } +#endif + } + } + + return Task.CompletedTask; + } + + private int Flush(out IRespMessage[] oversized, out IRespMessage? single) + { + lock (_unsent) + { + var count = _unsent.Count; + switch (count) + { + case 0: + oversized = []; + single = null; + break; + case 1: + oversized = []; + single = _unsent[0]; + break; + default: + oversized = ArrayPool.Shared.Rent(count); + single = null; + _unsent.CopyTo(oversized); + break; + } + + _unsent.Clear(); + return count; + } + } + + public Task FlushAsync() + { + var count = Flush(out var oversized, out var single); + return count switch + { + 0 => Task.CompletedTask, + 1 => _tail.SendAsync(single!), + _ => SendAndRecycleAsync(_tail, oversized, count), + }; + + static async Task SendAndRecycleAsync(IRespConnection tail, IRespMessage[] oversized, int count) + { + try + { + await tail.SendAsync(oversized.AsMemory(0, count)).ConfigureAwait(false); + ArrayPool.Shared.Return(oversized); // only on success, in case captured + } + catch (Exception ex) + { + foreach (var message in oversized.AsSpan(0, count)) + { + message.TrySetException(ex); + } + + throw; + } + } + } + + public void Flush() + { + var count = Flush(out var oversized, out var single); + switch (count) + { + case 0: + return; + case 1: + _tail.Send(single!); + return; + } + + try + { + _tail.Send(oversized.AsSpan(0, count)); + } + catch (Exception ex) + { + foreach (var message in oversized.AsSpan(0, count)) + { + message.TrySetException(ex); + } + + throw; + } + finally + { + // in the sync case, Send takes a span - hence can't have been captured anywhere; always recycle + ArrayPool.Shared.Return(oversized); + } + } +} diff --git a/src/RESP.Core/Builder.cs b/src/RESP.Core/Builder.cs new file mode 100644 index 000000000..0615bd3f9 --- /dev/null +++ b/src/RESP.Core/Builder.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; + +namespace Resp; + +public readonly ref struct RespMessageBuilder(RespContext context, ReadOnlySpan command, TRequest value, IRespFormatter formatter) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif +{ + private readonly ReadOnlySpan _command = command; + private readonly TRequest _value = value; // cannot inline to .ctor because of "allows ref struct" + + public TResponse Wait() + => Message.Send(context, _command, _value, formatter, RespParsers.Get()); + public TResponse Wait(IRespParser parser) + => Message.Send(context, _command, _value, formatter, parser); + + public void Wait() + => Message.Send(context, _command, _value, formatter, RespParsers.Success); + public void Wait(IRespParser parser) + => Message.Send(context, _command, _value, formatter, parser); + + public ValueTask AsValueTask() + => Message.SendAsync(context, _command, _value, formatter, RespParsers.Get()); + public ValueTask AsValueTask(IRespParser parser) + => Message.SendAsync(context, _command, _value, formatter, parser); + + public ValueTask AsValueTask() + => Message.SendAsync(context, _command, _value, formatter, RespParsers.Success); + public ValueTask AsValueTask(IRespParser parser) + => Message.SendAsync(context, _command, _value, formatter, parser); +} diff --git a/src/RESP.Core/CustomNetworkStream.cs b/src/RESP.Core/CustomNetworkStream.cs new file mode 100644 index 000000000..4673ea4ed --- /dev/null +++ b/src/RESP.Core/CustomNetworkStream.cs @@ -0,0 +1,297 @@ +#if NETCOREAPP3_0_OR_GREATER + +using System; +using System.Diagnostics; +using System.IO; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Sources; + +namespace Resp; + +internal sealed class CustomNetworkStream(Socket socket) : Stream +{ + private SocketAwaitableEventArgs _readArgs = new(), _writeArgs = new(); + private SocketAwaitableEventArgs ReadArgs() => _readArgs.Next(); + private SocketAwaitableEventArgs WriteArgs() => _writeArgs.Next(); + + public override void Close() + { + socket.Close(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + socket.Dispose(); + _readArgs.Dispose(); + _writeArgs.Dispose(); + } + + base.Dispose(disposing); + } + + public override void Flush() { } + + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override int Read(byte[] buffer, int offset, int count) => + socket.Receive(buffer, offset, count, SocketFlags.None); + + public override void Write(byte[] buffer, int offset, int count) => + socket.Send(buffer, offset, count, SocketFlags.None); + + public override int Read(Span buffer) => socket.Receive(buffer); + + public override void Write(ReadOnlySpan buffer) => socket.Send(buffer); + + private static void ThrowCancellable() => throw new NotSupportedException( + "Cancellable operations are not supported on this stream; cancellation should be handled at the message level, not the IO level."); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (cancellationToken.CanBeCanceled) ThrowCancellable(); + var args = ReadArgs(); + args.SetBuffer(buffer, offset, count); + if (socket.ReceiveAsync(args)) return args.Pending().AsTask(); + return Task.FromResult(args.GetInlineResult()); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (cancellationToken.CanBeCanceled) ThrowCancellable(); + var args = WriteArgs(); + args.SetBuffer(buffer, offset, count); + if (socket.SendAsync(args)) return args.Pending().AsTask(); + args.GetInlineResult(); // check for socket errors + return Task.CompletedTask; + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (cancellationToken.CanBeCanceled) ThrowCancellable(); + var args = ReadArgs(); + args.SetBuffer(buffer); + if (socket.ReceiveAsync(args)) return args.Pending(); + return new(args.GetInlineResult()); + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + if (cancellationToken.CanBeCanceled) ThrowCancellable(); + var args = WriteArgs(); + args.SetBuffer(MemoryMarshal.AsMemory(buffer)); + if (socket.SendAsync(args)) return args.PendingNoValue(); + args.GetInlineResult(); // check for socket errors + return default; + } + + public override int ReadByte() + { + Span buffer = stackalloc byte[1]; + int count = socket.Receive(buffer); + return count <= 0 ? -1 : buffer[0]; + } + + public override void WriteByte(byte value) + { + ReadOnlySpan buffer = [value]; + socket.Send(buffer); + } + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + { + var args = ReadArgs(); + args.SetBuffer(buffer, offset, count); + args.CompletedSynchronously = false; + if (socket.SendAsync(args)) + { + args.OnCompleted(callback, state); + } + else + { + args.CompletedSynchronously = true; + callback?.Invoke(args); + } + + return args; + } + + public override int EndRead(IAsyncResult asyncResult) => ((SocketAwaitableEventArgs)asyncResult).GetInlineResult(); + + public override IAsyncResult BeginWrite( + byte[] buffer, + int offset, + int count, + AsyncCallback? callback, + object? state) + { + var args = WriteArgs(); + args.SetBuffer(buffer, offset, count); + args.CompletedSynchronously = false; + if (socket.SendAsync(args)) + { + args.OnCompleted(callback, state); + } + else + { + args.CompletedSynchronously = true; + callback?.Invoke(args); + } + + return args; + } + + public override void EndWrite(IAsyncResult asyncResult) => + ((SocketAwaitableEventArgs)asyncResult).GetInlineResult(); + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override bool CanTimeout => socket.ReceiveTimeout != 0 || socket.SendTimeout != 0; + + public override int ReadTimeout + { + get => socket.ReceiveTimeout; + set => socket.ReceiveTimeout = value; + } + + public override int WriteTimeout + { + get => socket.SendTimeout; + set => socket.SendTimeout = value; + } + + // inspired from Pipelines.Sockets.Unofficial and Kestrel's SocketAwaitableEventArgs; extended to support more scenarios + private sealed class SocketAwaitableEventArgs : SocketAsyncEventArgs, + IValueTaskSource, IValueTaskSource, IAsyncResult + { +#if NET5_0_OR_GREATER + public SocketAwaitableEventArgs() : base(unsafeSuppressExecutionContextFlow: true) { } +#else + public SocketAwaitableEventArgs() { } +#endif + private static readonly Action ContinuationCompleted = _ => { }; + + public WaitHandle AsyncWaitHandle => throw new NotSupportedException(); + public bool CompletedSynchronously { get; set; } + private volatile Action? _continuation; + + private object? _asyncCallbackState; // need an additional state here, unless we introduce type-check overhead + object? IAsyncResult.AsyncState => _asyncCallbackState; + private Action? _reusedAsyncCallback; + private Action AsyncCallback => _reusedAsyncCallback ??= OnAsyncCallback; + + public ValueTask Pending() => new(this, _token); + public ValueTask PendingNoValue() => new(this, _token); + private short _token; + + public SocketAwaitableEventArgs Next() + { + unchecked { _token++; } + + return this; + } + + private void ThrowToken() => throw new InvalidOperationException("Invalid token - overlapped IO error?"); + + private void OnAsyncCallback(object? state) + { + if (state is WaitCallback wc) + { + wc(_asyncCallbackState); + } + } + + protected override void OnCompleted(SocketAsyncEventArgs args) + { + Debug.Assert(ReferenceEquals(args, this), "Incorrect SocketAsyncEventArgs"); + var c = _continuation; + + if (c != null || (c = Interlocked.CompareExchange(ref _continuation, ContinuationCompleted, null)) != null) + { + var continuationState = UserToken; + UserToken = null; + _continuation = ContinuationCompleted; // in case someone's polling IsCompleted + + c(continuationState); // note: inline continuation + } + } + + public int GetInlineResult() + { + _continuation = null; + if (SocketError != SocketError.Success) + { + ThrowSocketError(SocketError); + } + + return BytesTransferred; + } + + void IValueTaskSource.GetResult(short token) => GetResult(token); + + public int GetResult(short token) + { + if (token != _token) ThrowToken(); + _continuation = null; + + if (SocketError != SocketError.Success) + { + ThrowSocketError(SocketError); + } + + return BytesTransferred; + } + + private static void ThrowSocketError(SocketError e) => throw new SocketException((int)e); + + public ValueTaskSourceStatus GetStatus(short token) + { + if (token != _token) ThrowToken(); + return !ReferenceEquals(_continuation, ContinuationCompleted) ? ValueTaskSourceStatus.Pending : + SocketError == SocketError.Success ? ValueTaskSourceStatus.Succeeded : + ValueTaskSourceStatus.Faulted; + } + + public bool IsCompleted => ReferenceEquals(_continuation, ContinuationCompleted); + + public void OnCompleted(AsyncCallback? callback, object? state) + { + _asyncCallbackState = state; + OnCompleted(AsyncCallback, callback, _token, ValueTaskSourceOnCompletedFlags.None); + } + + public void OnCompleted( + Action continuation, + object? state, + short token, + ValueTaskSourceOnCompletedFlags flags) + { + if (token != _token) ThrowToken(); + UserToken = state; + var prevContinuation = Interlocked.CompareExchange(ref _continuation, continuation, null); + if (ReferenceEquals(prevContinuation, ContinuationCompleted)) + { + UserToken = null; + ThreadPool.UnsafeQueueUserWorkItem(continuation, state, preferLocal: true); + } + } + } +} +#endif diff --git a/src/RESP.Core/CycleBuffer.Simple.cs b/src/RESP.Core/CycleBuffer.Simple.cs new file mode 100644 index 000000000..e469a0fab --- /dev/null +++ b/src/RESP.Core/CycleBuffer.Simple.cs @@ -0,0 +1,114 @@ +/* +using System; +using System.Buffers; +using System.Diagnostics; + +#pragma warning disable SA1205 // accessibility on partial - for debugging/test practicality + +partial struct CycleBuffer // basic impl for debugging / validation; just uses single-buffer pack-down +{ + private byte[] _buffer; + private int _committed; + + public void DiscardCommitted(long fullyConsumed) + => DiscardCommitted(checked((int)fullyConsumed)); + + public void DiscardCommitted(int fullyConsumed) + { + Debug.Assert(fullyConsumed >= 0 & fullyConsumed <= _committed); + var remaining = _committed - fullyConsumed; + if (remaining != 0) + { + var buffer = _buffer; + buffer.AsSpan(fullyConsumed, remaining).CopyTo(buffer); + } + + _committed -= fullyConsumed; + } + + public ReadOnlySequence GetAllCommitted() + => new(_buffer, 0, _committed); + + public bool TryGetCommitted(out ReadOnlySpan span) + { + span = _buffer.AsSpan(0, _committed); + return true; + } + + public void Release() + { + var buffer = _buffer; + _committed = 0; + _buffer = []; + ArrayPool.Shared.Return(buffer); + } + + public int PageSize { get; } + + private CycleBuffer(int pageSize) + { + _buffer = []; + PageSize = pageSize; + } + + public static CycleBuffer Create(MemoryPool? pool = null, int pageSize = 0) + { + _ = pool; + return new(Math.Max(pageSize, 1024)); + } + + public void Commit(int bytesRead) + { + Debug.Assert(bytesRead >= 0 & bytesRead <= UncommittedAvailable); + _committed += bytesRead; + } + + public Span GetUncommittedSpan(int hint = 1) + { + if (UncommittedAvailable < hint) Grow(hint); + return _buffer.AsSpan(_committed); + } + public Memory GetUncommittedMemory(int hint = 1) + { + if (UncommittedAvailable < hint) Grow(hint); + return _buffer.AsMemory(_committed); + } + + private void Grow(int hint) + { + hint = Math.Max(hint, 128); // at least a reasonable size + var newLength = Math.Max(_committed + hint, _committed * 2); // what we need, or double what we have; the larger + + var newBuffer = ArrayPool.Shared.Rent(newLength); + var oldBuffer = _buffer; + Debug.Assert(newBuffer.Length > oldBuffer.Length, " should have increased"); + oldBuffer.AsSpan(0, _committed).CopyTo(newBuffer); + ArrayPool.Shared.Return(oldBuffer); + _buffer = newBuffer; + } + + public int UncommittedAvailable => _buffer.Length - _committed; + public bool CommittedIsEmpty => _committed == 0; + + public int GetCommittedLength() => _committed; + + public bool TryGetFirstCommittedSpan(bool fullOnly, out ReadOnlySpan span) + { + var buffer = _buffer; + if (fullOnly) + { + if (_committed >= PageSize) + { + span = buffer.AsSpan(0, _committed); + return true; + } + // offer up a reasonable page size + span = default; + return false; + } + + span = buffer.AsSpan(0, _committed); + return _committed != 0; + } +} +*/ diff --git a/src/RESP.Core/CycleBuffer.cs b/src/RESP.Core/CycleBuffer.cs new file mode 100644 index 000000000..d3411bc2c --- /dev/null +++ b/src/RESP.Core/CycleBuffer.cs @@ -0,0 +1,707 @@ +using System; +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +#pragma warning disable SA1205 // accessibility on partial - for debugging/test practicality + +namespace Resp; + +/// +/// Manages the state for a based IO buffer. Unlike Pipe, +/// it is not intended for a separate producer-consumer - there is no thread-safety, and no +/// activation; it just handles the buffers. It is intended to be used as a mutable (non-readonly) +/// field in a type that performs IO; the internal state mutates - it should not be passed around. +/// +/// Notionally, there is an uncommitted area (write) and a committed area (read). Process: +/// - producer loop (*note no concurrency**) +/// - call to get a new scratch +/// - (write to that span) +/// - call to mark complete portions +/// - consumer loop (*note no concurrency**) +/// - call to see if there is a single-span chunk; otherwise +/// - call to get the multi-span chunk +/// - (process none, some, or all of that data) +/// - call to indicate how much data is no longer needed +/// Emphasis: no concurrency! This is intended for a single worker acting as both producer and consumer. +/// +/// There is a *lot* of validation in debug mode; we want to be super sure that we don't corrupt buffer state. +/// +partial struct CycleBuffer +{ + // note: if someone uses an uninitialized CycleBuffer (via default): that's a skills issue; git gud + public static CycleBuffer Create(MemoryPool? pool = null, int pageSize = DefaultPageSize) + { + pool ??= MemoryPool.Shared; + if (pageSize <= 0) pageSize = DefaultPageSize; + if (pageSize > pool.MaxBufferSize) pageSize = pool.MaxBufferSize; + + return new CycleBuffer(pool, pageSize); + } + + private CycleBuffer(MemoryPool pool, int pageSize) + { + Pool = pool; + PageSize = pageSize; + } + + private const int DefaultPageSize = 8 * 1024; + + public int PageSize { get; } + public MemoryPool Pool { get; } + + private Segment? startSegment, endSegment; + + private int endSegmentCommitted, endSegmentLength; + + public bool TryGetCommitted(out ReadOnlySpan span) + { + DebugAssertValid(); + if (!ReferenceEquals(startSegment, endSegment)) + { + span = default; + return false; + } + + span = startSegment is null ? default : startSegment.Memory.Span.Slice(start: 0, length: endSegmentCommitted); + return true; + } + + /// + /// Commits data written to buffers from , making it available for consumption + /// via . This compares to . + /// + public void Commit(int count) + { + DebugAssertValid(); + if (count <= 0) + { + if (count < 0) Throw(); + return; + } + + var available = endSegmentLength - endSegmentCommitted; + if (count > available) Throw(); + endSegmentCommitted += count; + DebugAssertValid(); + + static void Throw() => throw new ArgumentOutOfRangeException(nameof(count)); + } + + public bool CommittedIsEmpty => ReferenceEquals(startSegment, endSegment) & endSegmentCommitted == 0; + + /// + /// Marks committed data as fully consumed; it will no longer appear in later calls to . + /// + public void DiscardCommitted(int count) + { + DebugAssertValid(); + // optimize for most common case, where we consume everything + if (ReferenceEquals(startSegment, endSegment) + & count == endSegmentCommitted + & count > 0) + { + /* + we are consuming all the data in the single segment; we can + just reset that segment back to full size and re-use as-is; + note that we also know that there must *be* a segment + for the count check to pass + */ + endSegmentCommitted = 0; + endSegmentLength = endSegment!.Untrim(expandBackwards: true); + DebugAssertValid(0); + DebugCounters.OnDiscardFull(count); + } + else if (count == 0) + { + // nothing to do + } + else + { + DiscardCommittedSlow(count); + } + } + + public void DiscardCommitted(long count) + { + DebugAssertValid(); + // optimize for most common case, where we consume everything + if (ReferenceEquals(startSegment, endSegment) + & count == endSegmentCommitted + & count > 0) // checks sign *and* non-trimmed + { + // see for logic + endSegmentCommitted = 0; + endSegmentLength = endSegment!.Untrim(expandBackwards: true); + DebugAssertValid(0); + DebugCounters.OnDiscardFull(count); + } + else if (count == 0) + { + // nothing to do + } + else + { + DiscardCommittedSlow(count); + } + } + + private void DiscardCommittedSlow(long count) + { + DebugCounters.OnDiscardPartial(count); +#if DEBUG + var originalLength = GetCommittedLength(); + var originalCount = count; + var expectedLength = originalLength - originalCount; + string blame = nameof(DiscardCommittedSlow); +#endif + while (count > 0) + { + DebugAssertValid(); + var segment = startSegment; + if (segment is null) break; + if (ReferenceEquals(segment, endSegment)) + { + // first==final==only segment + if (count == endSegmentCommitted) + { + endSegmentLength = startSegment!.Untrim(); + endSegmentCommitted = 0; // = untrimmed and unused +#if DEBUG + blame += ",full-final (t)"; +#endif + } + else + { + // discard from the start + int count32 = checked((int)count); + segment.TrimStart(count32); + endSegmentLength -= count32; + endSegmentCommitted -= count32; +#if DEBUG + blame += ",partial-final"; +#endif + } + + count = 0; + break; + } + else if (count < segment.Length) + { + // multiple, but can take some (not all) of the first buffer +#if DEBUG + var len = segment.Length; +#endif + segment.TrimStart((int)count); + Debug.Assert(segment.Length > 0, "parial trim should have left non-empty segment"); +#if DEBUG + Debug.Assert(segment.Length == len - count, "trim failure"); + blame += ",partial-first"; +#endif + count = 0; + break; + } + else + { + // multiple; discard the entire first segment + count -= segment.Length; + startSegment = + segment.ResetAndGetNext(); // we already did a ref-check, so we know this isn't going past endSegment + endSegment!.AppendOrRecycle(segment, maxDepth: 2); + DebugAssertValid(); +#if DEBUG + blame += ",full-first"; +#endif + } + } + + if (count != 0) ThrowCount(); +#if DEBUG + DebugAssertValid(expectedLength, blame); + _ = originalLength; + _ = originalCount; +#endif + + [DoesNotReturn] + static void ThrowCount() => throw new ArgumentOutOfRangeException(nameof(count)); + } + + [Conditional("DEBUG")] + private void DebugAssertValid(long expectedCommittedLength, [CallerMemberName] string caller = "") + { + DebugAssertValid(); + var actual = GetCommittedLength(); + Debug.Assert( + expectedCommittedLength >= 0, + $"Expected committed length is just... wrong: {expectedCommittedLength} (from {caller})"); + Debug.Assert( + expectedCommittedLength == actual, + $"Committed length mismatch: expected {expectedCommittedLength}, got {actual} (from {caller})"); + } + + [Conditional("DEBUG")] + private void DebugAssertValid() + { + if (startSegment is null) + { + Debug.Assert( + endSegmentLength == 0 & endSegmentCommitted == 0, + "un-init state should be zero"); + return; + } + + Debug.Assert(endSegment is not null, "end segment must not be null if start segment exists"); + Debug.Assert( + endSegmentLength == endSegment!.Length, + $"end segment length is incorrect - expected {endSegmentLength}, got {endSegment.Length}"); + Debug.Assert(endSegmentCommitted <= endSegmentLength, $"end segment is over-committed - {endSegmentCommitted} of {endSegmentLength}"); + + // check running indices + startSegment?.DebugAssertValidChain(); + } + + public long GetCommittedLength() + { + DebugAssertValid(); + if (ReferenceEquals(startSegment, endSegment)) + { + return endSegmentCommitted; + } + + // note that the start-segment is pre-trimmed; we don't need to account for an offset on the left + return (endSegment!.RunningIndex + endSegmentCommitted) - startSegment!.RunningIndex; + } + + /// + /// When used with , this means "any non-empty buffer". + /// + public const int GetAnything = 0; + + /// + /// When used with , this means "any full buffer". + /// + public const int GetFullPagesOnly = -1; + + public bool TryGetFirstCommittedSpan(int minBytes, out ReadOnlySpan span) + { + DebugAssertValid(); + if (TryGetFirstCommittedMemory(minBytes, out var memory)) + { + span = memory.Span; + return true; + } + + span = default; + return false; + } + + /// + /// The minLength arg: -ve means "full segments only" (useful when buffering outbound network data to avoid + /// packet fragmentation); otherwise, it is the minimum length we want. + /// + public bool TryGetFirstCommittedMemory(int minBytes, out ReadOnlyMemory memory) + { + if (minBytes == 0) minBytes = 1; // success always means "at least something" + DebugAssertValid(); + if (ReferenceEquals(startSegment, endSegment)) + { + // single page + var available = endSegmentCommitted; + if (available == 0) + { + // empty (includes uninitialized) + memory = default; + return false; + } + + memory = startSegment!.Memory; + var memLength = memory.Length; + if (available == memLength) + { + // full segment; is it enough to make the caller happy? + return available >= minBytes; + } + + // partial segment (and we know it isn't empty) + memory = memory.Slice(start: 0, length: available); + return available >= minBytes & minBytes > 0; // last check here applies the -ve logic + } + + // multi-page; hand out the first page (which is, by definition: full) + memory = startSegment!.Memory; + return memory.Length >= minBytes; + } + + /// + /// Note that this chain is invalidated by any other operations; no concurrency. + /// + public ReadOnlySequence GetAllCommitted() + { + if (ReferenceEquals(startSegment, endSegment)) + { + // single segment, fine + return startSegment is null + ? default + : new ReadOnlySequence(startSegment.Memory.Slice(start: 0, length: endSegmentCommitted)); + } + +#if PARSE_DETAIL + long length = GetCommittedLength(); +#endif + ReadOnlySequence ros = new(startSegment!, 0, endSegment!, endSegmentCommitted); +#if PARSE_DETAIL + Debug.Assert(ros.Length == length, $"length mismatch: calculated {length}, actual {ros.Length}"); +#endif + return ros; + } + + private Segment GetNextSegment() + { + DebugAssertValid(); + if (endSegment is not null) + { + endSegment.TrimEnd(endSegmentCommitted); + Debug.Assert(endSegment.Length == endSegmentCommitted, "trim failure"); + endSegmentLength = endSegmentCommitted; + DebugAssertValid(); + + var spare = endSegment.Next; + if (spare is not null) + { + // we already have a dangling segment; just update state + endSegment.DebugAssertValidChain(); + endSegment = spare; + endSegmentCommitted = 0; + endSegmentLength = spare.Length; + DebugAssertValid(); + return spare; + } + } + + Segment newSegment = Segment.Create(Pool.Rent(PageSize)); + if (endSegment is null) + { + // tabula rasa + endSegmentLength = newSegment.Length; + endSegment = startSegment = newSegment; + DebugAssertValid(); + return newSegment; + } + + endSegment.Append(newSegment); + endSegmentCommitted = 0; + endSegmentLength = newSegment.Length; + endSegment = newSegment; + DebugAssertValid(); + return newSegment; + } + + /// + /// Gets a scratch area for new data; this compares to . + /// + public Span GetUncommittedSpan(int hint = 0) + => GetUncommittedMemory(hint).Span; + + /// + /// Gets a scratch area for new data; this compares to . + /// + public Memory GetUncommittedMemory(int hint = 0) + { + DebugAssertValid(); + var segment = endSegment; + if (segment is not null) + { + var memory = segment.Memory; + if (endSegmentCommitted != 0) memory = memory.Slice(start: endSegmentCommitted); + if (hint <= 0) // allow anything non-empty + { + if (!memory.IsEmpty) return MemoryMarshal.AsMemory(memory); + } + else if (memory.Length >= Math.Min(hint, PageSize >> 2)) // respect the hint up to 1/4 of the page size + { + return MemoryMarshal.AsMemory(memory); + } + } + + // new segment, will always be entire + return MemoryMarshal.AsMemory(GetNextSegment().Memory); + } + + public int UncommittedAvailable + { + get + { + DebugAssertValid(); + return endSegmentLength - endSegmentCommitted; + } + } + + private sealed class Segment : ReadOnlySequenceSegment + { + private Segment() { } + private IMemoryOwner _lease = NullLease.Instance; + private static Segment? _spare; + private Flags _flags; + + [Flags] + private enum Flags + { + None = 0, + StartTrim = 1 << 0, + EndTrim = 1 << 2, + } + + public static Segment Create(IMemoryOwner lease) + { + Debug.Assert(lease is not null, "null lease"); + var memory = lease!.Memory; + if (memory.IsEmpty) ThrowEmpty(); + + var obj = Interlocked.Exchange(ref _spare, null) ?? new(); + return obj.Init(lease, memory); + static void ThrowEmpty() => throw new InvalidOperationException("leased segment is empty"); + } + + private Segment Init(IMemoryOwner lease, Memory memory) + { + _lease = lease; + Memory = memory; + return this; + } + + public int Length => Memory.Length; + + public void Append(Segment next) + { + Debug.Assert(Next is null, "current segment already has a next"); + Debug.Assert(next.Next is null && next.RunningIndex == 0, "inbound next segment is already in a chain"); + next.RunningIndex = RunningIndex + Length; + Next = next; + DebugAssertValidChain(); + } + + private void ApplyChainDelta(int delta) + { + if (delta != 0) + { + var node = Next; + while (node is not null) + { + node.RunningIndex += delta; + node = node.Next; + } + } + } + + public void TrimEnd(int newLength) + { + var delta = Length - newLength; + if (delta != 0) + { + // buffer wasn't fully used; trim + _flags |= Flags.EndTrim; + Memory = Memory.Slice(0, newLength); + ApplyChainDelta(-delta); + DebugAssertValidChain(); + } + } + + public void TrimStart(int remove) + { + if (remove != 0) + { + _flags |= Flags.StartTrim; + Memory = Memory.Slice(start: remove); + RunningIndex += remove; // so that ROS length keeps working; note we *don't* need to adjust the chain + DebugAssertValidChain(); + } + } + + public new Segment? Next + { + get => (Segment?)base.Next; + private set => base.Next = value; + } + + public Segment? ResetAndGetNext() + { + var next = Next; + Next = null; + RunningIndex = 0; + _flags = Flags.None; + Memory = _lease.Memory; // reset, in case we trimmed it + DebugAssertValidChain(); + return next; + } + + public void Recycle() + { + var lease = _lease; + _lease = NullLease.Instance; + lease.Dispose(); + Next = null; + Memory = default; + RunningIndex = 0; + _flags = Flags.None; + Interlocked.Exchange(ref _spare, this); + DebugAssertValidChain(); + } + + private sealed class NullLease : IMemoryOwner + { + private NullLease() { } + public static readonly NullLease Instance = new NullLease(); + public void Dispose() { } + + public Memory Memory => default; + } + + /// + /// Undo any trimming, returning the new full capacity. + /// + public int Untrim(bool expandBackwards = false) + { + var fullMemory = _lease.Memory; + var fullLength = fullMemory.Length; + var delta = fullLength - Length; + if (delta != 0) + { + _flags &= ~(Flags.StartTrim | Flags.EndTrim); + Memory = fullMemory; + if (expandBackwards & RunningIndex >= delta) + { + // push our origin earlier; only valid if + // we're the first segment, otherwise + // we break someone-else's chain + RunningIndex -= delta; + } + else + { + // push everyone else later + ApplyChainDelta(delta); + } + + DebugAssertValidChain(); + } + return fullLength; + } + + public bool StartTrimmed => (_flags & Flags.StartTrim) != 0; + public bool EndTrimmed => (_flags & Flags.EndTrim) != 0; + + [Conditional("DEBUG")] + public void DebugAssertValidChain([CallerMemberName] string blame = "") + { + var node = this; + var runningIndex = RunningIndex; + int index = 0; + while (node.Next is { } next) + { + index++; + var nextRunningIndex = runningIndex + node.Length; + if (nextRunningIndex != next.RunningIndex) ThrowRunningIndex(blame, index); + node = next; + runningIndex = nextRunningIndex; + static void ThrowRunningIndex(string blame, int index) => throw new InvalidOperationException( + $"Critical running index corruption in dangling chain, from '{blame}', segment {index}"); + } + } + + public void AppendOrRecycle(Segment segment, int maxDepth) + { + var node = this; + while (maxDepth-- > 0 && node is not null) + { + if (node.Next is null) // found somewhere to attach it + { + if (segment.Untrim() == 0) break; // turned out to be useless + segment.RunningIndex = node.RunningIndex + node.Length; + node.Next = segment; + return; + } + + node = node.Next; + } + + segment.Recycle(); + } + } + + /// + /// Discard all data and buffers. + /// + public void Release() + { + var node = startSegment; + startSegment = endSegment = null; + endSegmentCommitted = endSegmentLength = 0; + while (node is not null) + { + var next = node.Next; + node.Recycle(); + node = next; + } + } +} + +// this can be shared between CycleBuffer and CycleBuffer.Simple +partial struct CycleBuffer +{ + /// + /// Writes a value to the buffer; comparable to . + /// + public void Write(ReadOnlySpan value) + { + int srcLength = value.Length; + while (srcLength != 0) + { + var target = GetUncommittedSpan(hint: srcLength); + var tgtLength = target.Length; + if (tgtLength >= srcLength) + { + value.CopyTo(target); + Commit(srcLength); + return; + } + + value.Slice(0, tgtLength).CopyTo(target); + Commit(tgtLength); + value = value.Slice(tgtLength); + srcLength -= tgtLength; + } + } + + /// + /// Writes a value to the buffer; comparable to . + /// + public void Write(in ReadOnlySequence value) + { + if (value.IsSingleSegment) + { +#if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1 + Write(value.FirstSpan); +#else + Write(value.First.Span); +#endif + } + else + { + WriteMultiSegment(ref this, in value); + } + + static void WriteMultiSegment(ref CycleBuffer @this, in ReadOnlySequence value) + { + foreach (var segment in value) + { +#if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1 + @this.Write(value.FirstSpan); +#else + @this.Write(value.First.Span); +#endif + } + } + } +} diff --git a/src/RESP.Core/DebugCounters.cs b/src/RESP.Core/DebugCounters.cs new file mode 100644 index 000000000..48a9f08df --- /dev/null +++ b/src/RESP.Core/DebugCounters.cs @@ -0,0 +1,183 @@ +using System.Diagnostics; +using System.Threading; + +namespace Resp; +#if DEBUG +public partial class DebugCounters +#else +internal partial class DebugCounters +#endif +{ +#if DEBUG + private static int _tallyReadCount, + _tallyAsyncReadCount, + _tallyAsyncReadInlineCount, + _tallyWriteCount, + _tallyAsyncWriteCount, + _tallyAsyncWriteInlineCount, + _tallyCopyOutCount, + _tallyDiscardFullCount, + _tallyDiscardPartialCount, + _tallyPipelineFullAsyncCount, + _tallyPipelineSendAsyncCount, + _tallyPipelineFullSyncCount, + _tallyBatchWriteCount, + _tallyBatchWriteFullPageCount, + _tallyBatchWritePartialPageCount, + _tallyBatchWriteMessageCount; + + private static long _tallyWriteBytes, _tallyReadBytes, _tallyCopyOutBytes, _tallyDiscardAverage; +#endif + [Conditional("DEBUG")] + internal static void OnRead(int bytes) + { +#if DEBUG + Interlocked.Increment(ref _tallyReadCount); + if (bytes > 0) Interlocked.Add(ref _tallyReadBytes, bytes); +#endif + } + + public static void OnBatchWrite(int messageCount) + { +#if DEBUG + Interlocked.Increment(ref _tallyBatchWriteCount); + if (messageCount != 0) Interlocked.Add(ref _tallyBatchWriteMessageCount, messageCount); +#endif + } + + public static void OnBatchWriteFullPage() + { +#if DEBUG + Interlocked.Increment(ref _tallyBatchWriteFullPageCount); +#endif + } + public static void OnBatchWritePartialPage() + { +#if DEBUG + Interlocked.Increment(ref _tallyBatchWritePartialPageCount); +#endif + } + + [Conditional("DEBUG")] + internal static void OnAsyncRead(int bytes, bool inline) + { +#if DEBUG + Interlocked.Increment(ref inline ? ref _tallyAsyncReadInlineCount : ref _tallyAsyncReadCount); + if (bytes > 0) Interlocked.Add(ref _tallyReadBytes, bytes); +#endif + } + + [Conditional("DEBUG")] + internal static void OnWrite(int bytes) + { +#if DEBUG + Interlocked.Increment(ref _tallyWriteCount); + if (bytes > 0) Interlocked.Add(ref _tallyWriteBytes, bytes); +#endif + } + + [Conditional("DEBUG")] + internal static void OnAsyncWrite(int bytes, bool inline) + { +#if DEBUG + Interlocked.Increment(ref inline ? ref _tallyAsyncWriteInlineCount : ref _tallyAsyncWriteCount); + if (bytes > 0) Interlocked.Add(ref _tallyWriteBytes, bytes); +#endif + } + + [Conditional("DEBUG")] + internal static void OnCopyOut(int bytes) + { +#if DEBUG + Interlocked.Increment(ref _tallyCopyOutCount); + if (bytes > 0) Interlocked.Add(ref _tallyCopyOutBytes, bytes); +#endif + } + + [Conditional("DEBUG")] + public static void OnDiscardFull(long count) + { +#if DEBUG + if (count > 0) + { + Interlocked.Increment(ref _tallyDiscardFullCount); + EstimatedMovingRangeAverage(ref _tallyDiscardAverage, count); + } +#endif + } + + [Conditional("DEBUG")] + public static void OnDiscardPartial(long count) + { +#if DEBUG + if (count > 0) + { + Interlocked.Increment(ref _tallyDiscardPartialCount); + EstimatedMovingRangeAverage(ref _tallyDiscardAverage, count); + } +#endif + } + + [Conditional("DEBUG")] + public static void OnPipelineFullAsync() + { +#if DEBUG + Interlocked.Increment(ref _tallyPipelineFullAsyncCount); +#endif + } + + [Conditional("DEBUG")] + public static void OnPipelineSendAsync() + { +#if DEBUG + Interlocked.Increment(ref _tallyPipelineSendAsyncCount); +#endif + } + + [Conditional("DEBUG")] + public static void OnPipelineFullSync() + { +#if DEBUG + Interlocked.Increment(ref _tallyPipelineFullSyncCount); +#endif + } + + private DebugCounters() + { + } + + public static DebugCounters Flush() => new(); + +#if DEBUG + private static void EstimatedMovingRangeAverage(ref long field, long value) + { + var oldValue = Volatile.Read(ref field); + var delta = (value - oldValue) >> 3; // is is a 7:1 old:new EMRA, using integer/bit math (alplha=0.125) + if (delta != 0) Interlocked.Add(ref field, delta); + // note: strictly conflicting concurrent calls can skew the value incorrectly; this is, however, + // preferable to getting into a CEX squabble or requiring a lock - it is debug-only and just useful data + } + + public int ReadCount { get; } = Interlocked.Exchange(ref _tallyReadCount, 0); + public int AsyncReadCount { get; } = Interlocked.Exchange(ref _tallyAsyncReadCount, 0); + public int AsyncReadInlineCount { get; } = Interlocked.Exchange(ref _tallyAsyncReadInlineCount, 0); + public long ReadBytes { get; } = Interlocked.Exchange(ref _tallyReadBytes, 0); + + public int WriteCount { get; } = Interlocked.Exchange(ref _tallyWriteCount, 0); + public int AsyncWriteCount { get; } = Interlocked.Exchange(ref _tallyAsyncWriteCount, 0); + public int AsyncWriteInlineCount { get; } = Interlocked.Exchange(ref _tallyAsyncWriteInlineCount, 0); + public long WriteBytes { get; } = Interlocked.Exchange(ref _tallyWriteBytes, 0); + public int CopyOutCount { get; } = Interlocked.Exchange(ref _tallyCopyOutCount, 0); + public long CopyOutBytes { get; } = Interlocked.Exchange(ref _tallyCopyOutBytes, 0); + public long DiscardAverage { get; } = Interlocked.Exchange(ref _tallyDiscardAverage, 32); + public int DiscardFullCount { get; } = Interlocked.Exchange(ref _tallyDiscardFullCount, 0); + public int DiscardPartialCount { get; } = Interlocked.Exchange(ref _tallyDiscardPartialCount, 0); + public int PipelineFullAsyncCount { get; } = Interlocked.Exchange(ref _tallyPipelineFullAsyncCount, 0); + public int PipelineSendAsyncCount { get; } = Interlocked.Exchange(ref _tallyPipelineSendAsyncCount, 0); + public int PipelineFullSyncCount { get; } = Interlocked.Exchange(ref _tallyPipelineFullSyncCount, 0); + public int BatchWriteCount { get; } = Interlocked.Exchange(ref _tallyBatchWriteCount, 0); + public int BatchWriteFullPageCount { get; } = Interlocked.Exchange(ref _tallyBatchWriteFullPageCount, 0); + public int BatchWritePartialPageCount { get; } = Interlocked.Exchange(ref _tallyBatchWritePartialPageCount, 0); + public int BatchWriteMessageCount { get; } = Interlocked.Exchange(ref _tallyBatchWriteMessageCount, 0); +#endif +} diff --git a/src/RESP.Core/DirectWriteConnection.cs b/src/RESP.Core/DirectWriteConnection.cs new file mode 100644 index 000000000..eaf569d09 --- /dev/null +++ b/src/RESP.Core/DirectWriteConnection.cs @@ -0,0 +1,722 @@ +// #define PARSE_DETAIL // additional trace info in CommitAndParseFrames + +#if DEBUG +#define PARSE_DETAIL // always enable this in debug builds +#endif + +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Resp; + +internal sealed class DirectWriteConnection : IRespConnection +{ + private bool _isDoomed; + private RespScanState _readScanState; + private CycleBuffer _readBuffer, _writeBuffer; + private readonly RespContext _context; + public ref readonly RespContext Context => ref _context; + public bool CanWrite => Volatile.Read(ref _readStatus) == WRITER_AVAILABLE; + + public int Outstanding => _outstanding.Count; + + public Task Reader { get; private set; } = Task.CompletedTask; + + private readonly Stream tail; + private ConcurrentQueue _outstanding = new(); + public RespConfiguration Configuration { get; } + + public DirectWriteConnection(RespConfiguration configuration, Stream tail, bool asyncRead = true) + { + Configuration = configuration; + if (!(tail.CanRead && tail.CanWrite)) Throw(); + this.tail = tail; + var memoryPool = configuration.GetService>(); + _readBuffer = CycleBuffer.Create(memoryPool); + _writeBuffer = CycleBuffer.Create(memoryPool); + if (asyncRead) + { + Reader = Task.Run(ReadAllAsync); + } + else + { + new Thread(ReadAll).Start(); + } + + _context = RespContext.For(this); + + static void Throw() => throw new ArgumentException("Stream must be readable and writable", nameof(tail)); + } + + public RespMode Mode { get; set; } = RespMode.Resp2; + + public enum RespMode + { + Resp2, + Resp2PubSub, + Resp3, + } + + private static byte[]? SharedNoLease; + + private bool CommitAndParseFrames(int bytesRead) + { + if (bytesRead <= 0) + { + return false; + } + + // let's bypass a bunch of ldarg0 by hoisting the field-refs (this is **NOT** a struct copy; emphasis "ref") + ref RespScanState state = ref _readScanState; + ref CycleBuffer readBuffer = ref _readBuffer; + +#if PARSE_DETAIL + string src = $"parse {bytesRead}"; + try +#endif + { + Debug.Assert(readBuffer.GetCommittedLength() >= 0, "multi-segment running-indices are corrupt"); +#if PARSE_DETAIL + src += $" ({readBuffer.GetCommittedLength()}+{bytesRead}-{state.TotalBytes})"; +#endif + Debug.Assert( + bytesRead <= readBuffer.UncommittedAvailable, + $"Insufficient bytes in {nameof(CommitAndParseFrames)}; got {bytesRead}, Available={readBuffer.UncommittedAvailable}"); + readBuffer.Commit(bytesRead); +#if PARSE_DETAIL + src += $",total {readBuffer.GetCommittedLength()}"; +#endif + var scanner = RespFrameScanner.Default; + + OperationStatus status = OperationStatus.NeedMoreData; + if (readBuffer.TryGetCommitted(out var fullSpan)) + { + int fullyConsumed = 0; + var toParse = fullSpan.Slice((int)state.TotalBytes); // skip what we've already parsed + + Debug.Assert(!toParse.IsEmpty); + while (true) + { +#if PARSE_DETAIL + src += $",span {toParse.Length}"; +#endif + int totalBytesBefore = (int)state.TotalBytes; + if (toParse.Length < RespScanState.MinBytes + || (status = scanner.TryRead(ref state, toParse)) != OperationStatus.Done) + { + break; + } + + Debug.Assert( + state is + { + IsComplete: true, TotalBytes: >= RespScanState.MinBytes, Prefix: not RespPrefix.None + }, + "Invalid RESP read state"); + + // extract the frame + var bytes = (int)state.TotalBytes; +#if PARSE_DETAIL + src += $",frame {bytes}"; +#endif + // send the frame somewhere (note this is the *full* frame, not just the bit we just parsed) + OnResponseFrame(state.Prefix, fullSpan.Slice(fullyConsumed, bytes), ref SharedNoLease); + + // update our buffers to the unread potions and reset for a new RESP frame + fullyConsumed += bytes; + toParse = toParse.Slice(bytes - totalBytesBefore); // move past the extra bytes we just read + state = default; + status = OperationStatus.NeedMoreData; + } + + readBuffer.DiscardCommitted(fullyConsumed); + } + else // the same thing again, but this time with multi-segment sequence + { + var fullSequence = readBuffer.GetAllCommitted(); + Debug.Assert( + fullSequence is { IsEmpty: false, IsSingleSegment: false }, + "non-trivial sequence expected"); + + long fullyConsumed = 0; + var toParse = fullSequence.Slice((int)state.TotalBytes); // skip what we've already parsed + while (true) + { +#if PARSE_DETAIL + src += $",ros {toParse.Length}"; +#endif + int totalBytesBefore = (int)state.TotalBytes; + if (toParse.Length < RespScanState.MinBytes + || (status = scanner.TryRead(ref state, toParse)) != OperationStatus.Done) + { + break; + } + + Debug.Assert( + state is + { + IsComplete: true, TotalBytes: >= RespScanState.MinBytes, Prefix: not RespPrefix.None + }, + "Invalid RESP read state"); + + // extract the frame + var bytes = (int)state.TotalBytes; +#if PARSE_DETAIL + src += $",frame {bytes}"; +#endif + // send the frame somewhere (note this is the *full* frame, not just the bit we just parsed) + OnResponseFrame(state.Prefix, fullSequence.Slice(fullyConsumed, bytes)); + + // update our buffers to the unread potions and reset for a new RESP frame + fullyConsumed += bytes; + toParse = toParse.Slice(bytes - totalBytesBefore); // move past the extra bytes we just read + state = default; + status = OperationStatus.NeedMoreData; + } + + readBuffer.DiscardCommitted(fullyConsumed); + } + + if (status != OperationStatus.NeedMoreData) + { + ThrowStatus(status); + + static void ThrowStatus(OperationStatus status) => + throw new InvalidOperationException($"Unexpected operation status: {status}"); + } + + return true; + } +#if PARSE_DETAIL + catch (Exception ex) + { + Debug.WriteLine($"{nameof(CommitAndParseFrames)}: {ex.Message}"); + Debug.WriteLine(src); + ActivationHelper.DebugBreak(); + throw new InvalidOperationException($"{src} lead to {ex.Message}", ex); + } +#endif + } + + private async Task ReadAllAsync() + { + try + { + int read; + do + { + var buffer = _readBuffer.GetUncommittedMemory(); + var pending = tail.ReadAsync(buffer, CancellationToken.None); +#if DEBUG + bool inline = pending.IsCompleted; +#endif + read = await pending.ConfigureAwait(false); +#if DEBUG + DebugCounters.OnAsyncRead(read, inline); +#endif + } + // another formatter glitch + while (CommitAndParseFrames(read)); + + Volatile.Write(ref _readStatus, READER_COMPLETED); + _readBuffer.Release(); // clean exit, we can recycle + } + catch (Exception ex) + { + OnReadException(ex); + throw; + } + finally + { + OnReadAllFinally(); + } + } + + private void ReadAll() + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + Reader = tcs.Task; + try + { + int read; + do + { +#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER + var buffer = _readBuffer.GetUncommittedSpan(); + read = tail.Read(buffer); +#else + var buffer = _readBuffer.GetUncommittedMemory(); + read = tail.Read(buffer); +#endif + DebugCounters.OnRead(read); + } + // another formatter glitch + while (CommitAndParseFrames(read)); + + Volatile.Write(ref _readStatus, READER_COMPLETED); + _readBuffer.Release(); // clean exit, we can recycle + tcs.TrySetResult(null); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + OnReadException(ex); + } + finally + { + OnReadAllFinally(); + } + } + + private void OnReadException(Exception ex) + { + _fault ??= ex; + Volatile.Write(ref _readStatus, READER_FAILED); + Debug.WriteLine($"Reader failed: {ex.Message}"); + ActivationHelper.DebugBreak(); + while (_outstanding.TryDequeue(out var pending)) + { + pending.TrySetException(ex); + } + } + + private void OnReadAllFinally() + { + Doom(); + _readBuffer.Release(); + + // abandon anything in the queue + while (_outstanding.TryDequeue(out var pending)) + { + pending.TrySetCanceled(CancellationToken.None); + } + } + + private static readonly ulong + ArrayPong_LC_Bulk = RespConstants.UnsafeCpuUInt64("*2\r\n$4\r\npong\r\n$"u8), + ArrayPong_UC_Bulk = RespConstants.UnsafeCpuUInt64("*2\r\n$4\r\nPONG\r\n$"u8), + ArrayPong_LC_Simple = RespConstants.UnsafeCpuUInt64("*2\r\n+pong\r\n$"u8), + ArrayPong_UC_Simple = RespConstants.UnsafeCpuUInt64("*2\r\n+PONG\r\n$"u8); + + private static readonly uint + pong = RespConstants.UnsafeCpuUInt32("pong"u8), + PONG = RespConstants.UnsafeCpuUInt32("PONG"u8); + + private void OnOutOfBand(ReadOnlySpan payload, ref byte[]? lease) + { + throw new NotImplementedException(nameof(OnOutOfBand)); + } + + private void OnResponseFrame(RespPrefix prefix, ReadOnlySequence payload) + { + if (payload.IsSingleSegment) + { +#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER + OnResponseFrame(prefix, payload.FirstSpan, ref SharedNoLease); +#else + OnResponseFrame(prefix, payload.First.Span, ref SharedNoLease); +#endif + } + else + { + var len = checked((int)payload.Length); + byte[]? oversized = ArrayPool.Shared.Rent(len); + payload.CopyTo(oversized); + OnResponseFrame(prefix, new(oversized, 0, len), ref oversized); + + // the lease could have been claimed by the activation code (to prevent another memcpy); otherwise, free + if (oversized is not null) + { + ArrayPool.Shared.Return(oversized); + } + } + } + + [Conditional("DEBUG")] + private static void DebugValidateSingleFrame(ReadOnlySpan payload) + { + var reader = new RespReader(payload); + reader.MoveNext(); + reader.SkipChildren(); + if (reader.TryMoveNext()) + { + throw new InvalidOperationException($"Unexpected trailing {reader.Prefix}"); + } + + if (reader.ProtocolBytesRemaining != 0) + { + var copy = reader; // leave reader alone for inspection + var prefix = copy.TryMoveNext() ? copy.Prefix : RespPrefix.None; + throw new InvalidOperationException( + $"Unexpected additional {reader.ProtocolBytesRemaining} bytes remaining, {prefix}"); + } + } + + private void OnResponseFrame(RespPrefix prefix, ReadOnlySpan payload, ref byte[]? lease) + { + DebugValidateSingleFrame(payload); + if (prefix == RespPrefix.Push || + (prefix == RespPrefix.Array && Mode is RespMode.Resp2PubSub && !IsArrayPong(payload))) + { + // out-of-band; pub/sub etc + OnOutOfBand(payload, ref lease); + return; + } + + // request/response; match to inbound + if (_outstanding.TryDequeue(out var pending)) + { + ActivationHelper.ProcessResponse(pending, payload, ref lease); + } + else + { + Debug.Fail("Unexpected response without pending message!"); + } + + static bool IsArrayPong(ReadOnlySpan payload) + { + if (payload.Length >= sizeof(ulong)) + { + var raw = RespConstants.UnsafeCpuUInt64(payload); + if (raw == ArrayPong_LC_Bulk + || raw == ArrayPong_UC_Bulk + || raw == ArrayPong_LC_Simple + || raw == ArrayPong_UC_Simple) + { + var reader = new RespReader(payload); + return reader.TryMoveNext() // have root + && reader.Prefix == RespPrefix.Array // root is array + && reader.TryMoveNext() // have first child + && (reader.IsInlneCpuUInt32(pong) || reader.IsInlneCpuUInt32(PONG)); // pong + } + } + + return false; + } + } + + private int _writeStatus, _readStatus; + private const int WRITER_AVAILABLE = 0, WRITER_TAKEN = 1, WRITER_DOOMED = 2; + private const int READER_ACTIVE = 0, READER_FAILED = 1, READER_COMPLETED = 2; + + private void TakeWriter() + { + var status = Interlocked.CompareExchange(ref _writeStatus, WRITER_TAKEN, WRITER_AVAILABLE); + if (status != WRITER_AVAILABLE) ThrowWriterNotAvailable(); + Debug.Assert(Volatile.Read(ref _writeStatus) == WRITER_TAKEN, "writer should be taken"); + } + + private void ThrowWriterNotAvailable() + { + var fault = Volatile.Read(ref _fault); + var status = Volatile.Read(ref _writeStatus); + var msg = status switch + { + WRITER_TAKEN => "A write operation is already in progress; concurrent writes are not supported.", + WRITER_DOOMED when fault is not null => "This connection is terminated; no further writes are possible: " + + fault.Message, + WRITER_DOOMED => "This connection is terminated; no further writes are possible.", + _ => $"Unexpected writer status: {status}", + }; + throw fault is null ? new InvalidOperationException(msg) : new InvalidOperationException(msg, fault); + } + + private Exception? _fault; + + private void ReleaseWriter(int status = WRITER_AVAILABLE) + { + if (status == WRITER_AVAILABLE && _isDoomed) + { + status = WRITER_DOOMED; + } + + Interlocked.CompareExchange(ref _writeStatus, status, WRITER_TAKEN); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void OnRequestUnavailable(IRespMessage message) + { + if (!message.IsCompleted) + { + // make sure they know something is wrong + message.TrySetException(new InvalidOperationException("Connection is not available")); + } + } + + public void Send(IRespMessage message) + { + bool releaseRequest = message.TryReserveRequest(out var bytes); + if (!releaseRequest) + { + OnRequestUnavailable(message); + return; + } + + DebugValidateSingleFrame(bytes.Span); + TakeWriter(); + try + { + _outstanding.Enqueue(message); + releaseRequest = false; // once we write, only release on success +#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER + tail.Write(bytes.Span); +#else + tail.Write(bytes); +#endif + DebugCounters.OnWrite(bytes.Length); + ReleaseWriter(); + message.ReleaseRequest(); + } + catch (Exception ex) + { + Debug.WriteLine($"Writer failed: {ex.Message}"); + ActivationHelper.DebugBreak(); + ReleaseWriter(WRITER_DOOMED); + if (releaseRequest) message.ReleaseRequest(); + throw; + } + } + + public void Send(ReadOnlySpan messages) + { + switch (messages.Length) + { + case 0: + return; + case 1: + Send(messages[0]); + return; + } + + TakeWriter(); + IRespMessage? toRelease = null; + try + { + foreach (var message in messages) + { + if (message.TryReserveRequest(out var bytes)) + { + toRelease = message; + } + else + { + OnRequestUnavailable(message); + continue; + } + + DebugValidateSingleFrame(bytes.Span); + _outstanding.Enqueue(message); + toRelease = null; // once we write, only release on success +#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER + tail.Write(bytes.Span); +#else + tail.Write(bytes); +#endif + DebugCounters.OnWrite(bytes.Length); + ReleaseWriter(); + message.ReleaseRequest(); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Writer failed: {ex.Message}"); + ActivationHelper.DebugBreak(); + ReleaseWriter(WRITER_DOOMED); + toRelease?.ReleaseRequest(); + foreach (var message in messages) + { + // assume all bad + message.TrySetException(ex); + } + + throw; + } + } + + public Task SendAsync(IRespMessage message) + { + bool releaseRequest = message.TryReserveRequest(out var bytes); + if (!releaseRequest) + { + OnRequestUnavailable(message); + return Task.CompletedTask; + } + + DebugValidateSingleFrame(bytes.Span); + TakeWriter(); + try + { + _outstanding.Enqueue(message); + releaseRequest = false; // once we write, only release on success + var pendingWrite = tail.WriteAsync(bytes, CancellationToken.None); + if (!pendingWrite.IsCompleted) + { + return AwaitedSingleWithToken( + this, + pendingWrite, +#if DEBUG + bytes.Length, +#endif + message); + } + + pendingWrite.GetAwaiter().GetResult(); + DebugCounters.OnAsyncWrite(bytes.Length, true); + ReleaseWriter(); + message.ReleaseRequest(); + return Task.CompletedTask; + } + catch (Exception ex) + { + Debug.WriteLine($"Writer failed: {ex.Message}"); + ActivationHelper.DebugBreak(); + ReleaseWriter(WRITER_DOOMED); + if (releaseRequest) message.ReleaseRequest(); + throw; + } + + static async Task AwaitedSingleWithToken( + DirectWriteConnection @this, + ValueTask pendingWrite, +#if DEBUG + int length, +#endif + IRespMessage message) + { + try + { + await pendingWrite.ConfigureAwait(false); +#if DEBUG + DebugCounters.OnAsyncWrite(length, false); +#endif + @this.ReleaseWriter(); + message.ReleaseRequest(); + } + catch + { + @this.ReleaseWriter(WRITER_DOOMED); + throw; + } + } + } + + public Task SendAsync(ReadOnlyMemory messages) + { + switch (messages.Length) + { + case 0: + return Task.CompletedTask; + case 1: + return SendAsync(messages.Span[0]); + default: + return CombineAndSendMultipleAsync(this, messages); + } + } + + private async Task CombineAndSendMultipleAsync(DirectWriteConnection @this, ReadOnlyMemory messages) + { + TakeWriter(); + IRespMessage? toRelease = null; + int definitelySent = 0; + try + { + int length = messages.Length; + for (int i = 0; i < length; i++) + { + var message = messages.Span[i]; + if (!message.TryReserveRequest(out var bytes)) + { + OnRequestUnavailable(message); + continue; // skip this message + } + + toRelease = message; + // append to the scratch and consider written (even though we haven't actually) + _writeBuffer.Write(bytes.Span); + toRelease = null; + message.ReleaseRequest(); + @this._outstanding.Enqueue(message); + + // do we have any full segments? if so, write them and narrow "messages" + if (_writeBuffer.TryGetFirstCommittedMemory(CycleBuffer.GetFullPagesOnly, out var memory)) + { + do + { + var pending = tail.WriteAsync(memory, CancellationToken.None); + DebugCounters.OnAsyncWrite(memory.Length, inline: pending.IsCompleted); + await pending.ConfigureAwait(false); + DebugCounters.OnBatchWriteFullPage(); + + _writeBuffer.DiscardCommitted(memory.Length); // mark the data as no longer needed + } + // and if one buffer was full, we might have multiple (think: "large BLOB outbound") + while (_writeBuffer.TryGetFirstCommittedMemory(CycleBuffer.GetFullPagesOnly, out memory)); + + definitelySent = i + 1; // for exception handling: no need to doom these if later fails + } + } + + // and send any remaining data + while (_writeBuffer.TryGetFirstCommittedMemory(CycleBuffer.GetAnything, out var memory)) + { + var pending = tail.WriteAsync(memory, CancellationToken.None); + DebugCounters.OnAsyncWrite(memory.Length, inline: pending.IsCompleted); + await pending.ConfigureAwait(false); + DebugCounters.OnBatchWritePartialPage(); + + _writeBuffer.DiscardCommitted(memory.Length); // mark the data as no longer needed + } + + Debug.Assert(_writeBuffer.CommittedIsEmpty, "should have written everything"); + + ReleaseWriter(); + DebugCounters.OnBatchWrite(messages.Length); + } + catch (Exception ex) + { + Debug.WriteLine($"Writer failed: {ex.Message}"); + ActivationHelper.DebugBreak(); + ReleaseWriter(WRITER_DOOMED); + toRelease?.ReleaseRequest(); + foreach (var message in messages.Span.Slice(start: definitelySent)) + { + message.TrySetException(ex); + } + + throw; + } + } + + private void Doom() + { + _isDoomed = true; // without a reader, there's no point writing + Interlocked.CompareExchange(ref _writeStatus, WRITER_DOOMED, WRITER_AVAILABLE); + } + + public void Dispose() + { + _fault ??= new ObjectDisposedException(ToString()); + Doom(); + tail.Dispose(); + } + + public override string ToString() => nameof(DirectWriteConnection); + + public ValueTask DisposeAsync() + { +#if COREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + return tail.DisposeAsync().AsTask(); +#else + Dispose(); + return default; +#endif + } +} diff --git a/src/RESP.Core/FrameScanInfo.cs b/src/RESP.Core/FrameScanInfo.cs new file mode 100644 index 000000000..84e4cb604 --- /dev/null +++ b/src/RESP.Core/FrameScanInfo.cs @@ -0,0 +1,34 @@ +namespace Resp; + +/* +/// +/// Additional information about a frame parsing operation. +/// +public struct FrameScanInfo +{ + /// + /// Initialize an instance. + /// + public FrameScanInfo(bool isOutbound) => IsOutbound = isOutbound; + + /// + /// Indicates whether the data operation is outbound. + /// + public bool IsOutbound { get; } + + /// + /// The amount of data, in bytes, to read before attempting to read the next frame. + /// + public int MinBytes => 3; // minimum legal RESP frame is: _\r\n + + /// + /// Gets the total number of bytes processed. + /// + public long BytesRead { get; set; } + + /// + /// Indicates whether this is an out-of-band payload. + /// + public bool IsOutOfBand { get; set; } +} +*/ diff --git a/src/RESP.Core/Global.cs b/src/RESP.Core/Global.cs new file mode 100644 index 000000000..593d3f98b --- /dev/null +++ b/src/RESP.Core/Global.cs @@ -0,0 +1,4 @@ +using System; +using System.Runtime.CompilerServices; + +[assembly: CLSCompliant(true)] diff --git a/src/RESP.Core/IRespConnection.cs b/src/RESP.Core/IRespConnection.cs new file mode 100644 index 000000000..55273e54e --- /dev/null +++ b/src/RESP.Core/IRespConnection.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Resp; + +public interface IRespConnection : IDisposable, IAsyncDisposable +{ + RespConfiguration Configuration { get; } + bool CanWrite { get; } + int Outstanding { get; } + + /// + /// Gets the default context associates with this connection. + /// + ref readonly RespContext Context { get; } + + void Send(IRespMessage message); + void Send(ReadOnlySpan messages); + + Task SendAsync(IRespMessage message); + Task SendAsync(ReadOnlyMemory messages); +} diff --git a/src/RESP.Core/IRespReader.cs b/src/RESP.Core/IRespReader.cs new file mode 100644 index 000000000..2629efdf0 --- /dev/null +++ b/src/RESP.Core/IRespReader.cs @@ -0,0 +1,14 @@ +// using RESPite.Messages; +// +// namespace Resp; +// +// /// +// /// Reads RESP payloads. +// /// +// internal interface IRespReader : IReader +// { +// /// +// /// Read a given value. +// /// +// TResponse Read(in TRequest request, ref RespReader reader); +// } diff --git a/src/RESP.Core/Message.cs b/src/RESP.Core/Message.cs new file mode 100644 index 000000000..edd50f594 --- /dev/null +++ b/src/RESP.Core/Message.cs @@ -0,0 +1,211 @@ +using System; +using System.Threading.Tasks; + +namespace Resp; + +public static class Message +{ + public static TResponse Send( + in RespContext context, + scoped ReadOnlySpan command, + in TRequest request, + IRespFormatter formatter, + IRespParser parser) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + var bytes = Serialize(context.RespCommandMap, command, request, formatter, out int length); + var msg = SyncInternalRespMessage.Create( + bytes, + length, + parser, + in Void.Instance, + context.CancellationToken); + context.Connection.Send(msg); + return msg.WaitAndRecycle(context.Connection.Configuration.SyncTimeout); + } + + public static TResponse Send( + in RespContext context, + scoped ReadOnlySpan command, + in TRequest request, + in TState state, + IRespFormatter formatter, + IRespParser parser) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + var bytes = Serialize(context.RespCommandMap, command, in request, formatter, out int length); + var msg = SyncInternalRespMessage.Create( + bytes, + length, + parser, + in state, + context.CancellationToken); + context.Connection.Send(msg); + return msg.WaitAndRecycle(context.Connection.Configuration.SyncTimeout); + } + + public static ValueTask SendAsync( + in RespContext context, + scoped ReadOnlySpan command, + in TRequest request, + IRespFormatter formatter, + IRespParser parser) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + var bytes = Serialize(context.RespCommandMap, command, request, formatter, out int length); + var msg = AsyncInternalRespMessage.Create( + bytes, + length, + parser, + in Void.Instance, + context.CancellationToken); + return msg.WaitTypedAsync(context.Connection.SendAsync(msg)); + } + + public static ValueTask SendAsync( + in RespContext context, + scoped ReadOnlySpan command, + in TRequest request, + in TState state, + IRespFormatter formatter, + IRespParser parser) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + var bytes = Serialize(context.RespCommandMap, command, in request, formatter, out int length); + var msg = AsyncInternalRespMessage.Create( + bytes, + length, + parser, + in state, + context.CancellationToken); + return msg.WaitTypedAsync(context.Connection.SendAsync(msg)); + } + + public static void Send( + in RespContext context, + scoped ReadOnlySpan command, + in TRequest request, + IRespFormatter formatter, + IRespParser parser) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + var bytes = Serialize(context.RespCommandMap, command, request, formatter, out int length); + var msg = SyncInternalRespMessage.Create( + bytes, + length, + parser, + in Void.Instance, + context.CancellationToken); + context.Connection.Send(msg); + msg.WaitAndRecycle(context.Connection.Configuration.SyncTimeout); + } + + public static void Send( + in RespContext context, + scoped ReadOnlySpan command, + in TRequest request, + in TState state, + IRespFormatter formatter, + IRespParser parser) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + var bytes = Serialize(context.RespCommandMap, command, in request, formatter, out int length); + var msg = SyncInternalRespMessage.Create( + bytes, + length, + parser, + in state, + context.CancellationToken); + context.Connection.Send(msg); + msg.WaitAndRecycle(context.Connection.Configuration.SyncTimeout); + } + + public static ValueTask SendAsync( + in RespContext context, + scoped ReadOnlySpan command, + in TRequest request, + IRespFormatter formatter, + IRespParser parser) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + var bytes = Serialize(context.RespCommandMap, command, request, formatter, out int length); + var msg = AsyncInternalRespMessage.Create( + bytes, + length, + parser, + in Void.Instance, + context.CancellationToken); + return msg.WaitUntypedAsync(context.Connection.SendAsync(msg)); + } + + public static ValueTask SendAsync( + in RespContext context, + scoped ReadOnlySpan command, + in TRequest request, + in TState state, + IRespFormatter formatter, + IRespParser parser) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + var bytes = Serialize(context.RespCommandMap, command, in request, formatter, out int length); + var msg = AsyncInternalRespMessage.Create( + bytes, + length, + parser, + in state, + context.CancellationToken); + return msg.WaitUntypedAsync(context.Connection.SendAsync(msg)); + } + + private static byte[] Serialize( + RespCommandMap commandMap, + ReadOnlySpan command, + in TRequest request, + IRespFormatter formatter, + out int length) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + int size = 0; + if (formatter is IRespSizeEstimator estimator) + { + size = estimator.EstimateSize(command, request); + } + + var buffer = AmbientBufferWriter.Get(size); + try + { + var writer = new RespWriter(buffer); + if (!ReferenceEquals(commandMap, RespCommandMap.Default)) + { + writer.CommandMap = commandMap; + } + + formatter.Format(command, ref writer, request); + writer.Flush(); + return buffer.Detach(out length); + } + catch + { + buffer.Reset(); + throw; + } + } +} diff --git a/src/RESP.Core/PipelinedConnection.cs b/src/RESP.Core/PipelinedConnection.cs new file mode 100644 index 000000000..fc27b8c47 --- /dev/null +++ b/src/RESP.Core/PipelinedConnection.cs @@ -0,0 +1,218 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Resp; + +internal class PipelinedConnection : IRespConnection +{ + private readonly IRespConnection _tail; + private readonly RespContext _context; + private readonly SemaphoreSlim _semaphore = new(1); + + public ref readonly RespContext Context => ref _context; + public PipelinedConnection(in RespContext tail) + { + _tail = tail.Connection; + _context = tail.WithConnection(this); + } + + public void Dispose() + { + _semaphore.Dispose(); + _tail.Dispose(); + } + + public ValueTask DisposeAsync() + { + _semaphore.Dispose(); + return _tail.DisposeAsync(); + } + + public RespConfiguration Configuration => _tail.Configuration; + public bool CanWrite => _semaphore.CurrentCount > 0 && _tail.CanWrite; + public int Outstanding => _tail.Outstanding; + + public void Send(IRespMessage message) + { + _semaphore.Wait(message.CancellationToken); + try + { + _tail.Send(message); + } + catch (Exception ex) + { + message.TrySetException(ex); + throw; + } + finally + { + _semaphore.Release(); + } + } + + public void Send(ReadOnlySpan messages) + { + switch (messages.Length) + { + case 0: return; + case 1: + Send(messages[0]); + return; + } + _semaphore.Wait(messages[0].CancellationToken); + try + { + _tail.Send(messages); + } + catch (Exception ex) + { + TrySetException(messages, ex); + throw; + } + finally + { + _semaphore.Release(); + } + } + + public Task SendAsync(IRespMessage message) + { + bool haveLock = false; + try + { + haveLock = _semaphore.Wait(0); + if (!haveLock) + { + DebugCounters.OnPipelineFullAsync(); + return FullAsync(this, message); + } + + var pending = _tail.SendAsync(message); + if (!pending.IsCompleted) + { + DebugCounters.OnPipelineSendAsync(); + haveLock = false; // transferring + return AwaitAndReleaseLock(pending); + } + + DebugCounters.OnPipelineFullSync(); + pending.GetAwaiter().GetResult(); + return Task.CompletedTask; + } + catch (Exception ex) + { + message.TrySetException(ex); + throw; + } + finally + { + if (haveLock) _semaphore.Release(); + } + + static async Task FullAsync(PipelinedConnection @this, IRespMessage message) + { + try + { + await @this._semaphore.WaitAsync(message.CancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + message.TrySetException(ex); + throw; + } + + try + { + await @this._tail.SendAsync(message).ConfigureAwait(false); + } + finally + { + @this._semaphore.Release(); + } + } + } + + private async Task AwaitAndReleaseLock(Task pending) + { + try + { + await pending.ConfigureAwait(false); + } + finally + { + _semaphore.Release(); + } + } + + private static void TrySetException(ReadOnlySpan messages, Exception ex) + { + foreach (var message in messages) + { + message.TrySetException(ex); + } + } + + public Task SendAsync(ReadOnlyMemory messages) + { + switch (messages.Length) + { + case 0: return Task.CompletedTask; + case 1: return SendAsync(messages.Span[0]); + } + bool haveLock = false; + try + { + haveLock = _semaphore.Wait(0); + if (!haveLock) + { + DebugCounters.OnPipelineFullAsync(); + return FullAsync(this, messages); + } + + var pending = _tail.SendAsync(messages); + if (!pending.IsCompleted) + { + DebugCounters.OnPipelineSendAsync(); + haveLock = false; // transferring + return AwaitAndReleaseLock(pending); + } + + DebugCounters.OnPipelineFullSync(); + pending.GetAwaiter().GetResult(); + return Task.CompletedTask; + } + catch (Exception ex) + { + TrySetException(messages.Span, ex); + throw; + } + finally + { + if (haveLock) _semaphore.Release(); + } + + static async Task FullAsync(PipelinedConnection @this, ReadOnlyMemory messages) + { + bool haveLock = false; // we don't have the lock initially + try + { + await @this._semaphore.WaitAsync(messages.Span[0].CancellationToken).ConfigureAwait(false); + haveLock = true; + await @this._tail.SendAsync(messages).ConfigureAwait(false); + } + catch (Exception ex) + { + TrySetException(messages.Span, ex); + throw; + } + finally + { + if (haveLock) + { + @this._semaphore.Release(); + } + } + } + } +} diff --git a/src/RESP.Core/PublicAPI/PublicAPI.Shipped.txt b/src/RESP.Core/PublicAPI/PublicAPI.Shipped.txt new file mode 100644 index 000000000..7dc5c5811 --- /dev/null +++ b/src/RESP.Core/PublicAPI/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/RESP.Core/PublicAPI/PublicAPI.Unshipped.txt b/src/RESP.Core/PublicAPI/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..91c1ad8f0 --- /dev/null +++ b/src/RESP.Core/PublicAPI/PublicAPI.Unshipped.txt @@ -0,0 +1,191 @@ +#nullable enable +abstract Resp.RespPayload.Dispose(bool disposing) -> void +abstract Resp.RespPayload.GetPayload() -> System.Buffers.ReadOnlySequence +override Resp.RespPayload.ToString() -> string! +override Resp.RespScanState.Equals(object? obj) -> bool +override Resp.RespScanState.GetHashCode() -> int +override Resp.RespScanState.ToString() -> string! +Resp.FrameScanInfo +Resp.FrameScanInfo.BytesRead.get -> long +Resp.FrameScanInfo.BytesRead.set -> void +Resp.FrameScanInfo.FrameScanInfo() -> void +Resp.FrameScanInfo.FrameScanInfo(bool isOutbound) -> void +Resp.FrameScanInfo.IsOutbound.get -> bool +Resp.FrameScanInfo.IsOutOfBand.get -> bool +Resp.FrameScanInfo.IsOutOfBand.set -> void +Resp.FrameScanInfo.ReadHint.get -> int +Resp.FrameScanInfo.ReadHint.set -> void +Resp.ICommandMap +Resp.ICommandMap.Map(scoped ref System.ReadOnlySpan command) -> void +Resp.IRespConnection +Resp.IRespConnection.CanWrite.get -> bool +Resp.IRespConnection.Outstanding.get -> int +Resp.IRespConnection.Send(Resp.RespPayload! payload) -> Resp.RespPayload! +Resp.IRespConnection.SendAsync(Resp.RespPayload! payload, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +Resp.IRespFormatter +Resp.IRespFormatter.Format(scoped System.ReadOnlySpan command, ref Resp.RespWriter writer, in TRequest request) -> void +Resp.IRespMetadataParser +Resp.IRespParser +Resp.IRespParser.Parse(in TRequest request, ref Resp.RespReader reader) -> TResponse +Resp.IRespParser +Resp.IRespParser.Parse(ref Resp.RespReader reader) -> TResponse +Resp.IRespSizeEstimator +Resp.IRespSizeEstimator.EstimateSize(scoped System.ReadOnlySpan command, in TRequest request) -> int +Resp.RedisCommands.RedisString +Resp.RedisCommands.RedisString.Get() -> string? +Resp.RedisCommands.RedisString.RedisString(Resp.IRespConnection! connection, string! key) -> void +Resp.RedisCommands.RedisString.Set(string! value) -> void +Resp.RedisCommands.RespConnectionExtensions +Resp.RespAttributeReader +Resp.RespAttributeReader.RespAttributeReader() -> void +Resp.RespConnectionExtensions +Resp.RespException +Resp.RespException.RespException(string! message) -> void +Resp.RespFrameScanner +Resp.RespFrameScanner.OnBeforeFrame(ref Resp.RespScanState state, ref Resp.FrameScanInfo info) -> void +Resp.RespFrameScanner.TryRead(ref Resp.RespScanState state, in System.Buffers.ReadOnlySequence data, ref Resp.FrameScanInfo info) -> System.Buffers.OperationStatus +Resp.RespFrameScanner.ValidateRequest(in System.Buffers.ReadOnlySequence message) -> void +Resp.RespPayload +Resp.RespPayload.Dispose() -> void +Resp.RespPayload.Payload.get -> System.Buffers.ReadOnlySequence +Resp.RespPayload.RespPayload() -> void +Resp.RespPayload.Validate(bool checkError = true) -> void +Resp.RespPrefix +Resp.RespPrefix.Array = 42 -> Resp.RespPrefix +Resp.RespPrefix.Attribute = 124 -> Resp.RespPrefix +Resp.RespPrefix.BigInteger = 40 -> Resp.RespPrefix +Resp.RespPrefix.Boolean = 35 -> Resp.RespPrefix +Resp.RespPrefix.BulkError = 33 -> Resp.RespPrefix +Resp.RespPrefix.BulkString = 36 -> Resp.RespPrefix +Resp.RespPrefix.Double = 44 -> Resp.RespPrefix +Resp.RespPrefix.Integer = 58 -> Resp.RespPrefix +Resp.RespPrefix.Map = 37 -> Resp.RespPrefix +Resp.RespPrefix.None = 0 -> Resp.RespPrefix +Resp.RespPrefix.Null = 95 -> Resp.RespPrefix +Resp.RespPrefix.Push = 62 -> Resp.RespPrefix +Resp.RespPrefix.Set = 126 -> Resp.RespPrefix +Resp.RespPrefix.SimpleError = 45 -> Resp.RespPrefix +Resp.RespPrefix.SimpleString = 43 -> Resp.RespPrefix +Resp.RespPrefix.StreamContinuation = 59 -> Resp.RespPrefix +Resp.RespPrefix.StreamTerminator = 46 -> Resp.RespPrefix +Resp.RespPrefix.VerbatimString = 61 -> Resp.RespPrefix +Resp.RespReader +Resp.RespReader.AggregateChildren() -> Resp.RespReader.AggregateEnumerator +Resp.RespReader.AggregateEnumerator +Resp.RespReader.AggregateEnumerator.AggregateEnumerator() -> void +Resp.RespReader.AggregateEnumerator.AggregateEnumerator(scoped in Resp.RespReader reader) -> void +Resp.RespReader.AggregateEnumerator.Current.get -> Resp.RespReader +Resp.RespReader.AggregateEnumerator.DemandNext() -> void +Resp.RespReader.AggregateEnumerator.FillAll(scoped System.Span target, Resp.RespReader.Projection! projection) -> void +Resp.RespReader.AggregateEnumerator.GetEnumerator() -> Resp.RespReader.AggregateEnumerator +Resp.RespReader.AggregateEnumerator.MoveNext() -> bool +Resp.RespReader.AggregateEnumerator.MoveNext(Resp.RespPrefix prefix) -> bool +Resp.RespReader.AggregateEnumerator.MoveNext(Resp.RespAttributeReader! respAttributeReader, ref T attributes) -> bool +Resp.RespReader.AggregateEnumerator.MoveNext(Resp.RespPrefix prefix, Resp.RespAttributeReader! respAttributeReader, ref T attributes) -> bool +Resp.RespReader.AggregateEnumerator.MovePast(out Resp.RespReader reader) -> void +Resp.RespReader.AggregateEnumerator.ReadOne(Resp.RespReader.Projection! projection) -> T +Resp.RespReader.AggregateEnumerator.Value -> Resp.RespReader +Resp.RespReader.AggregateLength() -> int +Resp.RespReader.BytesConsumed.get -> long +Resp.RespReader.CopyTo(System.Span target) -> int +Resp.RespReader.DemandAggregate() -> void +Resp.RespReader.DemandEnd() -> void +Resp.RespReader.DemandNotNull() -> void +Resp.RespReader.DemandScalar() -> void +Resp.RespReader.FillAll(scoped System.Span target, Resp.RespReader.Projection! projection) -> void +Resp.RespReader.Is(byte value) -> bool +Resp.RespReader.Is(System.ReadOnlySpan value) -> bool +Resp.RespReader.IsAggregate.get -> bool +Resp.RespReader.IsAttribute.get -> bool +Resp.RespReader.IsError.get -> bool +Resp.RespReader.IsNull.get -> bool +Resp.RespReader.IsScalar.get -> bool +Resp.RespReader.IsStreaming.get -> bool +Resp.RespReader.MoveNext() -> void +Resp.RespReader.MoveNext(Resp.RespPrefix prefix) -> void +Resp.RespReader.MoveNext(Resp.RespAttributeReader! respAttributeReader, ref T attributes) -> void +Resp.RespReader.MoveNext(Resp.RespPrefix prefix, Resp.RespAttributeReader! respAttributeReader, ref T attributes) -> void +Resp.RespReader.MoveNextAggregate() -> void +Resp.RespReader.MoveNextScalar() -> void +Resp.RespReader.ParseBytes(Resp.RespReader.Parser! parser, TState? state) -> T +Resp.RespReader.ParseBytes(Resp.RespReader.Parser! parser) -> T +Resp.RespReader.ParseChars(Resp.RespReader.Parser! parser, TState? state) -> T +Resp.RespReader.ParseChars(Resp.RespReader.Parser! parser) -> T +Resp.RespReader.Parser +Resp.RespReader.Parser +Resp.RespReader.Prefix.get -> Resp.RespPrefix +Resp.RespReader.Projection +Resp.RespReader.ReadBoolean() -> bool +Resp.RespReader.ReadDecimal() -> decimal +Resp.RespReader.ReadDouble() -> double +Resp.RespReader.ReadEnum(T unknownValue = default(T)) -> T +Resp.RespReader.ReadInt32() -> int +Resp.RespReader.ReadInt64() -> long +Resp.RespReader.ReadString() -> string? +Resp.RespReader.ReadString(out string! prefix) -> string? +Resp.RespReader.RespReader() -> void +Resp.RespReader.RespReader(scoped in System.Buffers.ReadOnlySequence value) -> void +Resp.RespReader.RespReader(System.ReadOnlySpan value) -> void +Resp.RespReader.ScalarChunks() -> Resp.RespReader.ScalarEnumerator +Resp.RespReader.ScalarEnumerator +Resp.RespReader.ScalarEnumerator.Current.get -> System.ReadOnlySpan +Resp.RespReader.ScalarEnumerator.CurrentLength.get -> int +Resp.RespReader.ScalarEnumerator.GetEnumerator() -> Resp.RespReader.ScalarEnumerator +Resp.RespReader.ScalarEnumerator.MoveNext() -> bool +Resp.RespReader.ScalarEnumerator.MovePast(out Resp.RespReader reader) -> void +Resp.RespReader.ScalarEnumerator.ScalarEnumerator() -> void +Resp.RespReader.ScalarEnumerator.ScalarEnumerator(scoped in Resp.RespReader reader) -> void +Resp.RespReader.ScalarIsEmpty() -> bool +Resp.RespReader.ScalarLength() -> int +Resp.RespReader.ScalarLongLength() -> long +Resp.RespReader.SkipChildren() -> void +Resp.RespReader.TryGetSpan(out System.ReadOnlySpan value) -> bool +Resp.RespReader.TryMoveNext() -> bool +Resp.RespReader.TryMoveNext(bool checkError) -> bool +Resp.RespReader.TryMoveNext(Resp.RespPrefix prefix) -> bool +Resp.RespReader.TryMoveNext(Resp.RespAttributeReader! respAttributeReader, ref T attributes) -> bool +Resp.RespReader.TryReadDouble(out double value, bool allowTokens = true) -> bool +Resp.RespReader.TryReadInt32(out int value) -> bool +Resp.RespReader.TryReadInt64(out long value) -> bool +Resp.RespReader.TryReadNext() -> bool +Resp.RespScanState +Resp.RespScanState.IsComplete.get -> bool +Resp.RespScanState.IsOutOfBand.get -> bool +Resp.RespScanState.RespScanState() -> void +Resp.RespScanState.TotalBytes.get -> long +Resp.RespScanState.TryRead(in System.Buffers.ReadOnlySequence value, out long bytesRead) -> bool +Resp.RespScanState.TryRead(ref Resp.RespReader reader, out long bytesRead) -> bool +Resp.RespScanState.TryRead(System.ReadOnlySpan value, out int bytesRead) -> bool +Resp.RespWriter +Resp.RespWriter.CommandMap.get -> Resp.ICommandMap? +Resp.RespWriter.CommandMap.set -> void +Resp.RespWriter.Flush() -> void +Resp.RespWriter.RespWriter() -> void +Resp.RespWriter.RespWriter(System.Buffers.IBufferWriter! target) -> void +Resp.RespWriter.RespWriter(System.Span target) -> void +Resp.RespWriter.WriteArray(int count) -> void +Resp.RespWriter.WriteBulkString(bool value) -> void +Resp.RespWriter.WriteBulkString(int value) -> void +Resp.RespWriter.WriteBulkString(long value) -> void +Resp.RespWriter.WriteBulkString(scoped System.ReadOnlySpan value) -> void +Resp.RespWriter.WriteBulkString(scoped System.ReadOnlySpan value) -> void +Resp.RespWriter.WriteBulkString(string! value) -> void +Resp.RespWriter.WriteCommand(scoped System.ReadOnlySpan command, int args) -> void +Resp.RespWriter.WriteRaw(scoped System.ReadOnlySpan buffer) -> void +static Resp.RespConnectionExtensions.Send(this Resp.IRespConnection! connection, scoped System.ReadOnlySpan command, TRequest request, Resp.IRespFormatter? formatter = null) -> Resp.RespPayload! +static Resp.RespFrameScanner.Default.get -> Resp.RespFrameScanner! +static Resp.RespFrameScanner.Subscription.get -> Resp.RespFrameScanner! +static Resp.RespPayload.Create(System.Buffers.ReadOnlySequence payload) -> Resp.RespPayload! +static Resp.RespPayload.Create(System.ReadOnlyMemory payload) -> Resp.RespPayload! +static Resp.RespScanState.Create(bool pubSubConnection) -> Resp.RespScanState +virtual Resp.RespAttributeReader.Read(ref Resp.RespReader reader, ref T value) -> void +virtual Resp.RespAttributeReader.ReadKeyValuePair(scoped System.ReadOnlySpan key, ref Resp.RespReader reader, ref T value) -> bool +virtual Resp.RespAttributeReader.ReadKeyValuePairs(ref Resp.RespReader reader, ref T value) -> int +virtual Resp.RespPayload.Wait(System.TimeSpan timeout) -> void +virtual Resp.RespPayload.WaitAsync() -> System.Threading.Tasks.Task! +virtual Resp.RespReader.Parser.Invoke(System.ReadOnlySpan value, TState? state) -> TValue +virtual Resp.RespReader.Parser.Invoke(System.ReadOnlySpan value) -> TValue +virtual Resp.RespReader.Projection.Invoke(ref Resp.RespReader value) -> T +Resp.RedisCommands.RedisString.GetAsync() -> System.Threading.Tasks.Task! +Resp.RedisCommands.RedisString.RedisString(Resp.IRespConnection! connection, string! key, System.Threading.CancellationToken cancellationToken) -> void +Resp.RedisCommands.RedisString.RedisString(Resp.IRespConnection! connection, string! key, System.TimeSpan timeout = default(System.TimeSpan)) -> void diff --git a/src/RESP.Core/PublicAPI/net5.0/PublicAPI.Shipped.txt b/src/RESP.Core/PublicAPI/net5.0/PublicAPI.Shipped.txt new file mode 100644 index 000000000..7dc5c5811 --- /dev/null +++ b/src/RESP.Core/PublicAPI/net5.0/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/RESP.Core/PublicAPI/net5.0/PublicAPI.Unshipped.txt b/src/RESP.Core/PublicAPI/net5.0/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..8ba652ba4 --- /dev/null +++ b/src/RESP.Core/PublicAPI/net5.0/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +#nullable enable +System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime) \ No newline at end of file diff --git a/src/RESP.Core/PublicAPI/net7.0/PublicAPI.Shipped.txt b/src/RESP.Core/PublicAPI/net7.0/PublicAPI.Shipped.txt new file mode 100644 index 000000000..7dc5c5811 --- /dev/null +++ b/src/RESP.Core/PublicAPI/net7.0/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/RESP.Core/PublicAPI/net7.0/PublicAPI.Unshipped.txt b/src/RESP.Core/PublicAPI/net7.0/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..76395b3e6 --- /dev/null +++ b/src/RESP.Core/PublicAPI/net7.0/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +#nullable enable +Resp.RespReader.ParseChars(System.IFormatProvider? formatProvider = null) -> T \ No newline at end of file diff --git a/src/RESP.Core/PublicAPI/net8.0/PublicAPI.Shipped.txt b/src/RESP.Core/PublicAPI/net8.0/PublicAPI.Shipped.txt new file mode 100644 index 000000000..815c92006 --- /dev/null +++ b/src/RESP.Core/PublicAPI/net8.0/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable \ No newline at end of file diff --git a/src/RESP.Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/RESP.Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..235faee15 --- /dev/null +++ b/src/RESP.Core/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +#nullable enable +Resp.RespReader.ParseBytes(System.IFormatProvider? formatProvider = null) -> T diff --git a/src/RESP.Core/README.md b/src/RESP.Core/README.md new file mode 100644 index 000000000..4a32b6bcc --- /dev/null +++ b/src/RESP.Core/README.md @@ -0,0 +1,3 @@ +# RESP.Core + +This library contains the low-level RESP (Redis, etc) APIs. It is not intended for general use. \ No newline at end of file diff --git a/src/RESP.Core/RESP.Core.csproj b/src/RESP.Core/RESP.Core.csproj new file mode 100644 index 000000000..5bdcb3e28 --- /dev/null +++ b/src/RESP.Core/RESP.Core.csproj @@ -0,0 +1,64 @@ + + + enable + + net461;netstandard2.0;net472;net6.0;net8.0;net9.0 + Resp + Low-level RESP (Redis, etc) APIs. + RESP.Core + RESP.Core + RESP.Core + RESP + true + true + README.md + $(NoWarn);CS1591 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FrameworkShims.cs + + + NullableHacks.cs + + + SkipLocalsInit.cs + + + \ No newline at end of file diff --git a/src/RESP.Core/Raw.cs b/src/RESP.Core/Raw.cs new file mode 100644 index 000000000..0328e7961 --- /dev/null +++ b/src/RESP.Core/Raw.cs @@ -0,0 +1,139 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +#if NETCOREAPP3_0_OR_GREATER +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif + +namespace Resp; + +/// +/// Pre-computed payload fragments, for high-volume scenarios / common values. +/// +/// +/// CPU-endianness applies here; we can't just use "const" - however, modern JITs treat "static readonly" *almost* the same as "const", so: meh. +/// +internal static class Raw +{ + public static ulong Create64(ReadOnlySpan bytes, int length) + { + if (length != bytes.Length) + { + throw new ArgumentException($"Length check failed: {length} vs {bytes.Length}, value: {RespConstants.UTF8.GetString(bytes)}", nameof(length)); + } + if (length < 0 || length > sizeof(ulong)) + { + throw new ArgumentOutOfRangeException(nameof(length), $"Invalid length {length} - must be 0-{sizeof(ulong)}"); + } + + // this *will* be aligned; this approach intentionally chosen for parity with write + Span scratch = stackalloc byte[sizeof(ulong)]; + if (length != sizeof(ulong)) scratch.Slice(length).Clear(); + bytes.CopyTo(scratch); + return Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(scratch)); + } + + public static uint Create32(ReadOnlySpan bytes, int length) + { + if (length != bytes.Length) + { + throw new ArgumentException($"Length check failed: {length} vs {bytes.Length}, value: {RespConstants.UTF8.GetString(bytes)}", nameof(length)); + } + if (length < 0 || length > sizeof(uint)) + { + throw new ArgumentOutOfRangeException(nameof(length), $"Invalid length {length} - must be 0-{sizeof(uint)}"); + } + + // this *will* be aligned; this approach intentionally chosen for parity with write + Span scratch = stackalloc byte[sizeof(uint)]; + if (length != sizeof(uint)) scratch.Slice(length).Clear(); + bytes.CopyTo(scratch); + return Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(scratch)); + } + + public static ulong BulkStringEmpty_6 = Create64("$0\r\n\r\n"u8, 6); + + public static ulong BulkStringInt32_M1_8 = Create64("$2\r\n-1\r\n"u8, 8); + public static ulong BulkStringInt32_0_7 = Create64("$1\r\n0\r\n"u8, 7); + public static ulong BulkStringInt32_1_7 = Create64("$1\r\n1\r\n"u8, 7); + public static ulong BulkStringInt32_2_7 = Create64("$1\r\n2\r\n"u8, 7); + public static ulong BulkStringInt32_3_7 = Create64("$1\r\n3\r\n"u8, 7); + public static ulong BulkStringInt32_4_7 = Create64("$1\r\n4\r\n"u8, 7); + public static ulong BulkStringInt32_5_7 = Create64("$1\r\n5\r\n"u8, 7); + public static ulong BulkStringInt32_6_7 = Create64("$1\r\n6\r\n"u8, 7); + public static ulong BulkStringInt32_7_7 = Create64("$1\r\n7\r\n"u8, 7); + public static ulong BulkStringInt32_8_7 = Create64("$1\r\n8\r\n"u8, 7); + public static ulong BulkStringInt32_9_7 = Create64("$1\r\n9\r\n"u8, 7); + public static ulong BulkStringInt32_10_8 = Create64("$2\r\n10\r\n"u8, 8); + + public static ulong BulkStringPrefix_M1_5 = Create64("$-1\r\n"u8, 5); + public static uint BulkStringPrefix_0_4 = Create32("$0\r\n"u8, 4); + public static uint BulkStringPrefix_1_4 = Create32("$1\r\n"u8, 4); + public static uint BulkStringPrefix_2_4 = Create32("$2\r\n"u8, 4); + public static uint BulkStringPrefix_3_4 = Create32("$3\r\n"u8, 4); + public static uint BulkStringPrefix_4_4 = Create32("$4\r\n"u8, 4); + public static uint BulkStringPrefix_5_4 = Create32("$5\r\n"u8, 4); + public static uint BulkStringPrefix_6_4 = Create32("$6\r\n"u8, 4); + public static uint BulkStringPrefix_7_4 = Create32("$7\r\n"u8, 4); + public static uint BulkStringPrefix_8_4 = Create32("$8\r\n"u8, 4); + public static uint BulkStringPrefix_9_4 = Create32("$9\r\n"u8, 4); + public static ulong BulkStringPrefix_10_5 = Create64("$10\r\n"u8, 5); + + public static ulong ArrayPrefix_M1_5 = Create64("*-1\r\n"u8, 5); + public static uint ArrayPrefix_0_4 = Create32("*0\r\n"u8, 4); + public static uint ArrayPrefix_1_4 = Create32("*1\r\n"u8, 4); + public static uint ArrayPrefix_2_4 = Create32("*2\r\n"u8, 4); + public static uint ArrayPrefix_3_4 = Create32("*3\r\n"u8, 4); + public static uint ArrayPrefix_4_4 = Create32("*4\r\n"u8, 4); + public static uint ArrayPrefix_5_4 = Create32("*5\r\n"u8, 4); + public static uint ArrayPrefix_6_4 = Create32("*6\r\n"u8, 4); + public static uint ArrayPrefix_7_4 = Create32("*7\r\n"u8, 4); + public static uint ArrayPrefix_8_4 = Create32("*8\r\n"u8, 4); + public static uint ArrayPrefix_9_4 = Create32("*9\r\n"u8, 4); + public static ulong ArrayPrefix_10_5 = Create64("*10\r\n"u8, 5); + +#if NETCOREAPP3_0_OR_GREATER + private static uint FirstAndLast(char first, char last) + { + Debug.Assert(first < 128 && last < 128, "ASCII please"); + Span scratch = [(byte)first, 0, 0, (byte)last]; + // this *will* be aligned; this approach intentionally chosen for how we read + return Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(scratch)); + } + + public const int CommonRespIndex_Success = 0; + public const int CommonRespIndex_SingleDigitInteger = 1; + public const int CommonRespIndex_DoubleDigitInteger = 2; + public const int CommonRespIndex_SingleDigitString = 3; + public const int CommonRespIndex_DoubleDigitString = 4; + public const int CommonRespIndex_SingleDigitArray = 5; + public const int CommonRespIndex_DoubleDigitArray = 6; + public const int CommonRespIndex_Error = 7; + + public static readonly Vector256 CommonRespPrefixes = Vector256.Create( + FirstAndLast('+', '\r'), // success +OK\r\n + FirstAndLast(':', '\n'), // single-digit integer :4\r\n + FirstAndLast(':', '\r'), // double-digit integer :42\r\n + FirstAndLast('$', '\n'), // 0-9 char string $0\r\n\r\n + FirstAndLast('$', '\r'), // null/10-99 char string $-1\r\n or $10\r\nABCDEFGHIJ\r\n + FirstAndLast('*', '\n'), // 0-9 length array *0\r\n + FirstAndLast('*', '\r'), // null/10-99 length array *-1\r\n or *10\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n + FirstAndLast('-', 'R')); // common errors -ERR something bad happened + + public static readonly Vector256 FirstLastMask = CreateUInt32(0xFF0000FF); + + private static Vector256 CreateUInt32(uint value) + { +#if NET7_0_OR_GREATER + return Vector256.Create(value); +#else + return Vector256.Create(value, value, value, value, value, value, value, value); +#endif + } + +#endif +} diff --git a/src/RESP.Core/RespAttributeReader.cs b/src/RESP.Core/RespAttributeReader.cs new file mode 100644 index 000000000..5d65a9200 --- /dev/null +++ b/src/RESP.Core/RespAttributeReader.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Resp; + +/// +/// Allows attribute data to be parsed conveniently. +/// +/// The type of data represented by this reader. +public abstract class RespAttributeReader +{ + /// + /// Parse a group of attributes. + /// + public virtual void Read(ref RespReader reader, ref T value) + { + reader.Demand(RespPrefix.Attribute); + _ = ReadKeyValuePairs(ref reader, ref value); + } + + /// + /// Parse an aggregate as a set of key/value pairs. + /// + /// The number of pairs successfully processed. + protected virtual int ReadKeyValuePairs(ref RespReader reader, ref T value) + { + var iterator = reader.AggregateChildren(); + + byte[] pooledBuffer = []; + Span localBuffer = stackalloc byte[128]; + int count = 0; + while (iterator.MoveNext() && iterator.Value.TryReadNext()) + { + if (iterator.Value.IsScalar) + { + var key = iterator.Value.Buffer(ref pooledBuffer, localBuffer); + + if (iterator.MoveNext() && iterator.Value.TryReadNext()) + { + if (ReadKeyValuePair(key, ref iterator.Value, ref value)) + { + count++; + } + } + else + { + break; // no matching value for this key + } + } + else + { + if (iterator.MoveNext() && iterator.Value.TryReadNext()) + { + // we won't try to handle aggregate keys; skip the value + } + else + { + break; // no matching value for this key + } + } + } + iterator.MovePast(out reader); + return count; + } + + /// + /// Parse an individual key/value pair. + /// + /// True if the pair was successfully processed. + public virtual bool ReadKeyValuePair(scoped ReadOnlySpan key, ref RespReader reader, ref T value) => false; +} diff --git a/src/RESP.Core/RespCommandAttribute.cs b/src/RESP.Core/RespCommandAttribute.cs new file mode 100644 index 000000000..be8c1e037 --- /dev/null +++ b/src/RESP.Core/RespCommandAttribute.cs @@ -0,0 +1,36 @@ +using System; +using System.Diagnostics; + +namespace Resp; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +[Conditional("DEBUG")] +public sealed class RespCommandAttribute(string? command = null) : Attribute +{ + public string? Command => command; + public string? Formatter { get; set; } + public string? Parser { get; set; } + + public static class Parsers + { + private const string Prefix = "global::Resp.RespParsers."; + + /// + public const string Summary = "global::Resp." + nameof(ResponseSummary) + "." + nameof(ResponseSummary.Parser); + + public const string ByteArray = Prefix + nameof(RespParsers.ByteArray); + public const string String = Prefix + nameof(RespParsers.String); + public const string Int32 = Prefix + nameof(RespParsers.Int32); + public const string Int64 = Prefix + nameof(RespParsers.Int64); + public const string NullableInt64 = Prefix + nameof(RespParsers.NullableInt64); + public const string NullableInt32 = Prefix + nameof(RespParsers.NullableInt32); + public const string NullableSingle = Prefix + nameof(RespParsers.NullableSingle); + public const string BufferWriter = Prefix + nameof(RespParsers.BufferWriter); + public const string ByteArrayArray = Prefix + nameof(RespParsers.ByteArrayArray); + public const string OK = Prefix + nameof(RespParsers.OK); + public const string Single = Prefix + nameof(RespParsers.Single); + public const string Double = Prefix + nameof(RespParsers.Double); + public const string Success = Prefix + nameof(RespParsers.Success); + public const string NullableDouble = Prefix + nameof(RespParsers.NullableDouble); + } +} diff --git a/src/RESP.Core/RespConnectionExtensions.cs b/src/RESP.Core/RespConnectionExtensions.cs new file mode 100644 index 000000000..3d9d38785 --- /dev/null +++ b/src/RESP.Core/RespConnectionExtensions.cs @@ -0,0 +1,287 @@ +// #define PREFER_SYNC_WRITE // makes async calls use synchronous writes + +using System; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Resp; + +public interface IRespFormatter +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif +{ + void Format(scoped ReadOnlySpan command, ref RespWriter writer, in TRequest request); +} + +public interface IRespSizeEstimator : IRespFormatter +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif +{ + int EstimateSize(scoped ReadOnlySpan command, in TRequest request); +} + +public interface IRespParser +{ + TResponse Parse(in TState state, ref RespReader reader); +} + +internal interface IRespInternalMessage : IRespMessage +{ + bool AllowInlineParsing { get; } +} + +internal interface IRespInlineParser // if implemented, parsing is permitted on the IO thread +{ +} + +public interface IRespMetadataParser // if implemented, the consumer must manually advance to the content +{ +} + +public abstract class RespCommandMap +{ + /// + /// Apply any remapping to the command. + /// + /// The command requested. + /// The remapped command; this can be the original command, a remapped command, or an empty instance if the command is not available. + public abstract ReadOnlySpan Map(ReadOnlySpan command); + + /// + /// Indicates whether the specified command is available. + /// + public virtual bool IsAvailable(ReadOnlySpan command) + => Map(command).Length != 0; + + public static RespCommandMap Default { get; } = new DefaultRespCommandMap(); + + private sealed class DefaultRespCommandMap : RespCommandMap + { + public override ReadOnlySpan Map(ReadOnlySpan command) => command; + public override bool IsAvailable(ReadOnlySpan command) => true; + } +} + +/// +/// Over-arching configuration for a RESP system. +/// +public class RespConfiguration +{ + private static readonly TimeSpan DefaultSyncTimeout = TimeSpan.FromSeconds(10); + + public static RespConfiguration Default { get; } = new( + RespCommandMap.Default, [], DefaultSyncTimeout, NullServiceProvider.Instance); + + public static Builder Create() => default; // for discoverability + + public struct Builder // intentionally mutable + { + public TimeSpan? SyncTimeout { get; set; } + public IServiceProvider? ServiceProvider { get; set; } + public RespCommandMap? CommandMap { get; set; } + public object? KeyPrefix { get; set; } // can be a string or byte[] + + public Builder(RespConfiguration? source) + { + if (source is not null) + { + CommandMap = source.RespCommandMap; + SyncTimeout = source.SyncTimeout; + KeyPrefix = source.KeyPrefix.ToArray(); + ServiceProvider = source.ServiceProvider; + // undo defaults + if (ReferenceEquals(CommandMap, RespCommandMap.Default)) CommandMap = null; + if (ReferenceEquals(ServiceProvider, NullServiceProvider.Instance)) ServiceProvider = null; + } + } + + public RespConfiguration Create() + { + byte[] prefix = KeyPrefix switch + { + null => [], + string { Length: 0 } => [], + string s => Encoding.UTF8.GetBytes(s), + byte[] { Length: 0 } => [], + byte[] b => b.AsSpan().ToArray(), // create isolated copy for mutability reasons + _ => throw new ArgumentException("KeyPrefix must be a string or byte[]", nameof(KeyPrefix)), + }; + + if (prefix.Length == 0 & SyncTimeout is null & CommandMap is null & ServiceProvider is null) return Default; + + return new( + CommandMap ?? RespCommandMap.Default, + prefix, + SyncTimeout ?? DefaultSyncTimeout, + ServiceProvider ?? NullServiceProvider.Instance); + } + } + + private RespConfiguration( + RespCommandMap respCommandMap, + byte[] keyPrefix, + TimeSpan syncTimeout, + IServiceProvider serviceProvider) + { + RespCommandMap = respCommandMap; + SyncTimeout = syncTimeout; + _keyPrefix = (byte[])keyPrefix.Clone(); // create isolated copy + ServiceProvider = serviceProvider; + } + + private readonly byte[] _keyPrefix; + public IServiceProvider ServiceProvider { get; } + public RespCommandMap RespCommandMap { get; } + public TimeSpan SyncTimeout { get; } + public ReadOnlySpan KeyPrefix => _keyPrefix; + + public Builder AsBuilder() => new(this); + + private sealed class NullServiceProvider : IServiceProvider + { + public static readonly NullServiceProvider Instance = new(); + private NullServiceProvider() { } + public object? GetService(Type serviceType) => null; + } + + internal T? GetService() where T : class + => ServiceProvider.GetService(typeof(T)) as T; +} + +/// +/// Transient state for a RESP operation. +/// +public readonly struct RespContext +{ + private readonly IRespConnection _connection; + private readonly int _database; + private readonly CancellationToken _cancellationToken; + + private const string CtorUsageWarning = $"The context from {nameof(IRespConnection)}.{nameof(IRespConnection.Context)} should be preferred, using {nameof(WithCancellationToken)} etc as necessary."; + + /// + public override string ToString() => _connection?.ToString() ?? "(null)"; + + [Obsolete(CtorUsageWarning)] + public RespContext(IRespConnection connection) : this(connection, -1, CancellationToken.None) + { + } + + [Obsolete(CtorUsageWarning)] + public RespContext(IRespConnection connection, CancellationToken cancellationToken) + : this(connection, -1, cancellationToken) + { + } + + /// + /// Transient state for a RESP operation. + /// + [Obsolete(CtorUsageWarning)] + public RespContext( + IRespConnection connection, + int database = -1, + CancellationToken cancellationToken = default) + { + _connection = connection; + _database = database; + _cancellationToken = cancellationToken; + } + + public IRespConnection Connection => _connection; + public int Database => _database; + public CancellationToken CancellationToken => _cancellationToken; + + public RespMessageBuilder Command(ReadOnlySpan command, T value, IRespFormatter formatter) + => new(this, command, value, formatter); + + public RespMessageBuilder Command(ReadOnlySpan command) + => new(this, command, Void.Instance, RespFormatters.Void); + + public RespMessageBuilder Command(ReadOnlySpan command, string value, bool isKey) + => new(this, command, value, RespFormatters.String(isKey)); + + public RespMessageBuilder Command(ReadOnlySpan command, byte[] value, bool isKey) + => new(this, command, value, RespFormatters.ByteArray(isKey)); + + public RespCommandMap RespCommandMap => _connection.Configuration.RespCommandMap; + + public RespContext WithCancellationToken(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + RespContext clone = this; + Unsafe.AsRef(in clone._cancellationToken) = cancellationToken; + return clone; + } + + public RespContext WithDatabase(int database) + { + RespContext clone = this; + Unsafe.AsRef(in clone._database) = database; + return clone; + } + + public RespContext WithConnection(IRespConnection connection) + { + RespContext clone = this; + Unsafe.AsRef(in clone._connection) = connection; + return clone; + } + + public IBatchConnection CreateBatch(int sizeHint = 0) => new BatchConnection(in this, sizeHint); + + internal static RespContext For(IRespConnection connection) +#pragma warning disable CS0618 // Type or member is obsolete + => new(connection); +#pragma warning restore CS0618 // Type or member is obsolete +} + +public static class RespConnectionExtensions +{ + /// + /// Enforces stricter ordering guarantees, so that unawaited async operations cannot cause overlapping writes. + /// + public static IRespConnection ForPipeline(this IRespConnection connection) + => connection is PipelinedConnection ? connection : new PipelinedConnection(in connection.Context); + + public static IRespConnection WithConfiguration(this IRespConnection connection, RespConfiguration configuration) + => ReferenceEquals(configuration, connection.Configuration) + ? connection + : new ConfiguredConnection(connection, configuration); + + private sealed class ConfiguredConnection : IRespConnection + { + private readonly IRespConnection _tail; + private readonly RespConfiguration _configuration; + private readonly RespContext _context; + + public ref readonly RespContext Context => ref _context; + public ConfiguredConnection(IRespConnection tail, RespConfiguration configuration) + { + _tail = tail; + _configuration = configuration; + _context = RespContext.For(this); + } + + public void Dispose() => _tail.Dispose(); + + public ValueTask DisposeAsync() => _tail.DisposeAsync(); + + public RespConfiguration Configuration => _configuration; + + public bool CanWrite => _tail.CanWrite; + + public int Outstanding => _tail.Outstanding; + + public void Send(IRespMessage message) => _tail.Send(message); + public void Send(ReadOnlySpan messages) => _tail.Send(messages); + + public Task SendAsync(IRespMessage message) => + _tail.SendAsync(message); + + public Task SendAsync(ReadOnlyMemory messages) => _tail.SendAsync(messages); + } +} diff --git a/src/RESP.Core/RespConnectionPool.cs b/src/RESP.Core/RespConnectionPool.cs new file mode 100644 index 000000000..71a3f97bf --- /dev/null +++ b/src/RESP.Core/RespConnectionPool.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Concurrent; +using System.ComponentModel; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Resp; + +public sealed class RespConnectionPool : IDisposable +{ + private readonly RespConfiguration _configuration; + private const int DefaultCount = 10; + private bool _isDisposed; + + [Obsolete("This is for testing only")] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public bool UseCustomNetworkStream { get; set; } + + private readonly ConcurrentQueue _pool = []; + private readonly Func _createConnection; + private readonly int _count; + + public RespConnectionPool( + Func createConnection, + RespConfiguration? configuration = null, + int count = RespConnectionPool.DefaultCount) + { + _createConnection = createConnection; + _count = count; + _configuration = configuration ?? RespConfiguration.Default; + } + + public RespConnectionPool( + IPAddress? address = null, + int port = 6379, + RespConfiguration? configuration = null, + int count = DefaultCount) + : this(new IPEndPoint(address ?? IPAddress.Loopback, port), configuration, count) + { + } + + public RespConnectionPool(EndPoint endPoint, RespConfiguration? configuration = null, int count = DefaultCount) + { +#pragma warning disable CS0618 // Type or member is obsolete + _createConnection = config => CreateConnection(config, endPoint, UseCustomNetworkStream); +#pragma warning restore CS0618 // Type or member is obsolete + _count = count; + _configuration = configuration ?? RespConfiguration.Default; + } + + /// + /// Borrow a connection from the pool. + /// + /// The database to override in the context of the leased connection. + /// The cancellation token to override in the context of the leased connection. + public IRespConnection GetConnection(int? database = null, CancellationToken? cancellationToken = null) + { + ThrowIfDisposed(); + if (cancellationToken.HasValue) + { + cancellationToken.GetValueOrDefault().ThrowIfCancellationRequested(); + } + + if (!_pool.TryDequeue(out var connection)) + { + connection = _createConnection(_configuration); + } + + return new PoolWrapper(this, connection, database, cancellationToken); + } + + private void ThrowIfDisposed() + { + if (_isDisposed) Throw(); + static void Throw() => throw new ObjectDisposedException(nameof(RespConnectionPool)); + } + + public void Dispose() + { + _isDisposed = true; + while (_pool.TryDequeue(out var connection)) + { + connection.Dispose(); + } + } + + private void Return(IRespConnection tail) + { + if (_isDisposed || !tail.CanWrite || _pool.Count >= _count) + { + tail.Dispose(); + } + else + { + _pool.Enqueue(tail); + } + } + + private static IRespConnection CreateConnection(RespConfiguration config, EndPoint endpoint, bool useCustom) + { + Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.NoDelay = true; + socket.Connect(endpoint); + return new DirectWriteConnection(config, Wrap(socket, useCustom)); + + static Stream Wrap(Socket socket, bool useCustom) + { +#if NETCOREAPP3_0_OR_GREATER + if (useCustom) return new CustomNetworkStream(socket); +#endif + return new NetworkStream(socket); + } + } + + private sealed class PoolWrapper : IRespConnection + { + private bool _isDisposed; + private readonly RespConnectionPool _pool; + private readonly IRespConnection _tail; + private readonly RespContext _context; + + public ref readonly RespContext Context => ref _context; + + public PoolWrapper( + RespConnectionPool pool, + IRespConnection tail, + int? database, + CancellationToken? cancellationToken) + { + _pool = pool; + _tail = tail; + _context = RespContext.For(this); + if (database.HasValue) _context = _context.WithDatabase(database.GetValueOrDefault()); + if (cancellationToken.HasValue) + _context = _context.WithCancellationToken(cancellationToken.GetValueOrDefault()); + } + + public void Dispose() + { + _isDisposed = true; + _pool.Return(_tail); + } + + public bool CanWrite => !_isDisposed && _tail.CanWrite; + + public int Outstanding => _tail.Outstanding; + + public RespConfiguration Configuration => _tail.Configuration; + + private void ThrowIfDisposed() + { + if (_isDisposed) Throw(); + static void Throw() => throw new ObjectDisposedException(nameof(PoolWrapper)); + } + + public ValueTask DisposeAsync() + { + Dispose(); + return default; + } + + public void Send(IRespMessage message) + { + ThrowIfDisposed(); + _tail.Send(message); + } + + public void Send(ReadOnlySpan messages) + { + ThrowIfDisposed(); + _tail.Send(messages); + } + + public Task SendAsync(IRespMessage message) + { + ThrowIfDisposed(); + return _tail.SendAsync(message); + } + + public Task SendAsync(ReadOnlyMemory messages) + { + ThrowIfDisposed(); + return _tail.SendAsync(messages); + } + } +} diff --git a/src/RESP.Core/RespConstants.cs b/src/RESP.Core/RespConstants.cs new file mode 100644 index 000000000..4eff2a509 --- /dev/null +++ b/src/RESP.Core/RespConstants.cs @@ -0,0 +1,52 @@ +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace Resp; + +internal static class RespConstants +{ + public static readonly UTF8Encoding UTF8 = new(false); + + public static ReadOnlySpan CrlfBytes => "\r\n"u8; + + public static readonly ushort CrLfUInt16 = UnsafeCpuUInt16(CrlfBytes); + + public static ReadOnlySpan OKBytes => "OK"u8; + public static readonly ushort OKUInt16 = UnsafeCpuUInt16(OKBytes); + + public static readonly uint BulkStringStreaming = UnsafeCpuUInt32("$?\r\n"u8); + public static readonly uint BulkStringNull = UnsafeCpuUInt32("$-1\r"u8); + + public static readonly uint ArrayStreaming = UnsafeCpuUInt32("*?\r\n"u8); + public static readonly uint ArrayNull = UnsafeCpuUInt32("*-1\r"u8); + + public static ushort UnsafeCpuUInt16(ReadOnlySpan bytes) + => Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(bytes)); + public static ushort UnsafeCpuUInt16(ReadOnlySpan bytes, int offset) + => Unsafe.ReadUnaligned(ref Unsafe.Add(ref MemoryMarshal.GetReference(bytes), offset)); + public static byte UnsafeCpuByte(ReadOnlySpan bytes, int offset) + => Unsafe.Add(ref MemoryMarshal.GetReference(bytes), offset); + public static uint UnsafeCpuUInt32(ReadOnlySpan bytes) + => Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(bytes)); + public static uint UnsafeCpuUInt32(ReadOnlySpan bytes, int offset) + => Unsafe.ReadUnaligned(ref Unsafe.Add(ref MemoryMarshal.GetReference(bytes), offset)); + public static ulong UnsafeCpuUInt64(ReadOnlySpan bytes) + => Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(bytes)); + public static ushort CpuUInt16(ushort bigEndian) + => BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(bigEndian) : bigEndian; + public static uint CpuUInt32(uint bigEndian) + => BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(bigEndian) : bigEndian; + public static ulong CpuUInt64(ulong bigEndian) + => BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(bigEndian) : bigEndian; + + public const int MaxRawBytesInt32 = 11, // "-2147483648" + MaxRawBytesInt64 = 20, // "-9223372036854775808", + MaxProtocolBytesIntegerInt32 = MaxRawBytesInt32 + 3, // ?X10X\r\n where ? could be $, *, etc - usually a length prefix + MaxProtocolBytesBulkStringIntegerInt32 = MaxRawBytesInt32 + 7, // $NN\r\nX11X\r\n for NN (length) 1-11 + MaxProtocolBytesBulkStringIntegerInt64 = MaxRawBytesInt64 + 7, // $NN\r\nX20X\r\n for NN (length) 1-20 + MaxRawBytesNumber = 20, // note G17 format, allow 20 for payload + MaxProtocolBytesBytesNumber = MaxRawBytesNumber + 7; // $NN\r\nX...X\r\n for NN (length) 1-20 +} diff --git a/src/RESP.Core/RespException.cs b/src/RESP.Core/RespException.cs new file mode 100644 index 000000000..edce85fb8 --- /dev/null +++ b/src/RESP.Core/RespException.cs @@ -0,0 +1,10 @@ +using System; + +namespace Resp; + +/// +/// Represents a RESP error message. +/// +public sealed class RespException(string message) : Exception(message) +{ +} diff --git a/src/RESP.Core/RespFormatters.cs b/src/RESP.Core/RespFormatters.cs new file mode 100644 index 000000000..d0ab98d9e --- /dev/null +++ b/src/RESP.Core/RespFormatters.cs @@ -0,0 +1,61 @@ +using System; + +namespace Resp; + +public static class RespFormatters +{ + public static IRespFormatter String(bool isKey) => isKey ? Key.String : Value.String; + public static IRespFormatter ByteArray(bool isKey) => isKey ? Key.ByteArray : Value.ByteArray; + public static class Key + { + // ReSharper disable once MemberHidesStaticFromOuterClass + public static IRespFormatter String => Formatter.Default; + // ReSharper disable once MemberHidesStaticFromOuterClass + public static IRespFormatter ByteArray => Formatter.Default; + + internal sealed class Formatter : IRespFormatter, IRespFormatter + { + private Formatter() { } + public static readonly Formatter Default = new(); + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in string value) + { + writer.WriteCommand(command, 1); + writer.WriteKey(value); + } + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in byte[] value) + { + writer.WriteCommand(command, 1); + writer.WriteKey(value); + } + } + } + + public static class Value + { + // ReSharper disable once MemberHidesStaticFromOuterClass + public static IRespFormatter String => Formatter.Default; + // ReSharper disable once MemberHidesStaticFromOuterClass + public static IRespFormatter ByteArray => Formatter.Default; + + internal sealed class Formatter : IRespFormatter, IRespFormatter, IRespFormatter + { + private Formatter() { } + public static readonly Formatter Default = new(); + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in Void value) + { + writer.WriteCommand(command, 0); + } + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in string value) + { + writer.WriteCommand(command, 1); + writer.WriteBulkString(value); + } + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in byte[] value) + { + writer.WriteCommand(command, 1); + writer.WriteBulkString(value); + } + } + } + public static IRespFormatter Void => Value.Formatter.Default; +} diff --git a/src/RESP.Core/RespFrameScanner.cs b/src/RESP.Core/RespFrameScanner.cs new file mode 100644 index 000000000..628b4c65b --- /dev/null +++ b/src/RESP.Core/RespFrameScanner.cs @@ -0,0 +1,193 @@ +using System; +using System.Buffers; +using static Resp.RespConstants; +namespace Resp; + +/// +/// Scans RESP frames. +/// . +public sealed class RespFrameScanner // : IFrameSacanner, IFrameValidator +{ + /// + /// Gets a frame scanner for RESP2 request/response connections, or RESP3 connections. + /// + public static RespFrameScanner Default { get; } = new(false); + + /// + /// Gets a frame scanner that identifies RESP2 pub/sub messages. + /// + public static RespFrameScanner Subscription { get; } = new(true); + private RespFrameScanner(bool pubsub) => _pubsub = pubsub; + private readonly bool _pubsub; + + private static readonly uint FastNull = UnsafeCpuUInt32("_\r\n\0"u8), + SingleCharScalarMask = CpuUInt32(0xFF00FFFF), + SingleDigitInteger = UnsafeCpuUInt32(":\0\r\n"u8), + EitherBoolean = UnsafeCpuUInt32("#\0\r\n"u8), + FirstThree = CpuUInt32(0xFFFFFF00); + private static readonly ulong OK = UnsafeCpuUInt64("+OK\r\n\0\0\0"u8), + PONG = UnsafeCpuUInt64("+PONG\r\n\0"u8), + DoubleCharScalarMask = CpuUInt64(0xFF0000FFFF000000), + DoubleDigitInteger = UnsafeCpuUInt64(":\0\0\r\n"u8), + FirstFive = CpuUInt64(0xFFFFFFFFFF000000), + FirstSeven = CpuUInt64(0xFFFFFFFFFFFFFF00); + + private const OperationStatus UseReader = (OperationStatus)(-1); + private static OperationStatus TryFastRead(ReadOnlySpan data, ref RespScanState info) + { + // use silly math to detect the most common short patterns without needing + // to access a reader, or use indexof etc; handles: + // +OK\r\n + // +PONG\r\n + // :N\r\n for any single-digit N (integer) + // :NN\r\n for any double-digit N (integer) + // #N\r\n for any single-digit N (boolean) + // _\r\n (null) + uint hi, lo; + switch (data.Length) + { + case 0: + case 1: + case 2: + return OperationStatus.NeedMoreData; + case 3: + hi = (((uint)UnsafeCpuUInt16(data)) << 16) | (((uint)UnsafeCpuByte(data, 2)) << 8); + break; + default: + hi = UnsafeCpuUInt32(data); + break; + } + if ((hi & FirstThree) == FastNull) + { + info.SetComplete(3, RespPrefix.Null); + return OperationStatus.Done; + } + + var masked = hi & SingleCharScalarMask; + if (masked == SingleDigitInteger) + { + info.SetComplete(4, RespPrefix.Integer); + return OperationStatus.Done; + } + else if (masked == EitherBoolean) + { + info.SetComplete(4, RespPrefix.Boolean); + return OperationStatus.Done; + } + + switch (data.Length) + { + case 3: + return OperationStatus.NeedMoreData; + case 4: + return UseReader; + case 5: + lo = ((uint)data[4]) << 24; + break; + case 6: + lo = ((uint)UnsafeCpuUInt16(data, 4)) << 16; + break; + case 7: + lo = ((uint)UnsafeCpuUInt16(data, 4)) << 16 | ((uint)UnsafeCpuByte(data, 6)) << 8; + break; + default: + lo = UnsafeCpuUInt32(data, 4); + break; + } + var u64 = BitConverter.IsLittleEndian ? ((((ulong)lo) << 32) | hi) : ((((ulong)hi) << 32) | lo); + if (((u64 & FirstFive) == OK) | ((u64 & DoubleCharScalarMask) == DoubleDigitInteger)) + { + info.SetComplete(5, RespPrefix.SimpleString); + return OperationStatus.Done; + } + if ((u64 & FirstSeven) == PONG) + { + info.SetComplete(7, RespPrefix.SimpleString); + return OperationStatus.Done; + } + return UseReader; + } + + /// + /// Attempt to read more data as part of the current frame. + /// + public OperationStatus TryRead(ref RespScanState state, in ReadOnlySequence data) + { + if (!_pubsub & state.TotalBytes == 0 & data.IsSingleSegment) + { +#if NETCOREAPP3_1_OR_GREATER + var status = TryFastRead(data.FirstSpan, ref state); +#else + var status = TryFastRead(data.First.Span, ref state); +#endif + if (status != UseReader) return status; + } + + return TryReadViaReader(ref state, in data); + + static OperationStatus TryReadViaReader(ref RespScanState state, in ReadOnlySequence data) + { + var reader = new RespReader(in data); + var complete = state.TryRead(ref reader, out var consumed); + if (complete) + { + return OperationStatus.Done; + } + return OperationStatus.NeedMoreData; + } + } + + /// + /// Attempt to read more data as part of the current frame. + /// + public OperationStatus TryRead(ref RespScanState state, ReadOnlySpan data) + { + if (!_pubsub & state.TotalBytes == 0) + { +#if NETCOREAPP3_1_OR_GREATER + var status = TryFastRead(data, ref state); +#else + var status = TryFastRead(data, ref state); +#endif + if (status != UseReader) return status; + } + + return TryReadViaReader(ref state, data); + + static OperationStatus TryReadViaReader(ref RespScanState state, ReadOnlySpan data) + { + var reader = new RespReader(data); + var complete = state.TryRead(ref reader, out var consumed); + if (complete) + { + return OperationStatus.Done; + } + return OperationStatus.NeedMoreData; + } + } + + /// + /// Validate that the supplied message is a valid RESP request, specifically: that it contains a single + /// top-level array payload with bulk-string elements, the first of which is non-empty (the command). + /// + public void ValidateRequest(in ReadOnlySequence message) + { + if (message.IsEmpty) Throw("Empty RESP frame"); + RespReader reader = new(in message); + reader.MoveNext(RespPrefix.Array); + reader.DemandNotNull(); + if (reader.IsStreaming) Throw("Streaming is not supported in this context"); + var count = reader.AggregateLength(); + for (int i = 0; i < count; i++) + { + reader.MoveNext(RespPrefix.BulkString); + reader.DemandNotNull(); + if (reader.IsStreaming) Throw("Streaming is not supported in this context"); + + if (i == 0 && reader.ScalarIsEmpty()) Throw("command must be non-empty"); + } + reader.DemandEnd(); + + static void Throw(string message) => throw new InvalidOperationException(message); + } +} diff --git a/src/RESP.Core/RespParsers.cs b/src/RESP.Core/RespParsers.cs new file mode 100644 index 000000000..958b3a3af --- /dev/null +++ b/src/RESP.Core/RespParsers.cs @@ -0,0 +1,177 @@ +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; + +namespace Resp; + +public readonly struct ResponseSummary(RespPrefix prefix, int length, long protocolBytes) : IEquatable +{ + public RespPrefix Prefix { get; } = prefix; + public int Length { get; } = length; + public long ProtocolBytes { get; } = protocolBytes; + + /// + public override string ToString() => $"{Prefix}, Length: {Length}, Protocol Bytes: {ProtocolBytes}"; + + /// + public bool Equals(ResponseSummary other) => EqualsCore(in other); + + private bool EqualsCore(in ResponseSummary other) => + Prefix == other.Prefix && Length == other.Length && ProtocolBytes == other.ProtocolBytes; + + bool IEquatable.Equals(ResponseSummary other) => EqualsCore(in other); + + /// + public override bool Equals(object? obj) => obj is ResponseSummary summary && EqualsCore(in summary); + + /// + public override int GetHashCode() => (int)Prefix ^ Length ^ ProtocolBytes.GetHashCode(); + + public static IRespParser Parser => ResponseSummaryParser.Default; + + private sealed class ResponseSummaryParser : IRespParser, IRespInlineParser, IRespMetadataParser + { + private ResponseSummaryParser() { } + public static readonly ResponseSummaryParser Default = new(); + + public ResponseSummary Parse(in Void state, ref RespReader reader) + { + var protocolBytes = reader.ProtocolBytesRemaining; + int length = 0; + if (reader.TryMoveNext()) + { + if (reader.IsScalar) length = reader.ScalarLength(); + else if (reader.IsAggregate) length = reader.AggregateLength(); + } + return new ResponseSummary(reader.Prefix, length, protocolBytes); + } + } +} + +public static class RespParsers +{ + public static IRespParser Success => InbuiltInlineParsers.Default; + public static IRespParser OK => OKParser.Default; + public static IRespParser String => InbuiltCopyOutParsers.Default; + public static IRespParser Int32 => InbuiltInlineParsers.Default; + public static IRespParser NullableInt32 => InbuiltInlineParsers.Default; + public static IRespParser Int64 => InbuiltInlineParsers.Default; + public static IRespParser NullableInt64 => InbuiltInlineParsers.Default; + public static IRespParser Single => InbuiltInlineParsers.Default; + public static IRespParser NullableSingle => InbuiltInlineParsers.Default; + public static IRespParser Double => InbuiltInlineParsers.Default; + public static IRespParser NullableDouble => InbuiltInlineParsers.Default; + public static IRespParser ByteArray => InbuiltCopyOutParsers.Default; + public static IRespParser ByteArrayArray => InbuiltCopyOutParsers.Default; + public static IRespParser, int> BufferWriter => InbuiltCopyOutParsers.Default; + + private sealed class Cache + { + public static IRespParser? Instance = + (InbuiltCopyOutParsers.Default as IRespParser) ?? // regular (may allocate, etc) + (InbuiltInlineParsers.Default as IRespParser) ?? // inline + (ResponseSummary.Parser as IRespParser); // inline+metadata + } + + public static IRespParser Get() + => Cache.Instance ??= GetCore(); + + public static void Set(IRespParser parser) + { + var obj = (InbuiltCopyOutParsers.Default as IRespParser) ?? + (InbuiltInlineParsers.Default as IRespParser); + if (obj is not null) ThrowInbuiltParser(typeof(TResponse)); + Cache.Instance = parser; + } + + private static IRespParser GetCore() + { + var obj = (InbuiltCopyOutParsers.Default as IRespParser) ?? + (InbuiltInlineParsers.Default as IRespParser); + if (obj is null) + { + ThrowNoParser(typeof(TResponse)); + } + + return Cache.Instance = obj; + } + + [DoesNotReturn] + private static void ThrowNoParser(Type type) => throw new InvalidOperationException( + message: + $"No default parser registered for type '{type.FullName}'; a custom parser must be specified via {nameof(RespParsers)}.{nameof(RespParsers.Set)}(...)."); + + [DoesNotReturn] + private static void ThrowInbuiltParser(Type type) => throw new InvalidOperationException( + message: $"Type '{type.FullName}' has inbuilt handling and cannot be changed."); + + private sealed class InbuiltInlineParsers : IRespParser, IRespInlineParser, + IRespParser, IRespParser, + IRespParser, IRespParser, + IRespParser, IRespParser, + IRespParser, IRespParser + { + private InbuiltInlineParsers() { } + public static readonly InbuiltInlineParsers Default = new(); + + public Void Parse(in Void state, ref RespReader reader) => Void.Instance; + + int IRespParser.Parse(in Void state, ref RespReader reader) => reader.ReadInt32(); + + int? IRespParser.Parse(in Void state, ref RespReader reader) => + reader.IsNull ? null : reader.ReadInt32(); + + long IRespParser.Parse(in Void state, ref RespReader reader) => reader.ReadInt64(); + + long? IRespParser.Parse(in Void state, ref RespReader reader) => + reader.IsNull ? null : reader.ReadInt64(); + + float IRespParser.Parse(in Void state, ref RespReader reader) => (float)reader.ReadDouble(); + + float? IRespParser.Parse(in Void state, ref RespReader reader) => + reader.IsNull ? null : (float)reader.ReadDouble(); + + double IRespParser.Parse(in Void state, ref RespReader reader) => reader.ReadDouble(); + + double? IRespParser.Parse(in Void state, ref RespReader reader) => + reader.IsNull ? null : reader.ReadDouble(); + } + + private sealed class OKParser : IRespParser, IRespInlineParser + { + private OKParser() { } + public static readonly OKParser Default = new(); + + public Void Parse(in Void state, ref RespReader reader) + { + if (!(reader.Prefix == RespPrefix.SimpleString && reader.IsOK())) + { + Throw(); + } + + return default; + static void Throw() => throw new InvalidOperationException("Expected +OK response"); + } + } + + private sealed class InbuiltCopyOutParsers : IRespParser, + IRespParser, IRespParser, + IRespParser, int> + { + private InbuiltCopyOutParsers() { } + public static readonly InbuiltCopyOutParsers Default = new(); + + string? IRespParser.Parse(in Void state, ref RespReader reader) => reader.ReadString(); + byte[]? IRespParser.Parse(in Void state, ref RespReader reader) => reader.ReadByteArray(); + + byte[]?[]? IRespParser.Parse(in Void state, ref RespReader reader) => + reader.ReadArray(static (ref RespReader reader) => reader.ReadByteArray()); + + int IRespParser, int>.Parse(in IBufferWriter state, ref RespReader reader) + { + reader.DemandScalar(); + if (reader.IsNull) return -1; + return reader.CopyTo(state); + } + } +} diff --git a/src/RESP.Core/RespPayload.cs b/src/RESP.Core/RespPayload.cs new file mode 100644 index 000000000..62449c97c --- /dev/null +++ b/src/RESP.Core/RespPayload.cs @@ -0,0 +1,671 @@ +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Sources; + +namespace Resp; + +public interface IRespMessage +{ + /// + /// Gets the request payload, reserving the value. This must be released using . + /// + bool TryReserveRequest(out ReadOnlyMemory payload); + + bool IsCompleted { get; } + + /// + /// Releases the request payload. + /// + void ReleaseRequest(); + + bool TrySetCanceled(CancellationToken cancellationToken = default); + bool TrySetException(Exception exception); + + /// + /// Parse the response and complete the request. + /// + void ProcessResponse(ref RespReader reader); + + /// + /// Cancellation associated with this message. Note that this should not typically be used to + /// cancel IO operations (for example sockets), as that would break the entire stream - however, it + /// can be used to interrupt intermediate processing before it is submitted. + /// + CancellationToken CancellationToken { get; } +} + +internal static class ActivationHelper +{ + private sealed class WorkItem +#if NETCOREAPP3_0_OR_GREATER + : IThreadPoolWorkItem +#endif + { + private WorkItem() + { +#if NET5_0_OR_GREATER + Unsafe.SkipInit(out _payload); +#else + _payload = []; +#endif + } + + private void Init(byte[] payload, int length, IRespMessage message) + { + _payload = payload; + _length = length; + _message = message; + } + + private byte[] _payload; + private int _length; + private IRespMessage? _message; + + private static WorkItem? _spare; // do NOT use ThreadStatic - different producer/consumer, no overlap + + public static void UnsafeQueueUserWorkItem( + IRespMessage message, + ReadOnlySpan payload, + ref byte[]? lease) + { + if (lease is null) + { + // we need to create our own copy of the data + lease = ArrayPool.Shared.Rent(payload.Length); + payload.CopyTo(lease); + } + + var obj = Interlocked.Exchange(ref _spare, null) ?? new(); + obj.Init(lease, payload.Length, message); + lease = null; // count as claimed + + DebugCounters.OnCopyOut(payload.Length); +#if NETCOREAPP3_0_OR_GREATER + ThreadPool.UnsafeQueueUserWorkItem(obj, false); +#else + ThreadPool.UnsafeQueueUserWorkItem(WaitCallback, obj); +#endif + } +#if !NETCOREAPP3_0_OR_GREATER + private static readonly WaitCallback WaitCallback = state => ((WorkItem)state!).Execute(); +#endif + + public static void Execute(IRespMessage? message, ReadOnlySpan payload) + { + if (message is { IsCompleted: false }) + { + try + { + var reader = new RespReader(payload); + message.ProcessResponse(ref reader); + } + catch (Exception ex) + { + message.TrySetException(ex); + } + } + } + + public void Execute() + { + var message = _message; + var payload = _payload; + var length = _length; + _message = null; + _payload = []; + _length = 0; + Interlocked.Exchange(ref _spare, this); + Execute(message, new(payload, 0, length)); + ArrayPool.Shared.Return(payload); + } + } + + public static void ProcessResponse(IRespMessage? pending, ReadOnlySpan payload, ref byte[]? lease) + { + if (pending is null) + { + // nothing to do + } + else if (pending is IRespInternalMessage { AllowInlineParsing: true }) + { + WorkItem.Execute(pending, payload); + } + else + { + WorkItem.UnsafeQueueUserWorkItem(pending, payload, ref lease); + } + } + + private static readonly Action CancellationCallback = static state + => ((IRespMessage)state!).TrySetCanceled(); + + public static CancellationTokenRegistration RegisterForCancellation( + IRespMessage message, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return cancellationToken.Register(CancellationCallback, message); + } + + [Conditional("DEBUG")] + public static void DebugBreak() + { +#if DEBUG + if (Debugger.IsAttached) Debugger.Break(); +#endif + } + + [Conditional("DEBUG")] + public static void DebugBreakIf(bool condition) + { +#if DEBUG + if (condition && Debugger.IsAttached) Debugger.Break(); +#endif + } +} + +internal abstract class InternalRespMessageBase : IRespInternalMessage +{ + private IRespParser? _parser; + private byte[] _requestPayload = []; + private int _requestLength, _requestRefCount = 1; + private TState _state = default!; + private CancellationToken _cancellationToken; + private CancellationTokenRegistration _cancellationTokenRegistration; + + public abstract bool IsCompleted { get; } + + public bool TryReserveRequest(out ReadOnlyMemory payload) + { + payload = default; + while (true) // need to take reservation + { + if (IsCompleted) return false; + var oldCount = Volatile.Read(ref _requestRefCount); + if (oldCount == 0) return false; + if (Interlocked.CompareExchange(ref _requestRefCount, checked(oldCount + 1), oldCount) == oldCount) break; + } + + payload = new(_requestPayload, 0, _requestLength); + return true; + } + + public void ReleaseRequest() + { + if (!TryReleaseRequest()) ThrowReleased(); + + static void ThrowReleased() => + throw new InvalidOperationException("The request payload has already been released"); + } + + private bool TryReleaseRequest() // bool here means "it wasn't already zero"; it doesn't mean "it became zero" + { + while (true) + { + var oldCount = Volatile.Read(ref _requestRefCount); + if (oldCount == 0) return false; + if (Interlocked.CompareExchange(ref _requestRefCount, oldCount - 1, oldCount) == oldCount) + { + if (oldCount == 1) // we were the last one; recycle + { + _parser = null; + var arr = _requestPayload; + _requestLength = 0; + _requestPayload = []; + ArrayPool.Shared.Return(arr); + } + + return true; + } + } + } + + protected abstract bool TrySetResult(TResponse value); + public abstract bool TrySetException(Exception exception); + public abstract bool TrySetCanceled(CancellationToken cancellationToken = default); + + // ReSharper disable once SuspiciousTypeConversion.Global + public bool AllowInlineParsing => _parser is null or IRespInlineParser; + + void IRespMessage.ProcessResponse(ref RespReader reader) + { + // ReSharper disable once SuspiciousTypeConversion.Global + if (_parser is { } parser) + { + if (parser is not IRespMetadataParser) + { + reader.MoveNext(); // skip attributes and process errors + } + + var result = parser.Parse(in _state, ref reader); + TryReleaseRequest(); + TrySetResult(result); + } + } + + public CancellationToken CancellationToken => _cancellationToken; + + protected void UnregisterCancellation() + { + var reg = _cancellationTokenRegistration; + _cancellationTokenRegistration = default; + reg.Dispose(); + } + + protected void Reset() + { + _parser = null; + _state = default!; + _requestLength = 0; + _requestPayload = []; + _requestRefCount = 0; + _cancellationToken = CancellationToken.None; + _cancellationTokenRegistration = default; + } + + protected void Init( + byte[] requestPayload, + int requestLength, + IRespParser? parser, + in TState state, + CancellationToken cancellationToken) + { + if (cancellationToken.CanBeCanceled) + { + cancellationToken.ThrowIfCancellationRequested(); + _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellationToken); + } + + _parser = parser; + _state = state; + _requestPayload = requestPayload; + _requestLength = requestLength; + _requestRefCount = 1; + _cancellationToken = cancellationToken; + } +} + +internal static class SyncRespMessageStatus // think "enum", but need Volatile.Read friendliness +{ + internal const int + Pending = 0, + Completed = 1, + Faulted = 2, + Cancelled = 3, + Timeout = 4; +} + +internal sealed class SyncInternalRespMessage : InternalRespMessageBase +{ + private SyncInternalRespMessage() { } + + private int _status; + private TResponse _result = default!; + private Exception? _exception; + + protected override bool TrySetResult(TResponse value) + { + if (Volatile.Read(ref _status) == SyncRespMessageStatus.Pending) + { + lock (this) + { + if (_status == SyncRespMessageStatus.Pending) + { + _result = value; + _status = SyncRespMessageStatus.Completed; + Monitor.PulseAll(this); + return true; + } + } + } + + return false; + } + + public override bool TrySetException(Exception exception) + { + if (Volatile.Read(ref _status) == SyncRespMessageStatus.Pending) + { + var newStatus = exception switch + { + TimeoutException => SyncRespMessageStatus.Timeout, + OperationCanceledException => SyncRespMessageStatus.Cancelled, + _ => SyncRespMessageStatus.Faulted, + }; + lock (this) + { + if (_status == SyncRespMessageStatus.Pending) + { + _exception = exception; + _status = newStatus; + Monitor.PulseAll(this); + return true; + } + } + } + + return false; + } + + public override bool TrySetCanceled(CancellationToken cancellationToken = default) + { + if (Volatile.Read(ref _status) == SyncRespMessageStatus.Pending) + { + if (!cancellationToken.IsCancellationRequested) + { + // if the inbound token was not cancelled: use our own + cancellationToken = CancellationToken; + } + + lock (this) + { + if (_status == SyncRespMessageStatus.Pending) + { + _status = SyncRespMessageStatus.Cancelled; + _exception = new OperationCanceledException(cancellationToken); + Monitor.PulseAll(this); + return true; + } + } + } + + return false; + } + + public override bool IsCompleted => Volatile.Read(ref _status) != SyncRespMessageStatus.Pending; + + public TResponse WaitAndRecycle(TimeSpan timeout) + { + int status = Volatile.Read(ref _status); + if (status == SyncRespMessageStatus.Pending) + { + lock (this) + { + status = _status; + if (status == SyncRespMessageStatus.Pending) + { + if (timeout == TimeSpan.Zero) + { + Monitor.Wait(this); + status = _status; + } + else if (!Monitor.Wait(this, timeout)) + { + status = _status = SyncRespMessageStatus.Timeout; + } + else + { + status = _status; + } + } + } + } + + switch (status) + { + case SyncRespMessageStatus.Completed: + var result = _result; // snapshot + if (_spare is null && TryReset()) + { + _spare = this; + } + + return result; + case SyncRespMessageStatus.Faulted: + throw _exception ?? new InvalidOperationException("Operation failed"); + case SyncRespMessageStatus.Cancelled: + throw _exception ?? new OperationCanceledException(CancellationToken); + case SyncRespMessageStatus.Timeout: + throw _exception ?? new TimeoutException(); + default: + throw new InvalidOperationException($"Unexpected status: {status}"); + } + } + + private bool TryReset() + { + Reset(); + _exception = null; + _result = default!; + _status = SyncRespMessageStatus.Pending; + return true; + } + + [ThreadStatic] + // this comment just to stop a weird formatter glitch + private static SyncInternalRespMessage? _spare; + + public static SyncInternalRespMessage Create( + byte[] requestPayload, + int requestLength, + IRespParser? parser, + in TState state, + CancellationToken cancellationToken) + { + var obj = _spare ?? new(); + _spare = null; + obj.Init(requestPayload, requestLength, parser, in state, cancellationToken); + + return obj; + } +} + +#if NET9_0_OR_GREATER && NEVER +internal sealed class AsyncInternalRespMessage( + byte[] requestPayload, + int requestLength, + IRespParser? parser) + : InternalRespMessageBase(requestPayload, requestLength, parser) +{ + [UnsafeAccessor(UnsafeAccessorKind.Constructor)] + private static extern Task CreateTask(object? state, TaskCreationOptions options); + + [UnsafeAccessor(UnsafeAccessorKind.Method)] + private static extern bool TrySetException(Task obj, Exception exception); + + [UnsafeAccessor(UnsafeAccessorKind.Method)] + private static extern bool TrySetResult(Task obj, TResponse value); + + [UnsafeAccessor(UnsafeAccessorKind.Method)] + private static extern bool TrySetCanceled(Task obj, CancellationToken cancellationToken); + + // ReSharper disable once SuspiciousTypeConversion.Global + private readonly Task _task = CreateTask( + null, + // if we're using IO-thread parsing, we *must* still dispatch downstream continuations to the thread-pool to + // prevent thread-theft; otherwise, we're fine to run downstream inline (we already jumped) + parser is IRespInlineParser ? TaskCreationOptions.RunContinuationsAsynchronously : TaskCreationOptions.None); + + private CancellationTokenRegistration _cancellationTokenRegistration; + + public override bool IsCompleted => _task.IsCompleted; + protected override bool TrySetResult(TResponse value) + { + UnregisterCancellation(); + return TrySetResult(_task, value); + } + + public override bool TrySetException(Exception exception) + { + UnregisterCancellation(); + return TrySetException(_task, exception); + } + + public override bool TrySetCanceled(CancellationToken cancellationToken) + { + UnregisterCancellation(); + return TrySetCanceled(_task, cancellationToken); + } + + private void UnregisterCancellation() + { + _cancellationTokenRegistration.Dispose(); + _cancellationTokenRegistration = default; + } + + public Task WaitAsync(CancellationToken cancellationToken = default) + { + if (cancellationToken.CanBeCanceled) + { + _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellationToken); + } + + return _task; + } +} +#else +internal sealed class AsyncInternalRespMessage : InternalRespMessageBase, + IValueTaskSource, IValueTaskSource +{ + [ThreadStatic] + // this comment just to stop a weird formatter glitch + private static AsyncInternalRespMessage? _spare; + + // we need synchronization over multiple attempts (completion, cancellation, abort) trying + // to signal the MRTCS + private int _completedFlag; + + private bool SetCompleted(bool withSuccess = false) + { + if (Interlocked.CompareExchange(ref _completedFlag, 1, 0) == 0) + { + // stop listening for CT notifications + UnregisterCancellation(); + + // configure threading model; failure can be triggered from any thread - *always* + // dispatch to pool; in the success case, we're either on the IO thread + // (if inline-parsing is enabled) - in which case, yes: dispatch - or we've + // already jumped to a pool thread for the parse step. So: the only + // time we want to complete inline is success and not inline-parsing. + _asyncCore.RunContinuationsAsynchronously = !withSuccess || AllowInlineParsing; + + return true; + } + + return false; + } + + public static AsyncInternalRespMessage Create( + byte[] requestPayload, + int requestLength, + IRespParser? parser, + in TState state, + CancellationToken cancellationToken) + { + var obj = _spare ?? new(); + _spare = null; + obj._asyncCore.RunContinuationsAsynchronously = true; + obj.Init(requestPayload, requestLength, parser, in state, cancellationToken); + return obj; + } + + private ManualResetValueTaskSourceCore _asyncCore; + + public override bool IsCompleted => Volatile.Read(ref _completedFlag) == 1; + + protected override bool TrySetResult(TResponse value) + { + if (SetCompleted(withSuccess: true)) + { + _asyncCore.SetResult(value); + return true; + } + + return false; + } + + public override bool TrySetException(Exception exception) + { + if (SetCompleted()) + { + _asyncCore.SetException(exception); + return true; + } + + return false; + } + + public override bool TrySetCanceled(CancellationToken cancellationToken = default) + { + if (SetCompleted()) + { + if (!cancellationToken.IsCancellationRequested) + { + // if the inbound token was not cancelled: use our own + cancellationToken = CancellationToken; + } + + _asyncCore.SetException(new OperationCanceledException(cancellationToken)); + return true; + } + + return false; + } + + public ValueTask WaitTypedAsync() => new(this, _asyncCore.Version); + + internal ValueTask WaitTypedAsync(Task send) + { + if (!send.IsCompleted) return Awaited(send, this); + send.GetAwaiter().GetResult(); + return new(this, _asyncCore.Version); + + static async ValueTask Awaited(Task task, AsyncInternalRespMessage @this) + { + await task.ConfigureAwait(false); + return await @this.WaitTypedAsync().ConfigureAwait(false); + } + } + + public ValueTask WaitUntypedAsync() => new(this, _asyncCore.Version); + + internal ValueTask WaitUntypedAsync(Task send) + { + if (!send.IsCompleted) return Awaited(send, this); + send.GetAwaiter().GetResult(); + return new(this, _asyncCore.Version); + + static async ValueTask Awaited(Task task, AsyncInternalRespMessage @this) + { + await task.ConfigureAwait(false); + await @this.WaitUntypedAsync().ConfigureAwait(false); + } + } + + public ValueTaskSourceStatus GetStatus(short token) => _asyncCore.GetStatus(token); + + public void OnCompleted( + Action continuation, + object? state, + short token, + ValueTaskSourceOnCompletedFlags flags) + => _asyncCore.OnCompleted(continuation, state, token, flags); + + public TResponse GetResult(short token) + { + Debug.Assert(IsCompleted, "Async payload should already be completed"); + var result = _asyncCore.GetResult(token); + // recycle on success (only) + if (_spare is null && TryReset()) + { + _spare = this; + } + + return result; + } + + private bool TryReset() + { + Reset(); + _asyncCore.Reset(); // incr version, etc + _completedFlag = 0; + return true; + } + + void IValueTaskSource.GetResult(short token) => _ = GetResult(token); +} +#endif diff --git a/src/RESP.Core/RespPrefix.cs b/src/RESP.Core/RespPrefix.cs new file mode 100644 index 000000000..382be7925 --- /dev/null +++ b/src/RESP.Core/RespPrefix.cs @@ -0,0 +1,97 @@ +namespace Resp; + +/// +/// RESP protocol prefix. +/// +public enum RespPrefix : byte +{ + /// + /// Invalid. + /// + None = 0, + + /// + /// Simple strings: +OK\r\n. + /// + SimpleString = (byte)'+', + + /// + /// Simple errors: -ERR message\r\n. + /// + SimpleError = (byte)'-', + + /// + /// Integers: :123\r\n. + /// + Integer = (byte)':', + + /// + /// String with support for binary data: $7\r\nmessage\r\n. + /// + BulkString = (byte)'$', + + /// + /// Multiple inner messages: *1\r\n+message\r\n. + /// + Array = (byte)'*', + + /// + /// Null strings/arrays: _\r\n. + /// + Null = (byte)'_', + + /// + /// Boolean values: #T\r\n. + /// + Boolean = (byte)'#', + + /// + /// Floating-point number: ,123.45\r\n. + /// + Double = (byte)',', + + /// + /// Large integer number: (12...89\r\n. + /// + BigInteger = (byte)'(', + + /// + /// Error with support for binary data: !7\r\nmessage\r\n. + /// + BulkError = (byte)'!', + + /// + /// String that should be interpreted verbatim: =11\r\ntxt:message\r\n. + /// + VerbatimString = (byte)'=', + + /// + /// Multiple sub-items that represent a map. + /// + Map = (byte)'%', + + /// + /// Multiple sub-items that represent a set. + /// + Set = (byte)'~', + + /// + /// Out-of band messages. + /// + Push = (byte)'>', + + /// + /// Continuation of streaming scalar values. + /// + StreamContinuation = (byte)';', + + /// + /// End sentinel for streaming aggregate values. + /// + StreamTerminator = (byte)'.', + + /// + /// Metadata about the next element. + /// + Attribute = (byte)'|', +} diff --git a/src/RESP.Core/RespReader.AggregateEnumerator.cs b/src/RESP.Core/RespReader.AggregateEnumerator.cs new file mode 100644 index 000000000..7e57910e5 --- /dev/null +++ b/src/RESP.Core/RespReader.AggregateEnumerator.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace Resp; + +public ref partial struct RespReader +{ + /// + /// Reads the sub-elements associated with an aggregate value. + /// + public readonly AggregateEnumerator AggregateChildren() => new(in this); + + /// + /// Reads the sub-elements associated with an aggregate value. + /// + public ref struct AggregateEnumerator + { + // Note that _reader is the overall reader that can see outside this aggregate, as opposed + // to Current which is the sub-tree of the current element *only* + private RespReader _reader; + private int _remaining; + + /// + /// Create a new enumerator for the specified . + /// + /// The reader containing the data for this operation. + public AggregateEnumerator(scoped in RespReader reader) + { + reader.DemandAggregate(); + _remaining = reader.IsStreaming ? -1 : reader._length; + _reader = reader; + Value = default; + } + + /// + public readonly AggregateEnumerator GetEnumerator() => this; + + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public RespReader Current => Value; + + /// + /// Gets the current element associated with this reader. + /// + public RespReader Value; // intentionally a field, because of ref-semantics + + /// + /// Move to the next child if possible, and move the child element into the next node. + /// + public bool MoveNext(RespPrefix prefix) + { + bool result = MoveNext(); + if (result) + { + Value.MoveNext(prefix); + } + return result; + } + + /// + /// Move to the next child if possible, and move the child element into the next node. + /// + /// The type of data represented by this reader. + public bool MoveNext(RespPrefix prefix, RespAttributeReader respAttributeReader, ref T attributes) + { + bool result = MoveNext(respAttributeReader, ref attributes); + if (result) + { + Value.MoveNext(prefix); + } + return result; + } + + /// > + public bool MoveNext() + { + object? attributes = null; + return MoveNextCore(null, ref attributes); + } + + /// > + /// The type of data represented by this reader. + public bool MoveNext(RespAttributeReader respAttributeReader, ref T attributes) + => MoveNextCore(respAttributeReader, ref attributes); + + /// > + private bool MoveNextCore(RespAttributeReader? attributeReader, ref T attributes) + { + if (_remaining == 0) + { + Value = default; + return false; + } + + // in order to provide access to attributes etc, we want Current to be positioned + // *before* the next element; for that, we'll take a snapshot before we read + _reader.MovePastCurrent(); + var snapshot = _reader.Clone(); + + if (attributeReader is null) + { + _reader.MoveNext(); + } + else + { + _reader.MoveNext(attributeReader, ref attributes); + } + if (_remaining > 0) + { + // non-streaming, decrement + _remaining--; + } + else if (_reader.Prefix == RespPrefix.StreamTerminator) + { + // end of streaming aggregate + _remaining = 0; + Value = default; + return false; + } + + // move past that sub-tree and trim the "snapshot" state, giving + // us a scoped reader that is *just* that sub-tree + _reader.SkipChildren(); + snapshot.TrimToTotal(_reader.BytesConsumed); + + Value = snapshot; + return true; + } + + /// + /// Move to the end of this aggregate and export the state of the . + /// + /// The reader positioned at the end of the data; this is commonly + /// used to update a tree reader, to get to the next data after the aggregate. + public void MovePast(out RespReader reader) + { + while (MoveNext()) { } + reader = _reader; + } + + public void DemandNext() + { + if (!MoveNext()) ThrowEOF(); + Value.MoveNext(); // skip any attributes etc + } + + public T ReadOne(Projection projection) + { + DemandNext(); + return projection(ref Value); + } + + public void FillAll(scoped Span target, Projection projection) + { + for (int i = 0; i < target.Length; i++) + { + if (!MoveNext()) ThrowEOF(); + + Value.MoveNext(); // skip any attributes etc + target[i] = projection(ref Value); + } + } + } + + internal void TrimToTotal(long length) => TrimToRemaining(length - BytesConsumed); + + internal void TrimToRemaining(long bytes) + { + if (_prefix != RespPrefix.None || bytes < 0) Throw(); + + var current = CurrentAvailable; + if (bytes <= current) + { + UnsafeTrimCurrentBy(current - (int)bytes); + _remainingTailLength = 0; + return; + } + + bytes -= current; + if (bytes <= _remainingTailLength) + { + _remainingTailLength = bytes; + return; + } + + Throw(); + static void Throw() => throw new ArgumentOutOfRangeException(nameof(bytes)); + } +} diff --git a/src/RESP.Core/RespReader.Debug.cs b/src/RESP.Core/RespReader.Debug.cs new file mode 100644 index 000000000..9e911911c --- /dev/null +++ b/src/RESP.Core/RespReader.Debug.cs @@ -0,0 +1,33 @@ +using System.Diagnostics; + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace Resp; + +[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] +public ref partial struct RespReader +{ + internal bool DebugEquals(in RespReader other) + => _prefix == other._prefix + && _length == other._length + && _flags == other._flags + && _bufferIndex == other._bufferIndex + && _positionBase == other._positionBase + && _remainingTailLength == other._remainingTailLength; + + internal new string ToString() => $"{Prefix} ({_flags}); length {_length}, {TotalAvailable} remaining"; + + internal void DebugReset() + { + _bufferIndex = 0; + _length = 0; + _flags = 0; + _prefix = RespPrefix.None; + } + +#if DEBUG + internal bool VectorizeDisabled { get; set; } +#endif +} diff --git a/src/RESP.Core/RespReader.ScalarEnumerator.cs b/src/RESP.Core/RespReader.ScalarEnumerator.cs new file mode 100644 index 000000000..9169ad709 --- /dev/null +++ b/src/RESP.Core/RespReader.ScalarEnumerator.cs @@ -0,0 +1,107 @@ +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace Resp; + +public ref partial struct RespReader +{ + /// + /// Gets the chunks associated with a scalar value. + /// + public readonly ScalarEnumerator ScalarChunks() => new(in this); + + /// + /// Allows enumeration of chunks in a scalar value; this includes simple values + /// that span multiple segments, and streaming + /// scalar RESP values. + /// + public ref struct ScalarEnumerator + { + /// + public readonly ScalarEnumerator GetEnumerator() => this; + + private RespReader _reader; + + private ReadOnlySpan _current; + private ReadOnlySequenceSegment? _tail; + private int _offset, _remaining; + + /// + /// Create a new enumerator for the specified . + /// + /// The reader containing the data for this operation. + public ScalarEnumerator(scoped in RespReader reader) + { + reader.DemandScalar(); + _reader = reader; + InitSegment(); + } + + private void InitSegment() + { + _current = _reader.CurrentSpan(); + _tail = _reader._tail; + _offset = CurrentLength = 0; + _remaining = _reader._length; + if (_reader.TotalAvailable < _remaining) ThrowEOF(); + } + + /// + public bool MoveNext() + { + while (true) // for each streaming element + { + _offset += CurrentLength; + while (_remaining > 0) // for each span in the current element + { + // look in the active span + var take = Math.Min(_remaining, _current.Length - _offset); + if (take > 0) // more in the current chunk + { + _remaining -= take; + CurrentLength = take; + return true; + } + + // otherwise, we expect more tail data + if (_tail is null) ThrowEOF(); + + _current = _tail.Memory.Span; + _offset = 0; + _tail = _tail.Next; + } + + if (!_reader.MoveNextStreamingScalar()) break; + InitSegment(); + } + + CurrentLength = 0; + return false; + } + + /// + public readonly ReadOnlySpan Current => _current.Slice(_offset, CurrentLength); + + /// + /// Gets the or . + /// + public int CurrentLength { readonly get; private set; } + + /// + /// Move to the end of this aggregate and export the state of the . + /// + /// The reader positioned at the end of the data; this is commonly + /// used to update a tree reader, to get to the next data after the aggregate. + public void MovePast(out RespReader reader) + { + while (MoveNext()) { } + reader = _reader; + } + } +} diff --git a/src/RESP.Core/RespReader.Span.cs b/src/RESP.Core/RespReader.Span.cs new file mode 100644 index 000000000..796ecc397 --- /dev/null +++ b/src/RESP.Core/RespReader.Span.cs @@ -0,0 +1,85 @@ +#define USE_UNSAFE_SPAN + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace Resp; + +/* + How we actually implement the underlying buffer depends on the capabilities of the runtime. + */ + +#if NET7_0_OR_GREATER && USE_UNSAFE_SPAN + +public ref partial struct RespReader +{ + // intent: avoid lots of slicing by dealing with everything manually, and accepting the "don't get it wrong" rule + private ref byte _bufferRoot; + private int _bufferLength; + + private partial void UnsafeTrimCurrentBy(int count) + { + Debug.Assert(count >= 0 && count <= _bufferLength, "Unsafe trim length"); + _bufferLength -= count; + } + + private readonly partial ref byte UnsafeCurrent + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.Add(ref _bufferRoot, _bufferIndex); + } + + private readonly partial int CurrentLength + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _bufferLength; + } + + private readonly partial ReadOnlySpan CurrentSpan() => MemoryMarshal.CreateReadOnlySpan( + ref UnsafeCurrent, CurrentAvailable); + + private readonly partial ReadOnlySpan UnsafePastPrefix() => MemoryMarshal.CreateReadOnlySpan( + ref Unsafe.Add(ref _bufferRoot, _bufferIndex + 1), + _bufferLength - (_bufferIndex + 1)); + + private partial void SetCurrent(ReadOnlySpan value) + { + _bufferRoot = ref MemoryMarshal.GetReference(value); + _bufferLength = value.Length; + } +} +#else +public ref partial struct RespReader // much more conservative - uses slices etc +{ + private ReadOnlySpan _buffer; + + private partial void UnsafeTrimCurrentBy(int count) + { + _buffer = _buffer.Slice(0, _buffer.Length - count); + } + + private readonly partial ref byte UnsafeCurrent + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in _buffer[_bufferIndex]); // hack around CS8333 + } + + private readonly partial int CurrentLength + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _buffer.Length; + } + + private readonly partial ReadOnlySpan UnsafePastPrefix() => _buffer.Slice(_bufferIndex + 1); + + private readonly partial ReadOnlySpan CurrentSpan() => _buffer.Slice(_bufferIndex); + + private partial void SetCurrent(ReadOnlySpan value) => _buffer = value; +} +#endif diff --git a/src/RESP.Core/RespReader.Utils.cs b/src/RESP.Core/RespReader.Utils.cs new file mode 100644 index 000000000..a5302fb13 --- /dev/null +++ b/src/RESP.Core/RespReader.Utils.cs @@ -0,0 +1,318 @@ +using System; +using System.Buffers.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.CompilerServices; + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace Resp; + +public ref partial struct RespReader +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UnsafeAssertClLf(int offset) => UnsafeAssertClLf(ref UnsafeCurrent, offset); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void UnsafeAssertClLf(scoped ref byte source, int offset) + { + if (Unsafe.ReadUnaligned(ref Unsafe.Add(ref source, offset)) != RespConstants.CrLfUInt16) + { + ThrowProtocolFailure("Expected CR/LF"); + } + } + + private enum LengthPrefixResult + { + NeedMoreData, + Length, + Null, + Streaming, + } + + /// + /// Asserts that the current element is a scalar type. + /// + public readonly void DemandScalar() + { + if (!IsScalar) Throw(Prefix); + static void Throw(RespPrefix prefix) => throw new InvalidOperationException($"This operation requires a scalar element, got {prefix}"); + } + + /// + /// Asserts that the current element is a scalar type. + /// + public readonly void DemandAggregate() + { + if (!IsAggregate) Throw(Prefix); + static void Throw(RespPrefix prefix) => throw new InvalidOperationException($"This operation requires an aggregate element, got {prefix}"); + } + + private static LengthPrefixResult TryReadLengthPrefix(ReadOnlySpan bytes, out int value, out int byteCount) + { + var end = bytes.IndexOf(RespConstants.CrlfBytes); + if (end < 0) + { + byteCount = value = 0; + if (bytes.Length >= RespConstants.MaxRawBytesInt32 + 2) + { + ThrowProtocolFailure("Unterminated or over-length integer"); // should have failed; report failure to prevent infinite loop + } + return LengthPrefixResult.NeedMoreData; + } + byteCount = end + 2; + switch (end) + { + case 0: + ThrowProtocolFailure("Length prefix expected"); + goto case default; // not reached, just satisfying definite assignment + case 1 when bytes[0] == (byte)'?': + value = 0; + return LengthPrefixResult.Streaming; + default: + if (end > RespConstants.MaxRawBytesInt32 || !(Utf8Parser.TryParse(bytes, out value, out var consumed) && consumed == end)) + { + ThrowProtocolFailure("Unable to parse integer"); + value = 0; + } + if (value < 0) + { + if (value == -1) + { + value = 0; + return LengthPrefixResult.Null; + } + ThrowProtocolFailure("Invalid negative length prefix"); + } + return LengthPrefixResult.Length; + } + } + + private readonly RespReader Clone() => this; // useful for performing streaming operations without moving the primary + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + private static void ThrowProtocolFailure(string message) + => throw new InvalidOperationException("RESP protocol failure: " + message); // protocol exception? + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + internal static void ThrowEOF() => throw new EndOfStreamException(); + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + private static void ThrowFormatException() => throw new FormatException(); + + private int RawTryReadByte() + { + if (_bufferIndex < CurrentLength || TryMoveToNextSegment()) + { + var result = UnsafeCurrent; + _bufferIndex++; + return result; + } + return -1; + } + + private int RawPeekByte() + { + return (CurrentLength < _bufferIndex || TryMoveToNextSegment()) ? UnsafeCurrent : -1; + } + + private bool RawAssertCrLf() + { + if (CurrentAvailable >= 2) + { + UnsafeAssertClLf(0); + _bufferIndex += 2; + return true; + } + else + { + int next = RawTryReadByte(); + if (next < 0) return false; + if (next == '\r') + { + next = RawTryReadByte(); + if (next < 0) return false; + if (next == '\n') return true; + } + ThrowProtocolFailure("Expected CR/LF"); + return false; + } + } + + private LengthPrefixResult RawTryReadLengthPrefix() + { + _length = 0; + if (!RawTryFindCrLf(out int end)) + { + if (TotalAvailable >= RespConstants.MaxRawBytesInt32 + 2) + { + ThrowProtocolFailure("Unterminated or over-length integer"); // should have failed; report failure to prevent infinite loop + } + return LengthPrefixResult.NeedMoreData; + } + + switch (end) + { + case 0: + ThrowProtocolFailure("Length prefix expected"); + goto case default; // not reached, just satisfying definite assignment + case 1: + var b = (byte)RawTryReadByte(); + RawAssertCrLf(); + if (b == '?') + { + return LengthPrefixResult.Streaming; + } + else + { + _length = ParseSingleDigit(b); + return LengthPrefixResult.Length; + } + default: + if (end > RespConstants.MaxRawBytesInt32) + { + ThrowProtocolFailure("Unable to parse integer"); + } + Span bytes = stackalloc byte[end]; + RawFillBytes(bytes); + RawAssertCrLf(); + if (!(Utf8Parser.TryParse(bytes, out _length, out var consumed) && consumed == end)) + { + ThrowProtocolFailure("Unable to parse integer"); + } + + if (_length < 0) + { + if (_length == -1) + { + _length = 0; + return LengthPrefixResult.Null; + } + ThrowProtocolFailure("Invalid negative length prefix"); + } + + return LengthPrefixResult.Length; + } + } + + private void RawFillBytes(scoped Span target) + { + do + { + var current = CurrentSpan(); + if (current.Length >= target.Length) + { + // more than enough, need to trim + current.Slice(0, target.Length).CopyTo(target); + _bufferIndex += target.Length; + return; // we're done + } + else + { + // take what we can + current.CopyTo(target); + target = target.Slice(current.Length); + // we could move _bufferIndex here, but we're about to trash that in TryMoveToNextSegment + } + } + while (TryMoveToNextSegment()); + ThrowEOF(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ParseSingleDigit(byte value) + { + return value switch + { + (byte)'0' or (byte)'1' or (byte)'2' or (byte)'3' or (byte)'4' or (byte)'5' or (byte)'6' or (byte)'7' or (byte)'8' or (byte)'9' => value - (byte)'0', + _ => Invalid(value), + }; + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + static int Invalid(byte value) => throw new FormatException($"Unable to parse integer: '{(char)value}'"); + } + + private readonly bool RawTryAssertInlineScalarPayloadCrLf() + { + Debug.Assert(IsInlineScalar, "should be inline scalar"); + + var reader = Clone(); + var len = reader._length; + if (len == 0) return reader.RawAssertCrLf(); + + do + { + var current = reader.CurrentSpan(); + if (current.Length >= len) + { + reader._bufferIndex += len; + return reader.RawAssertCrLf(); // we're done + } + else + { + // take what we can + len -= current.Length; + // we could move _bufferIndex here, but we're about to trash that in TryMoveToNextSegment + } + } + while (reader.TryMoveToNextSegment()); + return false; // EOF + } + + private readonly bool RawTryFindCrLf(out int length) + { + length = 0; + RespReader reader = Clone(); + do + { + var span = reader.CurrentSpan(); + var index = span.IndexOf((byte)'\r'); + if (index >= 0) + { + checked + { + length += index; + } + // move past the CR and assert the LF + reader._bufferIndex += index + 1; + var next = reader.RawTryReadByte(); + if (next < 0) break; // we don't know + if (next != '\n') ThrowProtocolFailure("CR/LF expected"); + + return true; + } + checked + { + length += span.Length; + } + } + while (reader.TryMoveToNextSegment()); + length = 0; + return false; + } + + private string GetDebuggerDisplay() + { + return ToString(); + } + + internal int GetInitialScanCount(out ushort streamingAggregateDepth) + { + // this is *similar* to GetDelta, but: without any discount for attributes + switch (_flags & (RespFlags.IsAggregate | RespFlags.IsStreaming)) + { + case RespFlags.IsAggregate: + streamingAggregateDepth = 0; + return _length - 1; + case RespFlags.IsAggregate | RespFlags.IsStreaming: + streamingAggregateDepth = 1; + return 0; + default: + streamingAggregateDepth = 0; + return -1; + } + } +} diff --git a/src/RESP.Core/RespReader.cs b/src/RESP.Core/RespReader.cs new file mode 100644 index 000000000..1b0f0cead --- /dev/null +++ b/src/RESP.Core/RespReader.cs @@ -0,0 +1,1599 @@ +using System; +using System.Buffers; +using System.Buffers.Text; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; + +#if NETCOREAPP3_0_OR_GREATER +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace Resp; + +/// +/// Provides low level RESP parsing functionality. +/// +public ref partial struct RespReader +{ + [Flags] + private enum RespFlags : byte + { + None = 0, + IsScalar = 1 << 0, // simple strings, bulk strings, etc + IsAggregate = 1 << 1, // arrays, maps, sets, etc + IsNull = 1 << 2, // explicit null RESP types, or bulk-strings/aggregates with length -1 + IsInlineScalar = 1 << 3, // a non-null scalar, i.e. with payload+CrLf + IsAttribute = 1 << 4, // is metadata for following elements + IsStreaming = 1 << 5, // unknown length + IsError = 1 << 6, // an explicit error reported inside the protocol + } + + // relates to the element we're currently reading + private RespFlags _flags; + private RespPrefix _prefix; + + private int _length; // for null: 0; for scalars: the length of the payload; for aggregates: the child count + + // the current buffer that we're observing + private int _bufferIndex; // after TryRead, this should be positioned immediately before the actual data + + // the position in a multi-segment payload + private long _positionBase; // total data we've already moved past in *previous* buffers + private ReadOnlySequenceSegment? _tail; // the next tail node + private long _remainingTailLength; // how much more can we consume from the tail? + + public long ProtocolBytesRemaining => TotalAvailable; + + private readonly int CurrentAvailable => CurrentLength - _bufferIndex; + + private readonly long TotalAvailable => CurrentAvailable + _remainingTailLength; + private partial void UnsafeTrimCurrentBy(int count); + private readonly partial ref byte UnsafeCurrent { get; } + private readonly partial int CurrentLength { get; } + private partial void SetCurrent(ReadOnlySpan value); + private RespPrefix UnsafePeekPrefix() => (RespPrefix)UnsafeCurrent; + private readonly partial ReadOnlySpan UnsafePastPrefix(); + private readonly partial ReadOnlySpan CurrentSpan(); + + /// + /// Get the scalar value as a single-segment span. + /// + /// True if this is a non-streaming scalar element that covers a single span only, otherwise False. + /// If a scalar reports False, can be used to iterate the entire payload. + /// When True, the contents of the scalar value. + public readonly bool TryGetSpan(out ReadOnlySpan value) + { + if (IsInlineScalar && CurrentAvailable >= _length) + { + value = CurrentSpan().Slice(0, _length); + return true; + } + + value = default; + return IsNullScalar; + } + + /// + /// Returns the position after the end of the current element. + /// + public readonly long BytesConsumed => _positionBase + _bufferIndex + TrailingLength; + + /// + /// Body length of scalar values, plus any terminating sentinels. + /// + private readonly int TrailingLength => (_flags & RespFlags.IsInlineScalar) == 0 ? 0 : (_length + 2); + + /// + /// Gets the RESP kind of the current element. + /// + public readonly RespPrefix Prefix => _prefix; + + /// + /// The payload length of this scalar element (includes combined length for streaming scalars). + /// + public readonly int ScalarLength() => IsInlineScalar ? _length : IsNullScalar ? 0 : checked((int)ScalarLengthSlow()); + + /// + /// Indicates whether this scalar value is zero-length. + /// + public readonly bool ScalarIsEmpty() => IsInlineScalar ? _length == 0 : (IsNullScalar || !ScalarChunks().MoveNext()); + + /// + /// The payload length of this scalar element (includes combined length for streaming scalars). + /// + public readonly long ScalarLongLength() => IsInlineScalar ? _length : IsNullScalar ? 0 : ScalarLengthSlow(); + + private readonly long ScalarLengthSlow() + { + DemandScalar(); + long length = 0; + var iterator = ScalarChunks(); + while (iterator.MoveNext()) + { + length += iterator.CurrentLength; + } + return length; + } + + /// + /// The number of child elements associated with an aggregate. + /// + /// For + /// and aggregates, this is twice the value reported in the RESP protocol, + /// i.e. a map of the form %2\r\n... will report 4 as the length. + /// Note that if the data could be streaming (), it may be preferable to use + /// the API, using the API to update the outer reader. + public readonly int AggregateLength() => (_flags & (RespFlags.IsAggregate | RespFlags.IsStreaming)) == RespFlags.IsAggregate + ? _length : AggregateLengthSlow(); + + public delegate T Projection(ref RespReader value); + + public void FillAll(scoped Span target, Projection projection) + { + DemandNotNull(); + AggregateChildren().FillAll(target, projection); + } + + private readonly int AggregateLengthSlow() + { + switch (_flags & (RespFlags.IsAggregate | RespFlags.IsStreaming)) + { + case RespFlags.IsAggregate: + return _length; + case RespFlags.IsAggregate | RespFlags.IsStreaming: + break; + default: + DemandAggregate(); // we expect this to throw + break; + } + + int count = 0; + var reader = Clone(); + while (true) + { + if (!reader.TryMoveNext()) ThrowEOF(); + if (reader.Prefix == RespPrefix.StreamTerminator) + { + return count; + } + reader.SkipChildren(); + count++; + } + } + + /// + /// Indicates whether this is a scalar value, i.e. with a potential payload body. + /// + public readonly bool IsScalar => (_flags & RespFlags.IsScalar) != 0; + + internal readonly bool IsInlineScalar => (_flags & RespFlags.IsInlineScalar) != 0; + + internal readonly bool IsNullScalar => (_flags & (RespFlags.IsScalar | RespFlags.IsNull)) == (RespFlags.IsScalar | RespFlags.IsNull); + + /// + /// Indicates whether this is an aggregate value, i.e. represents a collection of sub-values. + /// + public readonly bool IsAggregate => (_flags & RespFlags.IsAggregate) != 0; + + /// + /// Indicates whether this is a null value; this could be an explicit , + /// or a scalar or aggregate a negative reported length. + /// + public readonly bool IsNull => (_flags & RespFlags.IsNull) != 0; + + /// + /// Indicates whether this is an attribute value, i.e. metadata relating to later element data. + /// + public readonly bool IsAttribute => (_flags & RespFlags.IsAttribute) != 0; + + /// + /// Indicates whether this represents streaming content, where the or is not known in advance. + /// + public readonly bool IsStreaming => (_flags & RespFlags.IsStreaming) != 0; + + /// + /// Equivalent to both and . + /// + internal readonly bool IsStreamingScalar => (_flags & (RespFlags.IsScalar | RespFlags.IsStreaming)) == (RespFlags.IsScalar | RespFlags.IsStreaming); + + /// + /// Indicates errors reported inside the protocol. + /// + public readonly bool IsError => (_flags & RespFlags.IsError) != 0; + + /// + /// Gets the effective change (in terms of how many RESP nodes we expect to see) from consuming this element. + /// For simple scalars, this is -1 because we have one less node to read; for simple aggregates, this is + /// AggregateLength-1 because we will have consumed one element, but now need to read the additional + /// child elements. Attributes report 0, since they supplement data + /// we still need to consume. The final terminator for streaming data reports a delta of -1, otherwise: 0. + /// + /// This does not account for being nested inside a streaming aggregate; the caller must deal with that manually. + internal int Delta() => (_flags & (RespFlags.IsScalar | RespFlags.IsAggregate | RespFlags.IsStreaming | RespFlags.IsAttribute)) switch + { + RespFlags.IsScalar => -1, + RespFlags.IsAggregate => _length - 1, + RespFlags.IsAggregate | RespFlags.IsAttribute => _length, + _ => 0, + }; + + /// + /// Assert that this is the final element in the current payload. + /// + /// If additional elements are available. + public void DemandEnd() + { + while (IsStreamingScalar) + { + if (!TryReadNext()) ThrowEOF(); + } + if (TryReadNext()) + { + Throw(Prefix); + } + static void Throw(RespPrefix prefix) => throw new InvalidOperationException($"Expected end of payload, but found {prefix}"); + } + + private bool TryReadNextSkipAttributes() + { + while (TryReadNext()) + { + if (IsAttribute) + { + SkipChildren(); + } + else + { + return true; + } + } + return false; + } + + private bool TryReadNextProcessAttributes(RespAttributeReader respAttributeReader, ref T attributes) + { + while (TryReadNext()) + { + if (IsAttribute) + { + respAttributeReader.Read(ref this, ref attributes); + } + else + { + return true; + } + } + return false; + } + + /// + /// Move to the next content element; this skips attribute metadata, checking for RESP error messages by default. + /// + /// If the data is exhausted before a streaming scalar is exhausted. + /// If the data contains an explicit error element. + public bool TryMoveNext() + { + while (IsStreamingScalar) // close out the current streaming scalar + { + if (!TryReadNextSkipAttributes()) ThrowEOF(); + } + + if (TryReadNextSkipAttributes()) + { + if (IsError) ThrowError(); + return true; + } + return false; + } + + /// + /// Move to the next content element; this skips attribute metadata, checking for RESP error messages by default. + /// + /// Whether to check and throw for error messages. + /// If the data is exhausted before a streaming scalar is exhausted. + /// If the data contains an explicit error element. + public bool TryMoveNext(bool checkError) + { + while (IsStreamingScalar) // close out the current streaming scalar + { + if (!TryReadNextSkipAttributes()) ThrowEOF(); + } + + if (TryReadNextSkipAttributes()) + { + if (checkError && IsError) ThrowError(); + return true; + } + return false; + } + + /// + /// Move to the next content element; this skips attribute metadata, checking for RESP error messages by default. + /// + /// Parser for attribute data preceding the data. + /// The state for attributes encountered. + /// If the data is exhausted before a streaming scalar is exhausted. + /// If the data contains an explicit error element. + /// The type of data represented by this reader. + public bool TryMoveNext(RespAttributeReader respAttributeReader, ref T attributes) + { + while (IsStreamingScalar) // close out the current streaming scalar + { + if (!TryReadNextSkipAttributes()) ThrowEOF(); + } + + if (TryReadNextProcessAttributes(respAttributeReader, ref attributes)) + { + if (IsError) ThrowError(); + return true; + } + return false; + } + + /// + /// Move to the next content element, asserting that it is of the expected type; this skips attribute metadata, checking for RESP error messages by default. + /// + /// The expected data type. + /// If the data is exhausted before a streaming scalar is exhausted. + /// If the data contains an explicit error element. + /// If the data is not of the expected type. + public bool TryMoveNext(RespPrefix prefix) + { + bool result = TryMoveNext(); + if (result) Demand(prefix); + return result; + } + + /// + /// Move to the next content element; this skips attribute metadata, checking for RESP error messages by default. + /// + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + public void MoveNext() + { + if (!TryMoveNext()) ThrowEOF(); + } + + /// + /// Move to the next content element; this skips attribute metadata, checking for RESP error messages by default. + /// + /// Parser for attribute data preceding the data. + /// The state for attributes encountered. + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + /// The type of data represented by this reader. + public void MoveNext(RespAttributeReader respAttributeReader, ref T attributes) + { + if (!TryMoveNext(respAttributeReader, ref attributes)) ThrowEOF(); + } + + private bool MoveNextStreamingScalar() + { + if (IsStreamingScalar) + { + while (TryReadNext()) + { + if (IsAttribute) + { + SkipChildren(); + } + else + { + if (Prefix != RespPrefix.StreamContinuation) ThrowProtocolFailure("Streaming continuation expected"); + return _length > 0; + } + } + ThrowEOF(); // we should have found something! + } + return false; + } + + /// + /// Move to the next content element () and assert that it is a scalar (). + /// + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + /// If the data is not a scalar type. + public void MoveNextScalar() + { + MoveNext(); + DemandScalar(); + } + + /// + /// Move to the next content element () and assert that it is an aggregate (). + /// + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + /// If the data is not an aggregate type. + public void MoveNextAggregate() + { + MoveNext(); + DemandAggregate(); + } + + /// + /// Move to the next content element () and assert that it of type specified + /// in . + /// + /// The expected data type. + /// Parser for attribute data preceding the data. + /// The state for attributes encountered. + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + /// If the data is not of the expected type. + /// The type of data represented by this reader. + public void MoveNext(RespPrefix prefix, RespAttributeReader respAttributeReader, ref T attributes) + { + MoveNext(respAttributeReader, ref attributes); + Demand(prefix); + } + + /// + /// Move to the next content element () and assert that it of type specified + /// in . + /// + /// The expected data type. + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + /// If the data is not of the expected type. + public void MoveNext(RespPrefix prefix) + { + MoveNext(); + Demand(prefix); + } + + internal void Demand(RespPrefix prefix) + { + if (Prefix != prefix) Throw(prefix, Prefix); + static void Throw(RespPrefix expected, RespPrefix actual) => throw new InvalidOperationException($"Expected {expected} element, but found {actual}."); + } + + private readonly void ThrowError() => throw new RespException(ReadString()!); + + /// + /// Skip all sub elements of the current node; this includes both aggregate children and scalar streaming elements. + /// + public void SkipChildren() + { + // if this is a simple non-streaming scalar, then: there's nothing complex to do; otherwise, re-use the + // frame scanner logic to seek past the noise (this way, we avoid recursion etc) + switch (_flags & (RespFlags.IsScalar | RespFlags.IsAggregate | RespFlags.IsStreaming)) + { + case RespFlags.None: + // no current element + break; + case RespFlags.IsScalar: + // simple scalar + MovePastCurrent(); + break; + default: + // something more complex + RespScanState state = new(in this); + if (!state.TryRead(ref this, out _)) ThrowEOF(); + break; + } + } + + /// + /// Reads the current element as a string value. + /// + public readonly string? ReadString() => ReadString(out _); + + /// + /// Reads the current element as a string value. + /// + public readonly string? ReadString(out string prefix) + { + byte[] pooled = []; + try + { + var span = Buffer(ref pooled, stackalloc byte[256]); + prefix = ""; + if (span.IsEmpty) + { + return IsNull ? null : ""; + } + if (Prefix == RespPrefix.VerbatimString + && span.Length >= 4 && span[3] == ':') + { + // "the first three bytes provide information about the format of the following string, + // which can be txt for plain text, or mkd for markdown. The fourth byte is always :. + // Then the real string follows." + var prefixValue = RespConstants.UnsafeCpuUInt32(span); + if (prefixValue == PrefixTxt) + { + prefix = "txt"; + } + else if (prefixValue == PrefixMkd) + { + prefix = "mkd"; + } + else + { + prefix = RespConstants.UTF8.GetString(span.Slice(0, 3)); + } + span = span.Slice(4); + } + return RespConstants.UTF8.GetString(span); + } + finally + { + ArrayPool.Shared.Return(pooled); + } + } + + private static readonly uint + PrefixTxt = RespConstants.UnsafeCpuUInt32("txt:"u8), + PrefixMkd = RespConstants.UnsafeCpuUInt32("mkd:"u8); + + /// + /// Reads the current element as a string value. + /// + public readonly byte[]? ReadByteArray() + { + byte[] pooled = []; + try + { + var span = Buffer(ref pooled, stackalloc byte[256]); + if (span.IsEmpty) + { + return IsNull ? null : []; + } + return span.ToArray(); + } + finally + { + ArrayPool.Shared.Return(pooled); + } + } + + /// + /// Reads the current element using a general purpose text parser. + /// + /// The type of data being parsed. + public readonly T ParseBytes(Parser parser) + { + byte[] pooled = []; + var span = Buffer(ref pooled, stackalloc byte[256]); + try + { + return parser(span); + } + finally + { + ArrayPool.Shared.Return(pooled); + } + } + + /// + /// Reads the current element using a general purpose text parser. + /// + /// The type of data being parsed. + /// State required by the parser. + public readonly T ParseBytes(Parser parser, TState? state) + { + byte[] pooled = []; + var span = Buffer(ref pooled, stackalloc byte[256]); + try + { + return parser(span, default); + } + finally + { + ArrayPool.Shared.Return(pooled); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly ReadOnlySpan Buffer(Span target) + { + if (TryGetSpan(out var simple)) + { + return simple; + } + +#if NET6_0_OR_GREATER + return BufferSlow(ref Unsafe.NullRef(), target, usePool: false); +#else + byte[] pooled = []; + return BufferSlow(ref pooled, target, usePool: false); +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly ReadOnlySpan Buffer(scoped ref byte[] pooled, Span target = default) + => TryGetSpan(out var simple) ? simple : BufferSlow(ref pooled, target, true); + + [MethodImpl(MethodImplOptions.NoInlining)] + private readonly ReadOnlySpan BufferSlow(scoped ref byte[] pooled, Span target, bool usePool) + { + DemandScalar(); + + if (IsInlineScalar && usePool) + { + // grow to the correct size in advance, if needed + var length = ScalarLength(); + if (length > target.Length) + { + var bigger = ArrayPool.Shared.Rent(length); + ArrayPool.Shared.Return(pooled); + target = pooled = bigger; + } + } + + var iterator = ScalarChunks(); + ReadOnlySpan current; + int offset = 0; + while (iterator.MoveNext()) + { + // will the current chunk fit? + current = iterator.Current; + if (current.TryCopyTo(target.Slice(offset))) + { + // fits into the current buffer + offset += current.Length; + } + else if (!usePool) + { + // rent disallowed; fill what we can + var available = target.Slice(offset); + current.Slice(0, available.Length).CopyTo(available); + return target; // we filled it + } + else + { + // rent a bigger buffer, copy and recycle + var bigger = ArrayPool.Shared.Rent(offset + current.Length); + if (offset != 0) + { + target.Slice(0, offset).CopyTo(bigger); + } + ArrayPool.Shared.Return(pooled); + target = pooled = bigger; + current.CopyTo(target.Slice(offset)); + } + } + return target.Slice(0, offset); + } + + /// + /// Reads the current element using a general purpose byte parser. + /// + /// The type of data being parsed. + public readonly T ParseChars(Parser parser) + { + byte[] bArr = []; + char[] cArr = []; + try + { + var bSpan = Buffer(ref bArr, stackalloc byte[128]); + var maxChars = RespConstants.UTF8.GetMaxCharCount(bSpan.Length); + Span cSpan = maxChars <= 128 ? stackalloc char[128] : (cArr = ArrayPool.Shared.Rent(maxChars)); + int chars = RespConstants.UTF8.GetChars(bSpan, cSpan); + return parser(cSpan.Slice(0, chars)); + } + finally + { + ArrayPool.Shared.Return(bArr); + ArrayPool.Shared.Return(cArr); + } + } + + /// + /// Reads the current element using a general purpose byte parser. + /// + /// The type of data being parsed. + /// State required by the parser. + public readonly T ParseChars(Parser parser, TState? state) + { + byte[] bArr = []; + char[] cArr = []; + try + { + var bSpan = Buffer(ref bArr, stackalloc byte[128]); + var maxChars = RespConstants.UTF8.GetMaxCharCount(bSpan.Length); + Span cSpan = maxChars <= 128 ? stackalloc char[128] : (cArr = ArrayPool.Shared.Rent(maxChars)); + int chars = RespConstants.UTF8.GetChars(bSpan, cSpan); + return parser(cSpan.Slice(0, chars), state); + } + finally + { + ArrayPool.Shared.Return(bArr); + ArrayPool.Shared.Return(cArr); + } + } + +#if NET7_0_OR_GREATER + /// + /// Reads the current element using . + /// + /// The type of data being parsed. +#pragma warning disable RS0016, RS0027 // back-compat overload + public readonly T ParseChars(IFormatProvider? formatProvider = null) where T : ISpanParsable +#pragma warning restore RS0016, RS0027 // back-compat overload + { + byte[] bArr = []; + char[] cArr = []; + try + { + var bSpan = Buffer(ref bArr, stackalloc byte[128]); + var maxChars = RespConstants.UTF8.GetMaxCharCount(bSpan.Length); + Span cSpan = maxChars <= 128 ? stackalloc char[128] : (cArr = ArrayPool.Shared.Rent(maxChars)); + int chars = RespConstants.UTF8.GetChars(bSpan, cSpan); + return T.Parse(cSpan.Slice(0, chars), formatProvider ?? CultureInfo.InvariantCulture); + } + finally + { + ArrayPool.Shared.Return(bArr); + ArrayPool.Shared.Return(cArr); + } + } +#endif + +#if NET8_0_OR_GREATER + /// + /// Reads the current element using . + /// + /// The type of data being parsed. +#pragma warning disable RS0016, RS0027 // back-compat overload + public readonly T ParseBytes(IFormatProvider? formatProvider = null) where T : IUtf8SpanParsable +#pragma warning restore RS0016, RS0027 // back-compat overload + { + byte[] bArr = []; + try + { + var bSpan = Buffer(ref bArr, stackalloc byte[128]); + return T.Parse(bSpan, formatProvider ?? CultureInfo.InvariantCulture); + } + finally + { + ArrayPool.Shared.Return(bArr); + } + } +#endif + + /// + /// General purpose parsing callback. + /// + /// The type of source data being parsed. + /// State required by the parser. + /// The output type of data being parsed. + public delegate TValue Parser(ReadOnlySpan value, TState? state); + + /// + /// General purpose parsing callback. + /// + /// The type of source data being parsed. + /// The output type of data being parsed. + public delegate TValue Parser(ReadOnlySpan value); + + /// + /// Initializes a new instance of the struct. + /// + /// The raw contents to parse with this instance. + public RespReader(ReadOnlySpan value) + { + _length = 0; + _flags = RespFlags.None; + _prefix = RespPrefix.None; + SetCurrent(value); + + _remainingTailLength = _positionBase = 0; + _tail = null; + } + + private void MovePastCurrent() + { + // skip past the trailing portion of a value, if any + var skip = TrailingLength; + if (_bufferIndex + skip <= CurrentLength) + { + _bufferIndex += skip; // available in the current buffer + } + else + { + AdvanceSlow(skip); + } + + // reset the current state + _length = 0; + _flags = 0; + _prefix = RespPrefix.None; + } + + /// + public RespReader(scoped in ReadOnlySequence value) +#if NETCOREAPP3_0_OR_GREATER + : this(value.FirstSpan) +#else + : this(value.First.Span) +#endif + { + if (!value.IsSingleSegment) + { + _remainingTailLength = value.Length - CurrentLength; + _tail = (value.Start.GetObject() as ReadOnlySequenceSegment)?.Next ?? MissingNext(); + } + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + static ReadOnlySequenceSegment MissingNext() => throw new ArgumentException("Unable to extract tail segment", nameof(value)); + } + + /// + /// Attempt to move to the next RESP element. + /// + /// Unless you are intentionally handling errors, attributes and streaming data, should be preferred. + [EditorBrowsable(EditorBrowsableState.Never), Browsable(false)] + public unsafe bool TryReadNext() + { + MovePastCurrent(); + +#if NETCOREAPP3_0_OR_GREATER + // check what we have available; don't worry about zero/fetching the next segment; this is only + // for SIMD lookup, and zero would only apply when data ends exactly on segment boundaries, which + // is incredible niche + var available = CurrentAvailable; + + if (Avx2.IsSupported && Bmi1.IsSupported && available >= sizeof(uint)) + { + // read the first 4 bytes + ref byte origin = ref UnsafeCurrent; + var comparand = Unsafe.ReadUnaligned(ref origin); + + // broadcast those 4 bytes into a vector, mask to get just the first and last byte, and apply a SIMD equality test with our known cases + var eqs = Avx2.CompareEqual(Avx2.And(Avx2.BroadcastScalarToVector256(&comparand), Raw.FirstLastMask), Raw.CommonRespPrefixes); + + // reinterpret that as floats, and pick out the sign bits (which will be 1 for "equal", 0 for "not equal"); since the + // test cases are mutually exclusive, we expect zero or one matches, so: lzcount tells us which matched + var index = Bmi1.TrailingZeroCount((uint)Avx.MoveMask(Unsafe.As, Vector256>(ref eqs))); + int len; +#if DEBUG + if (VectorizeDisabled) index = uint.MaxValue; // just to break the switch +#endif + switch (index) + { + case Raw.CommonRespIndex_Success when available >= 5 && Unsafe.Add(ref origin, 4) == (byte)'\n': + _prefix = RespPrefix.SimpleString; + _length = 2; + _bufferIndex++; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + return true; + case Raw.CommonRespIndex_SingleDigitInteger when Unsafe.Add(ref origin, 2) == (byte)'\r': + _prefix = RespPrefix.Integer; + _length = 1; + _bufferIndex++; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + return true; + case Raw.CommonRespIndex_DoubleDigitInteger when available >= 5 && Unsafe.Add(ref origin, 4) == (byte)'\n': + _prefix = RespPrefix.Integer; + _length = 2; + _bufferIndex++; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + return true; + case Raw.CommonRespIndex_SingleDigitString when Unsafe.Add(ref origin, 2) == (byte)'\r': + if (comparand == RespConstants.BulkStringStreaming) + { + _flags = RespFlags.IsScalar | RespFlags.IsStreaming; + } + else + { + len = ParseSingleDigit(Unsafe.Add(ref origin, 1)); + if (available < len + 6) break; // need more data + + UnsafeAssertClLf(4 + len); + _length = len; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + } + _prefix = RespPrefix.BulkString; + _bufferIndex += 4; + return true; + case Raw.CommonRespIndex_DoubleDigitString when available >= 5 && Unsafe.Add(ref origin, 4) == (byte)'\n': + if (comparand == RespConstants.BulkStringNull) + { + _length = 0; + _flags = RespFlags.IsScalar | RespFlags.IsNull; + } + else + { + len = ParseDoubleDigitsNonNegative(ref Unsafe.Add(ref origin, 1)); + if (available < len + 7) break; // need more data + + UnsafeAssertClLf(5 + len); + _length = len; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + } + _prefix = RespPrefix.BulkString; + _bufferIndex += 5; + return true; + case Raw.CommonRespIndex_SingleDigitArray when Unsafe.Add(ref origin, 2) == (byte)'\r': + if (comparand == RespConstants.ArrayStreaming) + { + _flags = RespFlags.IsAggregate | RespFlags.IsStreaming; + } + else + { + _flags = RespFlags.IsAggregate; + _length = ParseSingleDigit(Unsafe.Add(ref origin, 1)); + } + _prefix = RespPrefix.Array; + _bufferIndex += 4; + return true; + case Raw.CommonRespIndex_DoubleDigitArray when available >= 5 && Unsafe.Add(ref origin, 4) == (byte)'\n': + if (comparand == RespConstants.ArrayNull) + { + _flags = RespFlags.IsAggregate | RespFlags.IsNull; + } + else + { + _length = ParseDoubleDigitsNonNegative(ref Unsafe.Add(ref origin, 1)); + _flags = RespFlags.IsAggregate; + } + _prefix = RespPrefix.Array; + _bufferIndex += 5; + return true; + case Raw.CommonRespIndex_Error: + len = UnsafePastPrefix().IndexOf(RespConstants.CrlfBytes); + if (len < 0) break; // need more data + + _prefix = RespPrefix.SimpleError; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar | RespFlags.IsError; + _length = len; + _bufferIndex++; + return true; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static int ParseDoubleDigitsNonNegative(ref byte value) => (10 * ParseSingleDigit(value)) + ParseSingleDigit(Unsafe.Add(ref value, 1)); +#endif + + // no fancy vectorization, but: we can still try to find the payload the fast way in a single segment + if (_bufferIndex + 3 <= CurrentLength) // shortest possible RESP fragment is length 3 + { + var remaining = UnsafePastPrefix(); + switch (_prefix = UnsafePeekPrefix()) + { + case RespPrefix.SimpleString: + case RespPrefix.SimpleError: + case RespPrefix.Integer: + case RespPrefix.Boolean: + case RespPrefix.Double: + case RespPrefix.BigInteger: + // CRLF-terminated + _length = remaining.IndexOf(RespConstants.CrlfBytes); + if (_length < 0) break; // can't find, need more data + _bufferIndex++; // payload follows prefix directly + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + if (_prefix == RespPrefix.SimpleError) _flags |= RespFlags.IsError; + return true; + case RespPrefix.BulkError: + case RespPrefix.BulkString: + case RespPrefix.VerbatimString: + // length prefix with value payload; first, the length + switch (TryReadLengthPrefix(remaining, out _length, out int consumed)) + { + case LengthPrefixResult.Length: + // still need to valid terminating CRLF + if (remaining.Length < consumed + _length + 2) break; // need more data + UnsafeAssertClLf(1 + consumed + _length); + + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + break; + case LengthPrefixResult.Null: + _flags = RespFlags.IsScalar | RespFlags.IsNull; + break; + case LengthPrefixResult.Streaming: + _flags = RespFlags.IsScalar | RespFlags.IsStreaming; + break; + } + if (_flags == 0) break; // will need more data to know + if (_prefix == RespPrefix.BulkError) _flags |= RespFlags.IsError; + _bufferIndex += 1 + consumed; + return true; + case RespPrefix.StreamContinuation: + // length prefix, possibly with value payload; first, the length + switch (TryReadLengthPrefix(remaining, out _length, out consumed)) + { + case LengthPrefixResult.Length when _length == 0: + // EOF, no payload + _flags = RespFlags.IsScalar; // don't claim as streaming, we want this to count towards delta-decrement + break; + case LengthPrefixResult.Length: + // still need to valid terminating CRLF + if (remaining.Length < consumed + _length + 2) break; // need more data + UnsafeAssertClLf(1 + consumed + _length); + + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar | RespFlags.IsStreaming; + break; + case LengthPrefixResult.Null: + case LengthPrefixResult.Streaming: + ThrowProtocolFailure("Invalid streaming scalar length prefix"); + break; + } + if (_flags == 0) break; // will need more data to know + _bufferIndex += 1 + consumed; + return true; + case RespPrefix.Array: + case RespPrefix.Set: + case RespPrefix.Map: + case RespPrefix.Push: + case RespPrefix.Attribute: + // length prefix without value payload (child values follow) + switch (TryReadLengthPrefix(remaining, out _length, out consumed)) + { + case LengthPrefixResult.Length: + _flags = RespFlags.IsAggregate; + if (AggregateLengthNeedsDoubling()) _length *= 2; + break; + case LengthPrefixResult.Null: + _flags = RespFlags.IsAggregate | RespFlags.IsNull; + break; + case LengthPrefixResult.Streaming: + _flags = RespFlags.IsAggregate | RespFlags.IsStreaming; + break; + } + if (_flags == 0) break; // will need more data to know + if (_prefix is RespPrefix.Attribute) _flags |= RespFlags.IsAttribute; + _bufferIndex += consumed + 1; + return true; + case RespPrefix.Null: // null + // note we already checked we had 3 bytes + UnsafeAssertClLf(1); + _flags = RespFlags.IsScalar | RespFlags.IsNull; + _bufferIndex += 3; // skip prefix+terminator + return true; + case RespPrefix.StreamTerminator: + // note we already checked we had 3 bytes + UnsafeAssertClLf(1); + _flags = RespFlags.IsAggregate; // don't claim as streaming - this counts towards delta + _bufferIndex += 3; // skip prefix+terminator + return true; + default: + ThrowProtocolFailure("Unexpected protocol prefix: " + _prefix); + return false; + } + } + + return TryReadNextSlow(ref this); + } + + private static bool TryReadNextSlow(ref RespReader live) + { + // in the case of failure, we don't want to apply any changes, + // so we work against an isolated copy until we're happy + live.MovePastCurrent(); + RespReader isolated = live; + + int next = isolated.RawTryReadByte(); + if (next < 0) return false; + + switch (isolated._prefix = (RespPrefix)next) + { + case RespPrefix.SimpleString: + case RespPrefix.SimpleError: + case RespPrefix.Integer: + case RespPrefix.Boolean: + case RespPrefix.Double: + case RespPrefix.BigInteger: + // CRLF-terminated + if (!isolated.RawTryFindCrLf(out isolated._length)) return false; + isolated._flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + if (isolated._prefix == RespPrefix.SimpleError) isolated._flags |= RespFlags.IsError; + break; + case RespPrefix.BulkError: + case RespPrefix.BulkString: + case RespPrefix.VerbatimString: + // length prefix with value payload + switch (isolated.RawTryReadLengthPrefix()) + { + case LengthPrefixResult.Length: + // still need to valid terminating CRLF + isolated._flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + if (!isolated.RawTryAssertInlineScalarPayloadCrLf()) return false; + break; + case LengthPrefixResult.Null: + isolated._flags = RespFlags.IsScalar | RespFlags.IsNull; + break; + case LengthPrefixResult.Streaming: + isolated._flags = RespFlags.IsScalar | RespFlags.IsStreaming; + break; + case LengthPrefixResult.NeedMoreData: + return false; + default: + ThrowProtocolFailure("Unexpected length prefix"); + return false; + } + if (isolated._prefix == RespPrefix.BulkError) isolated._flags |= RespFlags.IsError; + break; + case RespPrefix.Array: + case RespPrefix.Set: + case RespPrefix.Map: + case RespPrefix.Push: + case RespPrefix.Attribute: + // length prefix without value payload (child values follow) + switch (isolated.RawTryReadLengthPrefix()) + { + case LengthPrefixResult.Length: + isolated._flags = RespFlags.IsAggregate; + if (isolated.AggregateLengthNeedsDoubling()) isolated._length *= 2; + break; + case LengthPrefixResult.Null: + isolated._flags = RespFlags.IsAggregate | RespFlags.IsNull; + break; + case LengthPrefixResult.Streaming: + isolated._flags = RespFlags.IsAggregate | RespFlags.IsStreaming; + break; + case LengthPrefixResult.NeedMoreData: + return false; + default: + ThrowProtocolFailure("Unexpected length prefix"); + return false; + } + if (isolated._prefix is RespPrefix.Attribute) isolated._flags |= RespFlags.IsAttribute; + break; + case RespPrefix.Null: // null + if (!isolated.RawAssertCrLf()) return false; + isolated._flags = RespFlags.IsScalar | RespFlags.IsNull; + break; + case RespPrefix.StreamTerminator: + if (!isolated.RawAssertCrLf()) return false; + isolated._flags = RespFlags.IsAggregate; // don't claim as streaming - this counts towards delta + break; + case RespPrefix.StreamContinuation: + // length prefix, possibly with value payload; first, the length + switch (isolated.RawTryReadLengthPrefix()) + { + case LengthPrefixResult.Length when isolated._length == 0: + // EOF, no payload + isolated._flags = RespFlags.IsScalar; // don't claim as streaming, we want this to count towards delta-decrement + break; + case LengthPrefixResult.Length: + // still need to valid terminating CRLF + isolated._flags = RespFlags.IsScalar | RespFlags.IsInlineScalar | RespFlags.IsStreaming; + if (!isolated.RawTryAssertInlineScalarPayloadCrLf()) return false; // need more data + break; + case LengthPrefixResult.Null: + case LengthPrefixResult.Streaming: + ThrowProtocolFailure("Invalid streaming scalar length prefix"); + break; + case LengthPrefixResult.NeedMoreData: + default: + return false; + } + break; + default: + ThrowProtocolFailure("Unexpected protocol prefix: " + isolated._prefix); + return false; + } + // commit the speculative changes back, and accept + live = isolated; + return true; + } + + private void AdvanceSlow(long bytes) + { + while (bytes > 0) + { + var available = CurrentLength - _bufferIndex; + if (bytes <= available) + { + _bufferIndex += (int)bytes; + return; + } + bytes -= available; + + if (!TryMoveToNextSegment()) Throw(); + } + + [DoesNotReturn] + static void Throw() => throw new EndOfStreamException("Unexpected end of payload; this is unexpected because we already validated that it was available!"); + } + + private bool AggregateLengthNeedsDoubling() => _prefix is RespPrefix.Map or RespPrefix.Attribute; + + private bool TryMoveToNextSegment() + { + while (_tail is not null && _remainingTailLength > 0) + { + var memory = _tail.Memory; + _tail = _tail.Next; + if (!memory.IsEmpty) + { + var span = memory.Span; // check we can get this before mutating anything + _positionBase += CurrentLength; + if (span.Length > _remainingTailLength) + { + span = span.Slice(0, (int)_remainingTailLength); + _remainingTailLength = 0; + } + else + { + _remainingTailLength -= span.Length; + } + SetCurrent(span); + _bufferIndex = 0; + return true; + } + } + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly bool IsOK() // go mad with this, because it is used so often + { + return TryGetSpan(out var span) && span.Length == 2 + ? Unsafe.ReadUnaligned(ref UnsafeCurrent) == RespConstants.OKUInt16 + : IsSlow(RespConstants.OKBytes); + } + + /// + /// Indicates whether the current element is a scalar with a value that matches the provided . + /// + /// The payload value to verify. + public readonly bool Is(ReadOnlySpan value) + => TryGetSpan(out var span) ? span.SequenceEqual(value) : IsSlow(value); + + internal readonly bool IsInlneCpuUInt32(uint value) + { + if (IsInlineScalar && _length == sizeof(uint)) + { + return CurrentAvailable >= sizeof(uint) + ? Unsafe.ReadUnaligned(ref UnsafeCurrent) == value + : SlowIsInlneCpuUInt32(value); + } + + return false; + } + + private readonly bool SlowIsInlneCpuUInt32(uint value) + { + Debug.Assert(IsInlineScalar && _length == sizeof(uint), "should be inline scalar of length 4"); + Span buffer = stackalloc byte[sizeof(uint)]; + var copy = this; + copy.RawFillBytes(buffer); + return RespConstants.UnsafeCpuUInt32(buffer) == value; + } + + /// + /// Indicates whether the current element is a scalar with a value that matches the provided . + /// + /// The payload value to verify. + public readonly bool Is(byte value) + { + if (IsInlineScalar && _length == 1 && CurrentAvailable >= 1) + { + return UnsafeCurrent == value; + } + + ReadOnlySpan span = [value]; + return IsSlow(span); + } + + private readonly bool IsSlow(ReadOnlySpan testValue) + { + DemandScalar(); + if (IsNull) return false; // nothing equals null + if (TotalAvailable < testValue.Length) return false; + + if (!IsStreaming && testValue.Length != ScalarLength()) return false; + + var iterator = ScalarChunks(); + while (true) + { + if (testValue.IsEmpty) + { + // nothing left to test; if also nothing left to read, great! + return !iterator.MoveNext(); + } + if (!iterator.MoveNext()) + { + return false; // test is longer + } + + var current = iterator.Current; + if (testValue.Length < current.Length) return false; // payload is longer + + if (!current.SequenceEqual(testValue.Slice(0, current.Length))) return false; // payload is different + + testValue = testValue.Slice(current.Length); // validated; continue + } + } + + /// + /// Copy the current scalar value out into the supplied , or as much as can be copied. + /// + /// The destination for the copy operation. + /// The number of bytes successfully copied. + public readonly int CopyTo(Span target) + { + if (TryGetSpan(out var value)) + { + if (target.Length < value.Length) value = value.Slice(0, target.Length); + + value.CopyTo(target); + return value.Length; + } + + int totalBytes = 0; + var iterator = ScalarChunks(); + while (iterator.MoveNext()) + { + value = iterator.Current; + if (target.Length <= value.Length) + { + value.Slice(0, target.Length).CopyTo(target); + return totalBytes + target.Length; + } + + value.CopyTo(target); + target = target.Slice(value.Length); + totalBytes += value.Length; + } + return totalBytes; + } + + /// + /// Copy the current scalar value out into the supplied , or as much as can be copied. + /// + /// The destination for the copy operation. + /// The number of bytes successfully copied. + public readonly int CopyTo(IBufferWriter target) + { + if (TryGetSpan(out var value)) + { + target.Write(value); + return value.Length; + } + + int totalBytes = 0; + var iterator = ScalarChunks(); + while (iterator.MoveNext()) + { + value = iterator.Current; + target.Write(value); + totalBytes += value.Length; + } + return totalBytes; + } + + /// + /// Asserts that the current element is not null. + /// + public void DemandNotNull() + { + if (IsNull) Throw(); + static void Throw() => throw new InvalidOperationException("A non-null element was expected"); + } + + /// + /// Read the current element as a value. + /// + [SuppressMessage("Style", "IDE0018:Inline variable declaration", Justification = "No it can't - conditional")] + public readonly long ReadInt64() + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesInt64 + 1]); + long value; + if (!(span.Length <= RespConstants.MaxRawBytesInt64 + && Utf8Parser.TryParse(span, out value, out int bytes) + && bytes == span.Length)) + { + ThrowFormatException(); + value = 0; + } + return value; + } + + /// + /// Try to read the current element as a value. + /// + public readonly bool TryReadInt64(out long value) + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesInt64 + 1]); + if (span.Length <= RespConstants.MaxRawBytesInt64) + { + return Utf8Parser.TryParse(span, out value, out int bytes) & bytes == span.Length; + } + + value = 0; + return false; + } + + /// + /// Read the current element as a value. + /// + [SuppressMessage("Style", "IDE0018:Inline variable declaration", Justification = "No it can't - conditional")] + public readonly int ReadInt32() + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesInt32 + 1]); + int value; + if (!(span.Length <= RespConstants.MaxRawBytesInt32 + && Utf8Parser.TryParse(span, out value, out int bytes) + && bytes == span.Length)) + { + ThrowFormatException(); + value = 0; + } + return value; + } + + /// + /// Try to read the current element as a value. + /// + public readonly bool TryReadInt32(out int value) + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesInt32 + 1]); + if (span.Length <= RespConstants.MaxRawBytesInt32) + { + return Utf8Parser.TryParse(span, out value, out int bytes) & bytes == span.Length; + } + + value = 0; + return false; + } + + /// + /// Read the current element as a value. + /// + public readonly double ReadDouble() + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesNumber + 1]); + + if (span.Length <= RespConstants.MaxRawBytesNumber + && Utf8Parser.TryParse(span, out double value, out int bytes) + && bytes == span.Length) + { + return value; + } + switch (span.Length) + { + case 3 when "inf"u8.SequenceEqual(span): + return double.PositiveInfinity; + case 3 when "nan"u8.SequenceEqual(span): + return double.NaN; + case 4 when "+inf"u8.SequenceEqual(span): // not actually mentioned in spec, but: we'll allow it + return double.PositiveInfinity; + case 4 when "-inf"u8.SequenceEqual(span): + return double.NegativeInfinity; + } + ThrowFormatException(); + return 0; + } + + /// + /// Try to read the current element as a value. + /// + public bool TryReadDouble(out double value, bool allowTokens = true) + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesNumber + 1]); + + if (span.Length <= RespConstants.MaxRawBytesNumber + && Utf8Parser.TryParse(span, out value, out int bytes) + && bytes == span.Length) + { + return true; + } + + if (allowTokens) + { + switch (span.Length) + { + case 3 when "inf"u8.SequenceEqual(span): + value = double.PositiveInfinity; + return true; + case 3 when "nan"u8.SequenceEqual(span): + value = double.NaN; + return true; + case 4 when "+inf"u8.SequenceEqual(span): // not actually mentioned in spec, but: we'll allow it + value = double.PositiveInfinity; + return true; + case 4 when "-inf"u8.SequenceEqual(span): + value = double.NegativeInfinity; + return true; + } + } + + value = 0; + return false; + } + + internal readonly bool TryReadShortAscii(out string value) + { + const int ShortLength = 31; + + var span = Buffer(stackalloc byte[ShortLength + 1]); + value = ""; + if (span.IsEmpty) return true; + + if (span.Length <= ShortLength) + { + // check for anything that looks binary or unicode + foreach (var b in span) + { + // allow [SPACE]-thru-[DEL], plus CR/LF + if (!(b < 127 & (b >= 32 | (b is 12 or 13)))) + { + return false; + } + } + + value = Encoding.UTF8.GetString(span); + return true; + } + + return false; + } + + /// + /// Read the current element as a value. + /// + [SuppressMessage("Style", "IDE0018:Inline variable declaration", Justification = "No it can't - conditional")] + public readonly decimal ReadDecimal() + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesNumber + 1]); + decimal value; + if (!(span.Length <= RespConstants.MaxRawBytesNumber + && Utf8Parser.TryParse(span, out value, out int bytes) + && bytes == span.Length)) + { + ThrowFormatException(); + value = 0; + } + return value; + } + + /// + /// Read the current element as a value. + /// + public readonly bool ReadBoolean() + { + var span = Buffer(stackalloc byte[2]); + if (span.Length == 1) + { + switch (span[0]) + { + case (byte)'0' when Prefix == RespPrefix.Integer: return false; + case (byte)'1' when Prefix == RespPrefix.Integer: return true; + case (byte)'f' when Prefix == RespPrefix.Boolean: return false; + case (byte)'t' when Prefix == RespPrefix.Boolean: return true; + } + } + ThrowFormatException(); + return false; + } + + /// + /// Parse a scalar value as an enum of type . + /// + /// The value to report if the value is not recognized. + /// The type of enum being parsed. + public readonly T ReadEnum(T unknownValue = default) where T : struct, Enum + { +#if NET6_0_OR_GREATER + return ParseChars(static (chars, state) => Enum.TryParse(chars, true, out T value) ? value : state, unknownValue); +#else + return Enum.TryParse(ReadString(), true, out T value) ? value : unknownValue; +#endif + } + + public T[]? ReadArray(Projection projection) + { + DemandAggregate(); + if (IsNull) return null; + var len = AggregateLength(); + if (len == 0) return []; + T[] result = new T[len]; + FillAll(result, projection); + return result; + } +} diff --git a/src/RESP.Core/RespReaderExtensions.cs b/src/RESP.Core/RespReaderExtensions.cs new file mode 100644 index 000000000..37e013dc9 --- /dev/null +++ b/src/RESP.Core/RespReaderExtensions.cs @@ -0,0 +1,142 @@ +// using System; +// using System.Buffers; +// using System.Diagnostics; +// +// namespace Resp; +// +// /// +// /// Utility methods for s. +// /// +// internal static class RespReaderExtensions +// { +// public static RedisValue ReadRedisValue(in this RespReader reader) +// { +// reader.DemandScalar(); +// if (reader.IsNull) return RedisValue.Null; +// +// var len = reader.ScalarLength(); +// switch (reader.Prefix) +// { +// case RespPrefix.Boolean: +// return reader.ReadBoolean(); +// case RespPrefix.Integer: +// return reader.ReadInt64(); +// case RespPrefix.Double: +// return reader.ReadDouble(); +// } +// +// if (len == 0) return RedisValue.EmptyString; +// +// // try to be efficient with obvious numbers and short strings +// if (reader.TryReadInt64(out var i64)) +// { +// return i64; +// } +// +// if (reader.TryReadDouble(out var f64, allowTokens: false)) +// { +// return f64; +// } +// +// if (reader.TryReadShortAscii(out var s)) +// { +// return s; +// } +// +// // otherwise, copy out the blob +// var result = new byte[len]; +// int actual = reader.CopyTo(result); +// Debug.Assert(actual == len); +// return result; +// } +// +// public static RedisKey ReadRedisKey(in this RespReader reader) +// { +// reader.DemandScalar(); +// if (reader.IsNull) return RedisKey.Null; +// +// var len = reader.ScalarLength(); +// if (len == 0) return ""; +// +// if (reader.TryReadShortAscii(out var s)) +// { +// return s; +// } +// +// // copy out the blob +// var result = new byte[len]; +// int actual = reader.CopyTo(result); +// Debug.Assert(actual == len); +// return result; +// } +// +// /* +// +// /// +// /// Interpret a scalar value as a value. +// /// +// public static LeasedString ReadLeasedString(in this RespReader reader) +// { +// if (reader.TryGetSpan(out var span)) return reader.IsNull ? default : new LeasedString(span); +// +// var len = reader.ScalarLength(); +// var result = new LeasedString(len, out var memory); +// int actual = reader.CopyTo(memory.Span); +// Debug.Assert(actual == len); +// return result; +// } +// +// /// +// /// Interpret an aggregate value as a value. +// /// +// public static LeasedStrings ReadLeasedStrings(in this RespReader reader) +// { +// Debug.Assert(reader.IsAggregate, "should have already checked for aggregate"); +// reader.DemandAggregate(); +// if (reader.IsNull) return default; +// +// int count = 0, bytes = 0; +// foreach (var child in reader.AggregateChildren()) +// { +// count++; +// bytes += child.ScalarLength(); +// } +// if (count == 0) return LeasedStrings.Empty; +// +// var builder = new LeasedStrings.Builder(count, bytes); +// foreach (var child in reader.AggregateChildren()) +// { +// if (child.IsNull) +// { +// builder.AddNull(); +// } +// else +// { +// var len = child.ScalarLength(); +// var span = builder.Add(len); +// child.CopyTo(span); +// } +// } +// return builder.Create(); +// } +// +// /// +// /// Indicates whether the given value is an byte match. +// /// +// public static bool Is(in this RespReader reader, in SimpleString value) +// { +// if (value.TryGetBytes(span: out var span)) +// { +// return reader.Is(span) & reader.IsNull == value.IsNull; +// } +// +// var len = value.GetByteCount(); +// var oversized = ArrayPool.Shared.Rent(len); +// var actual = value.CopyTo(oversized); +// Debug.Assert(actual == len); +// var result = reader.Is(new ReadOnlySpan(oversized, 0, len)); +// ArrayPool.Shared.Return(oversized); +// return result; +// } +// */ +// } diff --git a/src/RESP.Core/RespReaders.cs b/src/RESP.Core/RespReaders.cs new file mode 100644 index 000000000..58cbac2ea --- /dev/null +++ b/src/RESP.Core/RespReaders.cs @@ -0,0 +1,341 @@ +// using System.Buffers; +// using System.Diagnostics.CodeAnalysis; +// using System.Runtime.CompilerServices; +// using RESPite.Messages; +// using static RESPite.Resp.RespConstants; +// +// namespace Resp; +// +// /// +// /// Provides common RESP reader implementations. +// /// +// internal static class RespReaders +// { +// internal static readonly Impl Common = new(); +// +// /// +// /// Reads payloads. +// /// +// public static IRespReader String => Common; +// +// /// +// /// Reads payloads. +// /// +// public static IRespReader Int32 => Common; +// +// /// +// /// Reads payloads. +// /// +// public static IRespReader NullableInt32 => Common; +// +// /// +// /// Reads payloads. +// /// +// public static IRespReader Int64 => Common; +// +// /// +// /// Reads payloads. +// /// +// public static IRespReader NullableInt64 => Common; +// +// /// +// /// Reads 'OK' acknowledgements. +// /// +// public static IRespReader OK => Common; +// +// /// +// /// Reads payloads. +// /// +// public static IRespReader LeasedString => Common; +// +// /// +// /// Reads arrays of opaque payloads. +// /// +// public static IRespReader LeasedStrings => Common; +// +// internal static void ThrowMissingExpected(string expected, [CallerMemberName] string caller = "") +// => throw new InvalidOperationException($"Did not receive expected response: '{expected}'"); +// +// internal sealed class Impl : +// IRespReader, +// IRespReader, +// IRespReader, +// IRespReader, +// IRespReader, +// IRespReader, +// IRespReader, +// IRespReader, +// IRespReader +// { +// private static readonly uint OK_HiNibble = UnsafeCpuUInt32("+OK\r"u8); +// Empty IReader.Read(in Empty request, in ReadOnlySequence content) +// { +// if (content.IsSingleSegment) +// { +// #if NETCOREAPP3_1_OR_GREATER +// var span = content.FirstSpan; +// #else +// var span = content.First.Span; +// #endif +// if (span.Length != 5 || !(UnsafeCpuUInt32(span) == OK_HiNibble & UnsafeCpuByte(span, 4) == (byte)'\n')) ThrowMissingExpected("OK"); +// } +// else +// { +// Slower(content); +// } +// return default; +// +// static Empty Slower(scoped in ReadOnlySequence content) +// { +// var reader = new RespReader(content); +// reader.MoveNext(RespPrefix.SimpleString); +// if (!reader.IsOK()) ThrowMissingExpected("OK"); +// return default; +// } +// } +// +// Empty IRespReader.Read(in Empty request, ref RespReader reader) +// { +// reader.MoveNext(RespPrefix.SimpleString); +// if (!reader.IsOK()) ThrowMissingExpected("OK"); +// return default; +// } +// +// string? IRespReader.Read(in Empty request, ref RespReader reader) +// { +// reader.MoveNextScalar(); +// return reader.ReadString(); +// } +// +// string? IReader.Read(in Empty request, in ReadOnlySequence content) +// { +// var reader = new RespReader(in content); +// reader.MoveNextScalar(); +// return reader.ReadString(); +// } +// +// long IReader.Read(in Empty request, in ReadOnlySequence content) +// { +// if (content.IsSingleSegment && content.Length <= 12) // 9 chars for pre-billion integers, plus 3 protocol chars +// { +// return ((IReader)this).Read(request, content); +// } +// var reader = new RespReader(in content); +// reader.MoveNextScalar(); +// reader.DemandNotNull(); +// return reader.ReadInt64(); +// } +// +// long? IReader.Read(in Empty request, in ReadOnlySequence content) +// { +// if (content.IsSingleSegment && content.Length <= 12) // 9 chars for pre-billion integers, plus 3 protocol chars +// { +// return ((IReader)this).Read(request, content); +// } +// var reader = new RespReader(in content); +// reader.MoveNextScalar(); +// return reader.IsNull ? null : reader.ReadInt64(); +// } +// +// long IRespReader.Read(in Empty request, ref RespReader reader) +// { +// reader.MoveNextScalar(); +// reader.DemandNotNull(); +// return reader.ReadInt64(); +// } +// +// long? IRespReader.Read(in Empty request, ref RespReader reader) +// { +// reader.MoveNextScalar(); +// return reader.IsNull ? null : reader.ReadInt64(); +// } +// +// int IRespReader.Read(in Empty request, ref RespReader reader) +// { +// reader.MoveNextScalar(); +// reader.DemandNotNull(); +// return reader.ReadInt32(); +// } +// +// int? IRespReader.Read(in Empty request, ref RespReader reader) +// { +// reader.MoveNextScalar(); +// return reader.IsNull ? null : reader.ReadInt32(); +// } +// +// LeasedString IReader.Read(in Empty request, in ReadOnlySequence content) +// { +// var reader = new RespReader(in content); +// reader.MoveNextScalar(); +// return reader.ReadLeasedString(); +// } +// +// LeasedString IRespReader.Read(in Empty request, ref RespReader reader) +// { +// reader.MoveNextScalar(); +// return reader.ReadLeasedString(); +// } +// +// bool IReader.Read(in Empty request, in ReadOnlySequence content) +// { +// var reader = new RespReader(in content); +// reader.MoveNextScalar(); +// return reader.IsOK() || reader.Is((byte)'1'); +// } +// +// bool IRespReader.Read(in Empty request, ref RespReader reader) +// { +// reader.MoveNextScalar(); +// return reader.IsOK() || reader.Is((byte)'1'); +// } +// +// private static bool TryReadFastInt32(ReadOnlySpan span, out int value) +// { +// switch (span.Length) +// { +// case 4: // :N\r\n +// if ((UnsafeCpuUInt32(span) & SingleCharScalarMask) == SingleDigitInteger) +// { +// value = Digit(UnsafeCpuByte(span, 1)); +// return true; +// } +// break; +// case 5: // :NN\r\n +// if ((UnsafeCpuUInt32(span) & DoubleCharScalarMask) == DoubleDigitInteger +// & UnsafeCpuByte(span, 4) == (byte)'\n') +// { +// value = (10 * Digit(UnsafeCpuByte(span, 1))) +// + Digit(UnsafeCpuByte(span, 2)); +// return true; +// } +// break; +// case 7: // $1\r\nN\r\n +// if (UnsafeCpuUInt32(span) == BulkSingleDigitPrefix +// && UnsafeCpuUInt16(span, 5) == CrLfUInt16) +// { +// value = Digit(UnsafeCpuByte(span, 4)); +// return true; +// } +// break; +// case 8: // $2\r\nNN\r\n +// if (UnsafeCpuUInt32(span) == BulkDoubleDigitPrefix +// && UnsafeCpuUInt16(span, 6) == CrLfUInt16) +// { +// value = (10 * Digit(UnsafeCpuByte(span, 4))) +// + Digit(UnsafeCpuByte(span, 5)); +// return true; +// } +// break; +// } +// value = default; +// return false; +// +// static int Digit(byte value) +// { +// var i = value - '0'; +// if (i < 0 | i > 9) ThrowFormat(); +// return i; +// } +// } +// +// int IReader.Read(in Empty request, in ReadOnlySequence content) +// { +// if (content.IsSingleSegment) +// { +// #if NETCOREAPP3_1_OR_GREATER +// var span = content.FirstSpan; +// #else +// var span = content.First.Span; +// #endif +// if (TryReadFastInt32(span, out int i)) return i; +// } +// var reader = new RespReader(in content); +// reader.MoveNextScalar(); +// reader.DemandNotNull(); +// return reader.ReadInt32(); +// } +// +// int? IReader.Read(in Empty request, in ReadOnlySequence content) +// { +// if (content.IsSingleSegment) +// { +// #if NETCOREAPP3_1_OR_GREATER +// var span = content.FirstSpan; +// #else +// var span = content.First.Span; +// #endif +// if (TryReadFastInt32(span, out int i)) return i; +// } +// var reader = new RespReader(in content); +// reader.MoveNextScalar(); +// return reader.IsNull ? null : reader.ReadInt32(); +// } +// +// LeasedStrings IReader.Read(in Empty request, in ReadOnlySequence content) +// { +// var reader = new RespReader(in content); +// reader.MoveNextAggregate(); +// return reader.ReadLeasedStrings(); +// } +// +// LeasedStrings IRespReader.Read(in Empty request, ref RespReader reader) +// { +// reader.MoveNextAggregate(); +// return reader.ReadLeasedStrings(); +// } +// +// private static readonly uint +// SingleCharScalarMask = CpuUInt32(0xFF00FFFF), +// DoubleCharScalarMask = CpuUInt32(0xFF0000FF), +// SingleDigitInteger = UnsafeCpuUInt32(":\0\r\n"u8), +// DoubleDigitInteger = UnsafeCpuUInt32(":\0\0\r"u8), +// BulkSingleDigitPrefix = UnsafeCpuUInt32("$1\r\n"u8), +// BulkDoubleDigitPrefix = UnsafeCpuUInt32("$2\r\n"u8); +// } +// +// /// +// /// Reads values as an enum of type . +// /// +// public sealed class EnumReader : IRespReader, IRespReader where T : struct, Enum +// { +// /// +// /// Gets the reader instance. +// /// +// public static EnumReader Instance { get; } = new(); +// +// private EnumReader() +// { +// } +// +// T IReader.Read(in Empty request, in ReadOnlySequence content) +// { +// RespReader reader = new(content); +// reader.MoveNextScalar(); +// reader.DemandNotNull(); +// return reader.ReadEnum(default); +// } +// +// T? IReader.Read(in Empty request, in ReadOnlySequence content) +// { +// RespReader reader = new(content); +// reader.MoveNextScalar(); +// return reader.IsNull ? null : reader.ReadEnum(default); +// } +// +// T IRespReader.Read(in Empty request, ref RespReader reader) +// { +// reader.MoveNextScalar(); +// reader.DemandNotNull(); +// return reader.ReadEnum(default); +// } +// +// T? IRespReader.Read(in Empty request, ref RespReader reader) +// { +// reader.MoveNextScalar(); +// return reader.IsNull ? null : reader.ReadEnum(default); +// } +// } +// +// [DoesNotReturn, MethodImpl(MethodImplOptions.NoInlining)] +// private static void ThrowFormat() => throw new FormatException(); +// } diff --git a/src/RESP.Core/RespScanState.cs b/src/RESP.Core/RespScanState.cs new file mode 100644 index 000000000..7eba5d8be --- /dev/null +++ b/src/RESP.Core/RespScanState.cs @@ -0,0 +1,161 @@ +using System; +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Resp; + +/// +/// Holds state used for RESP frame parsing, i.e. detecting the RESP for an entire top-level message. +/// +public struct RespScanState +{ + /* + The key point of ScanState is to skim over a RESP stream with minimal frame processing, to find the + end of a single top-level RESP message. We start by expecting 1 message, and then just read, with the + rules that the end of a message subtracts one, and aggregates add N. Streaming scalars apply zero offset + until the scalar stream terminator. Attributes also apply zero offset. + Note that streaming aggregates change the rules - when at least one streaming aggregate is in effect, + no offsets are applied until we get back out of the outermost streaming aggregate - we achieve this + by simply counting the streaming aggregate depth, which is usually zero. + Note that in reality streaming (scalar and aggregates) and attributes are non-existent; in addition + to being specific to RESP3, no known server currently implements these parts of the RESP3 specification, + so everything here is theoretical, but: works according to the spec. + */ + private int _delta; // when this becomes -1, we have fully read a top-level message; + private ushort _streamingAggregateDepth; + private RespPrefix _prefix; + + public RespPrefix Prefix => _prefix; + + private long _totalBytes; +#if DEBUG + private int _elementCount; + + /// + public override string ToString() => $"{_prefix}, consumed: {_totalBytes} bytes, {_elementCount} nodes, complete: {IsComplete}"; +#else + /// + public override string ToString() => _prefix.ToString(); +#endif + + /// + public override bool Equals([NotNullWhen(true)] object? obj) => throw new NotSupportedException(); + + /// + public override int GetHashCode() => throw new NotSupportedException(); + + /// + /// Gets whether an entire top-level RESP message has been consumed. + /// + public bool IsComplete => _delta == -1; + + /// + /// Gets the total length of the payload read (or read so far, if it is not yet complete); this combines payloads from multiple + /// TryRead operations. + /// + public long TotalBytes => _totalBytes; + + // used when spotting common replies - we entirely bypass the usual reader/delta mechanism + internal void SetComplete(int totalBytes, RespPrefix prefix) + { + _totalBytes = totalBytes; + _delta = -1; + _prefix = prefix; +#if DEBUG + _elementCount = 1; +#endif + } + + /// + /// The amount of data, in bytes, to read before attempting to read the next frame. + /// + public const int MinBytes = 3; // minimum legal RESP frame is: _\r\n + + /// + /// Create a new value that can parse the supplied node (and subtree). + /// + internal RespScanState(in RespReader reader) + { + Debug.Assert(reader.Prefix != RespPrefix.None, "missing RESP prefix"); + _totalBytes = 0; + _delta = reader.GetInitialScanCount(out _streamingAggregateDepth); + } + + /// + /// Scan as far as possible, stopping when an entire top-level RESP message has been consumed or the data is exhausted. + /// + /// True if a top-level RESP message has been consumed. + public bool TryRead(ref RespReader reader, out long bytesRead) + { + bytesRead = ReadCore(ref reader, reader.BytesConsumed); + return IsComplete; + } + + /// + /// Scan as far as possible, stopping when an entire top-level RESP message has been consumed or the data is exhausted. + /// + /// True if a top-level RESP message has been consumed. + public bool TryRead(ReadOnlySpan value, out int bytesRead) + { + var reader = new RespReader(value); + bytesRead = (int)ReadCore(ref reader); + return IsComplete; + } + + /// + /// Scan as far as possible, stopping when an entire top-level RESP message has been consumed or the data is exhausted. + /// + /// True if a top-level RESP message has been consumed. + public bool TryRead(in ReadOnlySequence value, out long bytesRead) + { + var reader = new RespReader(in value); + bytesRead = ReadCore(ref reader); + return IsComplete; + } + + /// + /// Scan as far as possible, stopping when an entire top-level RESP message has been consumed or the data is exhausted. + /// + /// The number of bytes consumed in this operation. + private long ReadCore(ref RespReader reader, long startOffset = 0) + { + while (_delta >= 0 && reader.TryReadNext()) + { +#if DEBUG + _elementCount++; +#endif + if (!reader.IsAttribute & _prefix == RespPrefix.None) + { + _prefix = reader.Prefix; + } + + if (reader.IsAggregate) ApplyAggregateRules(ref reader); + + if (_streamingAggregateDepth == 0) _delta += reader.Delta(); + } + + var bytesRead = reader.BytesConsumed - startOffset; + _totalBytes += bytesRead; + return bytesRead; + } + + private void ApplyAggregateRules(ref RespReader reader) + { + Debug.Assert(reader.IsAggregate, "RESP aggregate expected"); + if (reader.IsStreaming) + { + // entering an aggregate stream + if (_streamingAggregateDepth == ushort.MaxValue) ThrowTooDeep(); + _streamingAggregateDepth++; + } + else if (reader.Prefix == RespPrefix.StreamTerminator) + { + // exiting an aggregate stream + if (_streamingAggregateDepth == 0) ThrowUnexpectedTerminator(); + _streamingAggregateDepth--; + } + static void ThrowTooDeep() => throw new InvalidOperationException("Maximum streaming aggregate depth exceeded."); + static void ThrowUnexpectedTerminator() => throw new InvalidOperationException("Unexpected streaming aggregate terminator."); + } +} diff --git a/src/RESP.Core/RespWriter.cs b/src/RESP.Core/RespWriter.cs new file mode 100644 index 000000000..db10d381d --- /dev/null +++ b/src/RESP.Core/RespWriter.cs @@ -0,0 +1,913 @@ +using System; +using System.Buffers; +using System.Buffers.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace Resp; + +/// +/// Provides low-level RESP formatting operations. +/// +public ref struct RespWriter +{ + private readonly IBufferWriter? _target; + + [SuppressMessage("Style", "IDE0032:Use auto property", Justification = "Clarity")] + private int _index; + + internal readonly int IndexInCurrentBuffer => _index; + +#if NET7_0_OR_GREATER + private ref byte StartOfBuffer; + private int BufferLength; + + private ref byte WriteHead + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.Add(ref StartOfBuffer, _index); + } + + private Span Tail + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => MemoryMarshal.CreateSpan(ref Unsafe.Add(ref StartOfBuffer, _index), BufferLength - _index); + } + + private void WriteRawUnsafe(byte value) => Unsafe.Add(ref StartOfBuffer, _index++) = value; + + private readonly ReadOnlySpan WrittenLocalBuffer => + MemoryMarshal.CreateReadOnlySpan(ref StartOfBuffer, _index); +#else + private Span _buffer; + private readonly int BufferLength => _buffer.Length; + + private readonly ref byte StartOfBuffer + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref MemoryMarshal.GetReference(_buffer); + } + + private readonly ref byte WriteHead + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.Add(ref MemoryMarshal.GetReference(_buffer), _index); + } + + private readonly Span Tail => _buffer.Slice(_index); + private void WriteRawUnsafe(byte value) => _buffer[_index++] = value; + + private readonly ReadOnlySpan WrittenLocalBuffer => _buffer.Slice(0, _index); +#endif + + internal readonly string DebugBuffer() => RespConstants.UTF8.GetString(WrittenLocalBuffer); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteCrLfUnsafe() + { + Unsafe.WriteUnaligned(ref WriteHead, RespConstants.CrLfUInt16); + _index += 2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteCrLf() + { + if (Available >= 2) + { + Unsafe.WriteUnaligned(ref WriteHead, RespConstants.CrLfUInt16); + _index += 2; + } + else + { + WriteRaw(RespConstants.CrlfBytes); + } + } + + private readonly int Available + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => BufferLength - _index; + } + + /// + /// Create a new RESP writer over the provided target. + /// + public RespWriter(IBufferWriter target) + { + _target = target; + _index = 0; +#if NET7_0_OR_GREATER + StartOfBuffer = ref Unsafe.NullRef(); + BufferLength = 0; +#else + _buffer = default; +#endif + GetBuffer(); + } + + /// + /// Create a new RESP writer over the provided target. + /// + public RespWriter(Span target) + { + _index = 0; +#if NET7_0_OR_GREATER + BufferLength = target.Length; + StartOfBuffer = ref MemoryMarshal.GetReference(target); +#else + _buffer = target; +#endif + } + + /// + /// Commits any unwritten bytes to the output. + /// + public void Flush() + { + if (_index != 0 && _target is not null) + { + _target.Advance(_index); +#if NET7_0_OR_GREATER + _index = BufferLength = 0; + StartOfBuffer = ref Unsafe.NullRef(); +#else + _index = 0; + _buffer = default; +#endif + } + } + + private void FlushAndGetBuffer(int sizeHint) + { + Flush(); + GetBuffer(sizeHint); + } + + private void GetBuffer(int sizeHint = 128) + { + if (Available == 0) + { + if (_target is null) + { + ThrowFixedBufferExceeded(); + } + else + { + const int MIN_BUFFER = 1024; + _index = 0; +#if NET7_0_OR_GREATER + var span = _target.GetSpan(Math.Max(sizeHint, MIN_BUFFER)); + BufferLength = span.Length; + StartOfBuffer = ref MemoryMarshal.GetReference(span); +#else + _buffer = _target.GetSpan(Math.Max(sizeHint, MIN_BUFFER)); +#endif + ActivationHelper.DebugBreakIf(Available == 0); + } + } + } + + [DoesNotReturn, MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowFixedBufferExceeded() => + throw new InvalidOperationException("Fixed buffer cannot be expanded"); + + /// + /// Write raw RESP data to the output; no validation will occur. + /// + public void WriteRaw(scoped ReadOnlySpan buffer) + { + const int MAX_TO_DOUBLE_BUFFER = 128; + if (buffer.Length <= MAX_TO_DOUBLE_BUFFER && buffer.Length <= Available) + { + buffer.CopyTo(Tail); + _index += buffer.Length; + } + else + { + // write directly to the output + Flush(); + if (_target is null) + { + ThrowFixedBufferExceeded(); + } + else + { + _target.Write(buffer); + } + } + } + + public RespCommandMap? CommandMap { get; set; } + + /// + /// Write a command header. + /// + /// The command name to write. + /// The number of arguments for the command (excluding the command itself). + public void WriteCommand(scoped ReadOnlySpan command, int args) + { + if (args < 0) Throw(); + WritePrefixedInteger(RespPrefix.Array, args + 1); + if (command.IsEmpty) ThrowEmptyCommand(); + if (CommandMap is { } map) + { + var mapped = map.Map(command); + if (mapped.IsEmpty) ThrowCommandUnavailable(command); + command = mapped; + } + + WriteBulkString(command); + + static void Throw() => throw new ArgumentOutOfRangeException(nameof(args)); + + static void ThrowEmptyCommand() => + throw new ArgumentException(paramName: nameof(command), message: "Empty command specified."); + + static void ThrowCommandUnavailable(ReadOnlySpan command) + => throw new ArgumentException( + paramName: nameof(command), + message: $"The command {Encoding.UTF8.GetString(command)} is not available."); + } + + /// + /// Write a key as a bulk string. + /// + /// The key to write. + public void WriteKey(scoped ReadOnlySpan value) => WriteBulkString(value); + + /// + /// Write a key as a bulk string. + /// + /// The key to write. + public void WriteKey(ReadOnlyMemory value) => WriteBulkString(value.Span); + + /// + /// Write a key as a bulk string. + /// + /// The key to write. + public void WriteKey(scoped ReadOnlySpan value) => WriteBulkString(value); + + /// + /// Write a key as a bulk string. + /// + /// The key to write. + public void WriteKey(ReadOnlyMemory value) => WriteBulkString(value.Span); + + /// + /// Write a key as a bulk string. + /// + /// The key to write. + public void WriteKey(string value) => WriteBulkString(value); + + /// + /// Write a key as a bulk string. + /// + /// The key to write. + public void WriteKey(byte[] value) => WriteBulkString(value.AsSpan()); + + /// + /// Write a payload as a bulk string. + /// + /// The payload to write. + public void WriteBulkString(byte[] value) => WriteBulkString(value.AsSpan()); + + /// + /// Write a payload as a bulk string. + /// + /// The payload to write. + public void WriteBulkString(ReadOnlyMemory value) + => WriteBulkString(value.Span); + + /// + /// Write a payload as a bulk string. + /// + /// The payload to write. + public void WriteBulkString(scoped ReadOnlySpan value) + { + if (value.IsEmpty) + { + if (Available >= 6) + { + WriteRawPrechecked(Raw.BulkStringEmpty_6, 6); + } + else + { + WriteRaw("$0\r\n\r\n"u8); + } + } + else + { + WriteBulkStringHeader(value.Length); + if (Available >= value.Length + 2) + { + value.CopyTo(Tail); + _index += value.Length; + WriteCrLfUnsafe(); + } + else + { + // slow path + WriteRaw(value); + WriteCrLf(); + } + } + } + + /* + /// + /// Write a payload as a bulk string. + /// + /// The payload to write. + public void WriteBulkString(in SimpleString value) + { + if (value.IsEmpty) + { + WriteRaw("$0\r\n\r\n"u8); + } + else if (value.TryGetBytes(span: out var bytes)) + { + WriteBulkString(bytes); + } + else if (value.TryGetChars(span: out var chars)) + { + WriteBulkString(chars); + } + else if (value.TryGetBytes(sequence: out var bytesSeq)) + { + WriteBulkString(bytesSeq); + } + else if (value.TryGetChars(sequence: out var charsSeq)) + { + WriteBulkString(charsSeq); + } + else + { + Throw(); + } + + static void Throw() => throw new InvalidOperationException($"It was not possible to read the {nameof(SimpleString)} contents"); + } + */ + + /// + /// Write an integer as a bulk string. + /// + public void WriteBulkString(bool value) => WriteBulkString(value ? 1 : 0); + + /// + /// Write a floating point as a bulk string. + /// + public void WriteBulkString(double value) + { + if (value == 0.0 | double.IsNaN(value) | double.IsInfinity(value)) + { + WriteKnownDouble(ref this, value); + + static void WriteKnownDouble(ref RespWriter writer, double value) + { + if (value == 0.0) + { + writer.WriteRaw("$1\r\n0\r\n"u8); + } + else if (double.IsNaN(value)) + { + writer.WriteRaw("$3\r\nnan\r\n"u8); + } + else if (double.IsPositiveInfinity(value)) + { + writer.WriteRaw("$3\r\ninf\r\n"u8); + } + else if (double.IsNegativeInfinity(value)) + { + writer.WriteRaw("$4\r\n-inf\r\n"u8); + } + else + { + Throw(); + static void Throw() => throw new ArgumentOutOfRangeException(nameof(value)); + } + } + } + else + { + Debug.Assert(RespConstants.MaxProtocolBytesBytesNumber <= 32); + Span scratch = stackalloc byte[24]; + if (!Utf8Formatter.TryFormat(value, scratch, out int bytes, G17)) + ThrowFormatException(); + WritePrefixedInteger(RespPrefix.BulkString, bytes); + WriteRaw(scratch.Slice(0, bytes)); + WriteCrLf(); + } + } + + private static readonly StandardFormat G17 = new('G', 17); + + /// + /// Write an integer as a bulk string. + /// + public void WriteBulkString(long value) + { + if (value >= -1 & value <= 20) + { + WriteRaw(value switch + { + -1 => "$2\r\n-1\r\n"u8, + 0 => "$1\r\n0\r\n"u8, + 1 => "$1\r\n1\r\n"u8, + 2 => "$1\r\n2\r\n"u8, + 3 => "$1\r\n3\r\n"u8, + 4 => "$1\r\n4\r\n"u8, + 5 => "$1\r\n5\r\n"u8, + 6 => "$1\r\n6\r\n"u8, + 7 => "$1\r\n7\r\n"u8, + 8 => "$1\r\n8\r\n"u8, + 9 => "$1\r\n9\r\n"u8, + 10 => "$2\r\n10\r\n"u8, + 11 => "$2\r\n11\r\n"u8, + 12 => "$2\r\n12\r\n"u8, + 13 => "$2\r\n13\r\n"u8, + 14 => "$2\r\n14\r\n"u8, + 15 => "$2\r\n15\r\n"u8, + 16 => "$2\r\n16\r\n"u8, + 17 => "$2\r\n17\r\n"u8, + 18 => "$2\r\n18\r\n"u8, + 19 => "$2\r\n19\r\n"u8, + 20 => "$2\r\n20\r\n"u8, + _ => Throw(), + }); + + static ReadOnlySpan Throw() => throw new ArgumentOutOfRangeException(nameof(value)); + } + else if (Available >= RespConstants.MaxProtocolBytesBulkStringIntegerInt64) + { + var singleDigit = value >= -99_999_999 && value <= 999_999_999; + WriteRawUnsafe((byte)RespPrefix.BulkString); + + var target = Tail.Slice(singleDigit ? 3 : 4); // N\r\n or NN\r\n + if (!Utf8Formatter.TryFormat(value, target, out var valueBytes)) + ThrowFormatException(); + + Debug.Assert(valueBytes > 0 && singleDigit ? valueBytes < 10 : valueBytes is 10 or 11); + if (!Utf8Formatter.TryFormat(valueBytes, Tail, out var prefixBytes)) + ThrowFormatException(); + Debug.Assert(prefixBytes == (singleDigit ? 1 : 2)); + _index += prefixBytes; + WriteCrLfUnsafe(); + _index += valueBytes; + WriteCrLfUnsafe(); + } + else + { + Debug.Assert(RespConstants.MaxRawBytesInt64 <= 24); + Span scratch = stackalloc byte[24]; + if (!Utf8Formatter.TryFormat(value, scratch, out int bytes)) + ThrowFormatException(); + WritePrefixedInteger(RespPrefix.BulkString, bytes); + WriteRaw(scratch.Slice(0, bytes)); + WriteCrLf(); + } + } + + private static void ThrowFormatException() => throw new FormatException(); + + private void WritePrefixedInteger(RespPrefix prefix, int length) + { + if (Available >= RespConstants.MaxProtocolBytesIntegerInt32) + { + WriteRawUnsafe((byte)prefix); + if (length >= 0 & length <= 9) + { + WriteRawUnsafe((byte)(length + '0')); + } + else + { + if (!Utf8Formatter.TryFormat(length, Tail, out var bytesWritten)) + { + ThrowFormatException(); + } + + _index += bytesWritten; + } + + WriteCrLfUnsafe(); + } + else + { + WriteViaStack(ref this, prefix, length); + } + + static void WriteViaStack(ref RespWriter respWriter, RespPrefix prefix, int length) + { + Debug.Assert(RespConstants.MaxProtocolBytesIntegerInt32 <= 16); + Span buffer = stackalloc byte[16]; + buffer[0] = (byte)prefix; + int payloadLength; + if (length >= 0 & length <= 9) + { + buffer[1] = (byte)(length + '0'); + payloadLength = 1; + } + else if (!Utf8Formatter.TryFormat(length, buffer.Slice(1), out payloadLength)) + { + ThrowFormatException(); + } + + Unsafe.WriteUnaligned(ref buffer[payloadLength + 1], RespConstants.CrLfUInt16); + respWriter.WriteRaw(buffer.Slice(0, payloadLength + 3)); + } + + bool writeToStack = Available < RespConstants.MaxProtocolBytesIntegerInt32; + + Span target = writeToStack ? stackalloc byte[16] : Tail; + target[0] = (byte)prefix; + } + + /// + /// Write a payload as a bulk string. + /// + /// The payload to write. + public void WriteBulkString(string value) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (value is null) ThrowNull(); + WriteBulkString(value.AsSpan()); + } + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + // ReSharper disable once NotResolvedInText + private static void ThrowNull() => + throw new ArgumentNullException("value", "Null values cannot be sent from client to server"); + + internal void WriteBulkStringUnoptimized(string? value) + { + if (value is null) ThrowNull(); + if (value.Length == 0) + { + WriteRaw("$0\r\n\r\n"u8); + } + else + { + var byteCount = RespConstants.UTF8.GetByteCount(value); + WritePrefixedInteger(RespPrefix.BulkString, byteCount); + if (Available >= byteCount) + { + var actual = RespConstants.UTF8.GetBytes(value.AsSpan(), Tail); + Debug.Assert(actual == byteCount); + _index += actual; + } + else + { + WriteUtf8Slow(value.AsSpan(), byteCount); + } + + WriteCrLf(); + } + } + + /// + /// Write a payload as a bulk string. + /// + /// The payload to write. + public void WriteBulkString(ReadOnlyMemory value) => WriteBulkString(value.Span); + + /// + /// Write a payload as a bulk string. + /// + /// The payload to write. + public void WriteBulkString(scoped ReadOnlySpan value) + { + if (value.Length == 0) + { + if (Available >= 6) + { + WriteRawPrechecked(Raw.BulkStringEmpty_6, 6); + } + else + { + WriteRaw("$0\r\n\r\n"u8); + } + } + else + { + var byteCount = RespConstants.UTF8.GetByteCount(value); + WriteBulkStringHeader(byteCount); + if (Available >= 2 + byteCount) + { + var actual = RespConstants.UTF8.GetBytes(value, Tail); + Debug.Assert(actual == byteCount); + _index += actual; + WriteCrLfUnsafe(); + } + else + { + FlushAndGetBuffer(Math.Min(byteCount, MAX_BUFFER_HINT)); + if (Available >= byteCount + 2) + { + // that'll work + var actual = RespConstants.UTF8.GetBytes(value, Tail); + Debug.Assert(actual == byteCount); + _index += actual; + WriteCrLfUnsafe(); + } + else + { + WriteUtf8Slow(value, byteCount); + WriteCrLf(); + } + } + } + } + + private const int MAX_BUFFER_HINT = 64 * 1024; + + private void WriteUtf8Slow(scoped ReadOnlySpan value, int remaining) + { + var enc = _perThreadEncoder; + if (enc is null) + { + enc = _perThreadEncoder = RespConstants.UTF8.GetEncoder(); + } + else + { + enc.Reset(); + } + + bool completed; + int charsUsed, bytesUsed; + do + { + enc.Convert(value, Tail, false, out charsUsed, out bytesUsed, out completed); + value = value.Slice(charsUsed); + _index += bytesUsed; + remaining -= bytesUsed; + FlushAndGetBuffer(Math.Min(remaining, MAX_BUFFER_HINT)); + } + // until done... + while (!completed); + + if (remaining != 0) + { + // any trailing data? + FlushAndGetBuffer(Math.Min(remaining, MAX_BUFFER_HINT)); + enc.Convert(value, Tail, true, out charsUsed, out bytesUsed, out completed); + Debug.Assert(charsUsed == 0 && completed); + _index += bytesUsed; + remaining -= bytesUsed; + } + + enc.Reset(); + Debug.Assert(remaining == 0); + } + + internal void WriteBulkString(in ReadOnlySequence value) + { + if (value.IsSingleSegment) + { +#if NETCOREAPP3_0_OR_GREATER + WriteBulkString(value.FirstSpan); +#else + WriteBulkString(value.First.Span); +#endif + } + else + { + // lazy for now + int len = checked((int)value.Length); + byte[] buffer = ArrayPool.Shared.Rent(len); + value.CopyTo(buffer); + WriteBulkString(new ReadOnlySpan(buffer, 0, len)); + ArrayPool.Shared.Return(buffer); + } + } + + internal void WriteBulkString(in ReadOnlySequence value) + { + if (value.IsSingleSegment) + { +#if NETCOREAPP3_0_OR_GREATER + WriteBulkString(value.FirstSpan); +#else + WriteBulkString(value.First.Span); +#endif + } + else + { + // lazy for now + int len = checked((int)value.Length); + char[] buffer = ArrayPool.Shared.Rent(len); + value.CopyTo(buffer); + WriteBulkString(new ReadOnlySpan(buffer, 0, len)); + ArrayPool.Shared.Return(buffer); + } + } + + /// + /// Experimental. + /// + public void WriteBulkString(int value) + { + if (Available >= sizeof(ulong)) + { + switch (value) + { + case -1: + WriteRawPrechecked(Raw.BulkStringInt32_M1_8, 8); + return; + case 0: + WriteRawPrechecked(Raw.BulkStringInt32_0_7, 7); + return; + case 1: + WriteRawPrechecked(Raw.BulkStringInt32_1_7, 7); + return; + case 2: + WriteRawPrechecked(Raw.BulkStringInt32_2_7, 7); + return; + case 3: + WriteRawPrechecked(Raw.BulkStringInt32_3_7, 7); + return; + case 4: + WriteRawPrechecked(Raw.BulkStringInt32_4_7, 7); + return; + case 5: + WriteRawPrechecked(Raw.BulkStringInt32_5_7, 7); + return; + case 6: + WriteRawPrechecked(Raw.BulkStringInt32_6_7, 7); + return; + case 7: + WriteRawPrechecked(Raw.BulkStringInt32_7_7, 7); + return; + case 8: + WriteRawPrechecked(Raw.BulkStringInt32_8_7, 7); + return; + case 9: + WriteRawPrechecked(Raw.BulkStringInt32_9_7, 7); + return; + case 10: + WriteRawPrechecked(Raw.BulkStringInt32_10_8, 8); + return; + } + } + + WriteBulkStringUnoptimized(value); + } + + internal void WriteBulkStringUnoptimized(int value) + { + if (Available >= RespConstants.MaxProtocolBytesBulkStringIntegerInt32) + { + var singleDigit = value >= -99_999_999 && value <= 999_999_999; + WriteRawUnsafe((byte)RespPrefix.BulkString); + + var target = Tail.Slice(singleDigit ? 3 : 4); // N\r\n or NN\r\n + if (!Utf8Formatter.TryFormat(value, target, out var valueBytes)) + ThrowFormatException(); + + Debug.Assert(valueBytes > 0 && singleDigit ? valueBytes < 10 : valueBytes is 10 or 11); + if (!Utf8Formatter.TryFormat(valueBytes, Tail, out var prefixBytes)) + ThrowFormatException(); + Debug.Assert(prefixBytes == (singleDigit ? 1 : 2)); + _index += prefixBytes; + WriteCrLfUnsafe(); + _index += valueBytes; + WriteCrLfUnsafe(); + } + else + { + Debug.Assert(RespConstants.MaxRawBytesInt32 <= 16); + Span scratch = stackalloc byte[16]; + if (!Utf8Formatter.TryFormat(value, scratch, out int bytes)) + ThrowFormatException(); + WritePrefixedInteger(RespPrefix.BulkString, bytes); + WriteRaw(scratch.Slice(0, bytes)); + WriteCrLf(); + } + } + + /// + /// Write an array header. + /// + /// The number of elements in the array. + public void WriteArray(int count) + { + if (Available >= sizeof(uint)) + { + switch (count) + { + case 0: + WriteRawPrechecked(Raw.ArrayPrefix_0_4, 4); + return; + case 1: + WriteRawPrechecked(Raw.ArrayPrefix_1_4, 4); + return; + case 2: + WriteRawPrechecked(Raw.ArrayPrefix_2_4, 4); + return; + case 3: + WriteRawPrechecked(Raw.ArrayPrefix_3_4, 4); + return; + case 4: + WriteRawPrechecked(Raw.ArrayPrefix_4_4, 4); + return; + case 5: + WriteRawPrechecked(Raw.ArrayPrefix_5_4, 4); + return; + case 6: + WriteRawPrechecked(Raw.ArrayPrefix_6_4, 4); + return; + case 7: + WriteRawPrechecked(Raw.ArrayPrefix_7_4, 4); + return; + case 8: + WriteRawPrechecked(Raw.ArrayPrefix_8_4, 4); + return; + case 9: + WriteRawPrechecked(Raw.ArrayPrefix_9_4, 4); + return; + case 10 when Available >= sizeof(ulong): + WriteRawPrechecked(Raw.ArrayPrefix_10_5, 5); + return; + case -1: + WriteRawPrechecked(Raw.ArrayPrefix_M1_5, 5); + return; + } + } + + WritePrefixedInteger(RespPrefix.Array, count); + } + + private void WriteBulkStringHeader(int count) + { + if (Available >= sizeof(uint)) + { + switch (count) + { + case 0: + WriteRawPrechecked(Raw.BulkStringPrefix_0_4, 4); + return; + case 1: + WriteRawPrechecked(Raw.BulkStringPrefix_1_4, 4); + return; + case 2: + WriteRawPrechecked(Raw.BulkStringPrefix_2_4, 4); + return; + case 3: + WriteRawPrechecked(Raw.BulkStringPrefix_3_4, 4); + return; + case 4: + WriteRawPrechecked(Raw.BulkStringPrefix_4_4, 4); + return; + case 5: + WriteRawPrechecked(Raw.BulkStringPrefix_5_4, 4); + return; + case 6: + WriteRawPrechecked(Raw.BulkStringPrefix_6_4, 4); + return; + case 7: + WriteRawPrechecked(Raw.BulkStringPrefix_7_4, 4); + return; + case 8: + WriteRawPrechecked(Raw.BulkStringPrefix_8_4, 4); + return; + case 9: + WriteRawPrechecked(Raw.BulkStringPrefix_9_4, 4); + return; + case 10 when Available >= sizeof(ulong): + WriteRawPrechecked(Raw.BulkStringPrefix_10_5, 5); + return; + case -1 when Available >= sizeof(ulong): + WriteRawPrechecked(Raw.BulkStringPrefix_M1_5, 5); + return; + } + } + + WritePrefixedInteger(RespPrefix.BulkString, count); + } + + internal void WriteArrayUnpotimized(int count) => WritePrefixedInteger(RespPrefix.Array, count); + + private void WriteRawPrechecked(ulong value, int count) + { + Debug.Assert(Available >= sizeof(ulong)); + Debug.Assert(count >= 0 && count <= sizeof(long)); + Unsafe.WriteUnaligned(ref WriteHead, value); + _index += count; + } + + private void WriteRawPrechecked(uint value, int count) + { + Debug.Assert(Available >= sizeof(uint)); + Debug.Assert(count >= 0 && count <= sizeof(uint)); + Unsafe.WriteUnaligned(ref WriteHead, value); + _index += count; + } + + internal void DebugResetIndex() => _index = 0; + + [ThreadStatic] + // used for multi-chunk encoding + private static Encoder? _perThreadEncoder; +} diff --git a/src/RESP.Core/ResponseReader.cs b/src/RESP.Core/ResponseReader.cs new file mode 100644 index 000000000..1702f1695 --- /dev/null +++ b/src/RESP.Core/ResponseReader.cs @@ -0,0 +1,54 @@ +// using System.Buffers; +// using RESPite.Messages; +// +// namespace Resp; +// +// /// +// /// Base implementation for RESP writers that do not depend on the request parameter. +// /// +// public abstract class ResponseReader : IReader, IRespReader +// { +// TResponse IReader.Read(in Empty request, in ReadOnlySequence content) +// => Read(content); +// +// /// +// /// Read a raw RESP payload. +// /// +// public virtual TResponse Read(scoped in ReadOnlySequence content) +// { +// var reader = new RespReader(in content); +// reader.MoveNext(); +// return Read(ref reader); +// } +// +// /// +// /// Read a RESP payload via the API. +// /// +// public virtual TResponse Read(ref RespReader reader) +// => throw new NotSupportedException("A " + nameof(Read) + " overload must be overridden"); +// +// TResponse IRespReader.Read(in Empty request, ref RespReader reader) +// => Read(ref reader); +// } +// +// /// +// /// Base implementation for RESP writers that do depend on the request parameter. +// /// +// public abstract class ResponseReader : IReader, IRespReader +// { +// /// +// /// Read a raw RESP payload. +// /// +// public virtual TResponse Read(in TRequest request, in ReadOnlySequence content) +// { +// var reader = new RespReader(in content); +// reader.MoveNext(); +// return Read(in request, ref reader); +// } +// +// /// +// /// Read a RESP payload via the API. +// /// +// public virtual TResponse Read(in TRequest request, ref RespReader reader) +// => throw new NotSupportedException("A " + nameof(Read) + " overload must be overridden"); +// } diff --git a/src/RESP.Core/Void.cs b/src/RESP.Core/Void.cs new file mode 100644 index 000000000..d213c6e29 --- /dev/null +++ b/src/RESP.Core/Void.cs @@ -0,0 +1,7 @@ +namespace Resp; + +public readonly struct Void +{ + private static readonly Void _shared = default; + public static ref readonly Void Instance => ref _shared; +} diff --git a/src/RESPite.Redis/Alt/DownlevelExtensions.cs b/src/RESPite.Redis/Alt/DownlevelExtensions.cs new file mode 100644 index 000000000..e437850bc --- /dev/null +++ b/src/RESPite.Redis/Alt/DownlevelExtensions.cs @@ -0,0 +1,16 @@ +using System.Runtime.CompilerServices; +using Resp; + +namespace RESPite.Redis.Alt; // legacy fallback for down-level compilers + +/// +/// For use with older compilers that don't support byref-return, extension-everything, etc. +/// +public static class DownlevelExtensions +{ + public static RedisStrings AsStrings(this in RespContext context) + => Unsafe.As(ref Unsafe.AsRef(in context)); + + public static RedisKeys AsKeys(this in RespContext context) + => Unsafe.As(ref Unsafe.AsRef(in context)); +} diff --git a/src/RESPite.Redis/Formatters.cs b/src/RESPite.Redis/Formatters.cs new file mode 100644 index 000000000..eecddd14d --- /dev/null +++ b/src/RESPite.Redis/Formatters.cs @@ -0,0 +1,9 @@ +namespace RESPite.Redis; + +internal static class Formatters +{ + private const string Global = "global::RESPite.Redis"; + + public const string KeyStringArray = + $"{Global}.{nameof(KeyStringArrayFormatter)}.{nameof(KeyStringArrayFormatter.Instance)}"; +} diff --git a/src/RESPite.Redis/KeyStringArrayFormatter.cs b/src/RESPite.Redis/KeyStringArrayFormatter.cs new file mode 100644 index 000000000..98d37494e --- /dev/null +++ b/src/RESPite.Redis/KeyStringArrayFormatter.cs @@ -0,0 +1,18 @@ +using System; +using Resp; + +namespace RESPite.Redis; + +internal sealed class KeyStringArrayFormatter : IRespFormatter> +{ + public static readonly KeyStringArrayFormatter Instance = new(); + + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in ReadOnlyMemory keys) + { + writer.WriteCommand(command, keys.Length); + foreach (var key in keys.Span) + { + writer.WriteKey(key); + } + } +} diff --git a/src/RESPite.Redis/RESPite.Redis.csproj b/src/RESPite.Redis/RESPite.Redis.csproj new file mode 100644 index 000000000..1b9f7fef5 --- /dev/null +++ b/src/RESPite.Redis/RESPite.Redis.csproj @@ -0,0 +1,23 @@ + + + + true + net461;netstandard2.0;net8.0 + $(NoWarn);CS1591 + 2025 - $([System.DateTime]::Now.Year) Marc Gravell + + + + + + + + + + NullableHacks.cs + + + SkipLocalsInit.cs + + + diff --git a/src/RESPite.Redis/RedisExtensions.cs b/src/RESPite.Redis/RedisExtensions.cs new file mode 100644 index 000000000..a10ca1064 --- /dev/null +++ b/src/RESPite.Redis/RedisExtensions.cs @@ -0,0 +1,22 @@ +using System.Runtime.CompilerServices; +using Resp; + +namespace RESPite.Redis; + +public static class RedisExtensions +{ +#if PREVIEW_LANGVER + extension(in RespContext context) + { + // since this is valid... + // public ref readonly RespContext Self => ref context; + + // so must this be (importantly, RedisStrings has only a single RespContext field) + public ref readonly RedisStrings Strings + => ref Unsafe.As(ref Unsafe.AsRef(in context)); + + public ref readonly RedisKeys Keys + => ref Unsafe.As(ref Unsafe.AsRef(in context)); + } +#endif +} diff --git a/src/RESPite.Redis/RedisKeys.cs b/src/RESPite.Redis/RedisKeys.cs new file mode 100644 index 000000000..1a45c34a0 --- /dev/null +++ b/src/RESPite.Redis/RedisKeys.cs @@ -0,0 +1,16 @@ +using System; +using Resp; + +namespace RESPite.Redis; + +// note that members may also be added as extensions if necessary +public readonly partial struct RedisKeys(in RespContext context) +{ + private readonly RespContext _context = context; + + [RespCommand] + public partial void Del(string key); + + [RespCommand(Formatter = Formatters.KeyStringArray)] + public partial int Del(ReadOnlyMemory keys); +} diff --git a/src/RESPite.Redis/RedisStrings.cs b/src/RESPite.Redis/RedisStrings.cs new file mode 100644 index 000000000..19e96c390 --- /dev/null +++ b/src/RESPite.Redis/RedisStrings.cs @@ -0,0 +1,99 @@ +using System; +using System.Threading.Tasks; +using Resp; + +#if !PREVIEW_LANGVER +using RESPite.Redis.Alt; +#endif + +namespace RESPite.Redis; + +// note that members may also be added as extensions if necessary +public readonly partial struct RedisStrings(in RespContext context) +{ + private readonly RespContext _context = context; + + // re-expose del +#if PREVIEW_LANGVER + public void Del(string key) => _context.Keys.Del(key); + public ValueTask DelAsync(string key) => _context.Keys.DelAsync(key); +#else + public void Del(string key) => _context.AsKeys().Del(key); + public ValueTask DelAsync(string key) => _context.AsKeys().DelAsync(key); +#endif + + [RespCommand] + public partial int Append(string key, string value); + + [RespCommand] + public partial int Append(string key, ReadOnlyMemory value); + + [RespCommand] + public partial int Decr(string key); + + [RespCommand] + public partial int DecrBy(string key, int value); + + [RespCommand] + public partial string Get(string key); + + [RespCommand("get")] + public partial int GetInt32(string key); + + [RespCommand("get")] + public partial double GetDouble(string key); + + [RespCommand] + public partial string GetDel(string key); + + [RespCommand(Formatter = ExpiryTimeSpanFormatter.Formatter)] + public partial string GetEx(string key, TimeSpan expiry); + + private sealed class ExpiryTimeSpanFormatter : IRespFormatter<(string Key, TimeSpan Expiry)> + { + public const string Formatter = $"{nameof(ExpiryTimeSpanFormatter)}.{nameof(Instance)}"; + + public static readonly ExpiryTimeSpanFormatter Instance = new(); + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (string Key, TimeSpan Expiry) request) + { + writer.WriteCommand(command, 3); + writer.WriteKey(request.Key); + writer.WriteBulkString("PX"u8); + writer.WriteBulkString((long)request.Expiry.TotalMilliseconds); + } + } + + [RespCommand] + public partial string GetRange(string key, int start, int end); + + [RespCommand] + public partial string GetSet(string key, string value); + + [RespCommand] + public partial string GetSet(string key, ReadOnlyMemory value); + + [RespCommand] + public partial int Incr(string key); + + [RespCommand] + public partial int IncrBy(string key, int value); + + [RespCommand] + public partial double IncrByFloat(string key, double value); + + [RespCommand] + public partial void Set(string key, string value); + + [RespCommand] + public partial void Set(string key, ReadOnlyMemory value); + + [RespCommand] + public partial void Set(string key, int value); + + [RespCommand] + public partial void Set(string key, double value); +} diff --git a/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj b/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj new file mode 100644 index 000000000..a8b89c0f4 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj @@ -0,0 +1,20 @@ + + + + true + net461;netstandard2.0;net8.0 + enable + enable + $(NoWarn);CS1591 + 2025 - $([System.DateTime]::Now.Year) Marc Gravell + + + + + NullableHacks.cs + + + SkipLocalsInit.cs + + + diff --git a/src/RESPite/Alt/DownlevelExtensions.cs b/src/RESPite/Alt/DownlevelExtensions.cs new file mode 100644 index 000000000..68e0c2850 --- /dev/null +++ b/src/RESPite/Alt/DownlevelExtensions.cs @@ -0,0 +1,10 @@ +namespace RESPite.Alt; + +/// +/// For use with older compilers that don't support byref-return, extension-everything, etc. +/// +public static class DownlevelExtensions +{ + public static RespContext GetContext(this IRespConnection connection) + => connection.Context; +} diff --git a/src/RESPite/Connections/BatchConnection.cs b/src/RESPite/Connections/BatchConnection.cs new file mode 100644 index 000000000..3d8c11f42 --- /dev/null +++ b/src/RESPite/Connections/BatchConnection.cs @@ -0,0 +1,219 @@ +using System.Buffers; +using System.Runtime.InteropServices; + +namespace RESPite.Connections; + +internal sealed class BatchConnection : IBatchConnection +{ + private bool _isDisposed; + private readonly List _unsent; + private readonly IRespConnection _tail; + private readonly RespContext _context; + + public BatchConnection(in RespContext context, int sizeHint) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - an abundance of caution + var tail = context.Connection; + if (tail is not { CanWrite: true }) ThrowNonWritable(); + if (tail is BatchConnection) ThrowBatch(); + + _unsent = sizeHint <= 0 ? [] : new List(sizeHint); + _tail = tail!; + _context = context.WithConnection(this); + static void ThrowBatch() => throw new ArgumentException("Nested batches are not supported", nameof(tail)); + + static void ThrowNonWritable() => + throw new ArgumentException("A writable connection is required", nameof(tail)); + } + + public void Dispose() + { + lock (_unsent) + { + /* everyone else checks disposal inside the lock, so: + once we've set this, we can be sure that no more + items will be added */ + _isDisposed = true; + } +#if NET5_0_OR_GREATER + var span = CollectionsMarshal.AsSpan(_unsent); + foreach (var message in span) + { + message.Message.TrySetException(message.Token, new ObjectDisposedException(ToString())); + } +#else + foreach (var message in _unsent) + { + message.Message.TrySetException(message.Token, new ObjectDisposedException(ToString())); + } +#endif + _unsent.Clear(); + } + + public ValueTask DisposeAsync() + { + Dispose(); + return default; + } + + public RespConfiguration Configuration => _tail.Configuration; + public bool CanWrite => _tail.CanWrite; + + public int Outstanding + { + get + { + lock (_unsent) + { + return _unsent.Count; + } + } + } + + public ref readonly RespContext Context => ref _context; + + private const string SyncMessage = "Batch connections do not support synchronous sends"; + public void Send(in RespOperation message) => throw new NotSupportedException(SyncMessage); + + public void Send(ReadOnlySpan messages) => throw new NotSupportedException(SyncMessage); + + private void ThrowIfDisposed() + { + if (_isDisposed) Throw(); + static void Throw() => throw new ObjectDisposedException(nameof(BatchConnection)); + } + + public Task SendAsync(in RespOperation message) + { + lock (_unsent) + { + ThrowIfDisposed(); + _unsent.Add(message); + } + + return Task.CompletedTask; + } + + public Task SendAsync(ReadOnlyMemory messages) + { + if (messages.Length != 0) + { + lock (_unsent) + { + ThrowIfDisposed(); +#if NET8_0_OR_GREATER + _unsent.AddRange(messages.Span); // internally optimized +#else + // two-step; first ensure capacity, then add in loop +#if NET6_0_OR_GREATER + _unsent.EnsureCapacity(_unsent.Count + messages.Length); +#else + var required = _unsent.Count + messages.Length; + if (_unsent.Capacity < required) + { + const int maxLength = 0X7FFFFFC7; // not directly available on down-level runtimes :( + var newCapacity = _unsent.Capacity * 2; // try doubling + if ((uint)newCapacity > maxLength) newCapacity = maxLength; // account for max + if (newCapacity < required) newCapacity = required; // in case doubling wasn't enough + _unsent.Capacity = newCapacity; + } +#endif + foreach (var message in messages.Span) + { + _unsent.Add(message); + } +#endif + } + } + + return Task.CompletedTask; + } + + private int Flush(out RespOperation[] oversized, out RespOperation single) + { + lock (_unsent) + { + var count = _unsent.Count; + switch (count) + { + case 0: + oversized = []; + single = default; + break; + case 1: + oversized = []; + single = _unsent[0]; + break; + default: + oversized = ArrayPool.Shared.Rent(count); + single = default; + _unsent.CopyTo(oversized); + break; + } + + _unsent.Clear(); + return count; + } + } + + public Task FlushAsync() + { + var count = Flush(out var oversized, out var single); + return count switch + { + 0 => Task.CompletedTask, + 1 => _tail.SendAsync(single!), + _ => SendAndRecycleAsync(_tail, oversized, count), + }; + + static async Task SendAndRecycleAsync(IRespConnection tail, RespOperation[] oversized, int count) + { + try + { + await tail.SendAsync(oversized.AsMemory(0, count)).ConfigureAwait(false); + ArrayPool.Shared.Return(oversized); // only on success, in case captured + } + catch (Exception ex) + { + foreach (var message in oversized.AsSpan(0, count)) + { + message.Message.TrySetException(message.Token, ex); + } + + throw; + } + } + } + + public void Flush() + { + var count = Flush(out var oversized, out var single); + switch (count) + { + case 0: + return; + case 1: + _tail.Send(single!); + return; + } + + try + { + _tail.Send(oversized.AsSpan(0, count)); + } + catch (Exception ex) + { + foreach (var message in oversized.AsSpan(0, count)) + { + message.Message.TrySetException(message.Token, ex); + } + + throw; + } + finally + { + // in the sync case, Send takes a span - hence can't have been captured anywhere; always recycle + ArrayPool.Shared.Return(oversized); + } + } +} diff --git a/src/RESPite/IRespBatch.cs b/src/RESPite/IRespBatch.cs new file mode 100644 index 000000000..12daa12b9 --- /dev/null +++ b/src/RESPite/IRespBatch.cs @@ -0,0 +1,7 @@ +namespace RESPite; + +public interface IBatchConnection : IRespConnection +{ + Task FlushAsync(); + void Flush(); +} diff --git a/src/RESPite/IRespConnection.cs b/src/RESPite/IRespConnection.cs new file mode 100644 index 000000000..17c62c727 --- /dev/null +++ b/src/RESPite/IRespConnection.cs @@ -0,0 +1,19 @@ +namespace RESPite; + +public interface IRespConnection : IDisposable, IAsyncDisposable +{ + RespConfiguration Configuration { get; } + bool CanWrite { get; } + int Outstanding { get; } + + /// + /// Gets the default context associates with this connection. + /// + ref readonly RespContext Context { get; } + + void Send(in RespOperation message); + void Send(ReadOnlySpan message); + + Task SendAsync(in RespOperation message); + Task SendAsync(ReadOnlyMemory message); +} diff --git a/src/RESPite/Internal/ActivationHelper.cs b/src/RESPite/Internal/ActivationHelper.cs new file mode 100644 index 000000000..15b07c7b2 --- /dev/null +++ b/src/RESPite/Internal/ActivationHelper.cs @@ -0,0 +1,115 @@ +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace RESPite.Internal; + +internal static class ActivationHelper +{ + private sealed class WorkItem +#if NETCOREAPP3_0_OR_GREATER + : IThreadPoolWorkItem +#endif + { + private WorkItem() + { +#if NET5_0_OR_GREATER + Unsafe.SkipInit(out _payload); +#else + _payload = []; +#endif + } + + private void Init(byte[] payload, int length, in RespOperation message) + { + _payload = payload; + _length = length; + _message = message; + } + + private byte[] _payload; + private int _length; + private RespOperation _message; + + private static WorkItem? _spare; // do NOT use ThreadStatic - different producer/consumer, no overlap + + public static void UnsafeQueueUserWorkItem( + in RespOperation message, + ReadOnlySpan payload, + ref byte[]? lease) + { + if (lease is null) + { + // we need to create our own copy of the data + lease = ArrayPool.Shared.Rent(payload.Length); + payload.CopyTo(lease); + } + + var obj = Interlocked.Exchange(ref _spare, null) ?? new(); + obj.Init(lease, payload.Length, message); + lease = null; // count as claimed + + DebugCounters.OnCopyOut(payload.Length); +#if NETCOREAPP3_0_OR_GREATER + ThreadPool.UnsafeQueueUserWorkItem(obj, false); +#else + ThreadPool.UnsafeQueueUserWorkItem(WaitCallback, obj); +#endif + } +#if !NETCOREAPP3_0_OR_GREATER + private static readonly WaitCallback WaitCallback = state => ((WorkItem)state!).Execute(); +#endif + + public void Execute() + { + var message = _message; + var payload = _payload; + var length = _length; + _message = default; + _payload = []; + _length = 0; + Interlocked.Exchange(ref _spare, this); + message.Message.TrySetResult(message.Token, new ReadOnlySpan(payload, 0, length)); + ArrayPool.Shared.Return(payload); + } + } + + public static void ProcessResponse(in RespOperation pending, ReadOnlySpan payload, ref byte[]? lease) + { + if (pending.Message.AllowInlineParsing) + { + pending.Message.TrySetResult(pending.Token, payload); + } + else + { + WorkItem.UnsafeQueueUserWorkItem(pending, payload, ref lease); + } + } + + private static readonly Action CancellationCallback = static state + => ((IRespMessage)state!).TrySetCanceled(); + + public static CancellationTokenRegistration RegisterForCancellation( + IRespMessage message, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return cancellationToken.Register(CancellationCallback, message); + } + + [Conditional("DEBUG")] + public static void DebugBreak() + { +#if DEBUG + if (Debugger.IsAttached) Debugger.Break(); +#endif + } + + [Conditional("DEBUG")] + public static void DebugBreakIf(bool condition) + { +#if DEBUG + if (condition && Debugger.IsAttached) Debugger.Break(); +#endif + } +} diff --git a/src/RESPite/Internal/CycleBuffer.cs b/src/RESPite/Internal/CycleBuffer.cs new file mode 100644 index 000000000..f32f66b28 --- /dev/null +++ b/src/RESPite/Internal/CycleBuffer.cs @@ -0,0 +1,705 @@ +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#pragma warning disable SA1205 // accessibility on partial - for debugging/test practicality + +namespace RESPite.Internal; + +/// +/// Manages the state for a based IO buffer. Unlike Pipe, +/// it is not intended for a separate producer-consumer - there is no thread-safety, and no +/// activation; it just handles the buffers. It is intended to be used as a mutable (non-readonly) +/// field in a type that performs IO; the internal state mutates - it should not be passed around. +/// +/// Notionally, there is an uncommitted area (write) and a committed area (read). Process: +/// - producer loop (*note no concurrency**) +/// - call to get a new scratch +/// - (write to that span) +/// - call to mark complete portions +/// - consumer loop (*note no concurrency**) +/// - call to see if there is a single-span chunk; otherwise +/// - call to get the multi-span chunk +/// - (process none, some, or all of that data) +/// - call to indicate how much data is no longer needed +/// Emphasis: no concurrency! This is intended for a single worker acting as both producer and consumer. +/// +/// There is a *lot* of validation in debug mode; we want to be super sure that we don't corrupt buffer state. +/// +partial struct CycleBuffer +{ + // note: if someone uses an uninitialized CycleBuffer (via default): that's a skills issue; git gud + public static CycleBuffer Create(MemoryPool? pool = null, int pageSize = DefaultPageSize) + { + pool ??= MemoryPool.Shared; + if (pageSize <= 0) pageSize = DefaultPageSize; + if (pageSize > pool.MaxBufferSize) pageSize = pool.MaxBufferSize; + + return new CycleBuffer(pool, pageSize); + } + + private CycleBuffer(MemoryPool pool, int pageSize) + { + Pool = pool; + PageSize = pageSize; + } + + private const int DefaultPageSize = 8 * 1024; + + public int PageSize { get; } + public MemoryPool Pool { get; } + + private Segment? startSegment, endSegment; + + private int endSegmentCommitted, endSegmentLength; + + public bool TryGetCommitted(out ReadOnlySpan span) + { + DebugAssertValid(); + if (!ReferenceEquals(startSegment, endSegment)) + { + span = default; + return false; + } + + span = startSegment is null ? default : startSegment.Memory.Span.Slice(start: 0, length: endSegmentCommitted); + return true; + } + + /// + /// Commits data written to buffers from , making it available for consumption + /// via . This compares to . + /// + public void Commit(int count) + { + DebugAssertValid(); + if (count <= 0) + { + if (count < 0) Throw(); + return; + } + + var available = endSegmentLength - endSegmentCommitted; + if (count > available) Throw(); + endSegmentCommitted += count; + DebugAssertValid(); + + static void Throw() => throw new ArgumentOutOfRangeException(nameof(count)); + } + + public bool CommittedIsEmpty => ReferenceEquals(startSegment, endSegment) & endSegmentCommitted == 0; + + /// + /// Marks committed data as fully consumed; it will no longer appear in later calls to . + /// + public void DiscardCommitted(int count) + { + DebugAssertValid(); + // optimize for most common case, where we consume everything + if (ReferenceEquals(startSegment, endSegment) + & count == endSegmentCommitted + & count > 0) + { + /* + we are consuming all the data in the single segment; we can + just reset that segment back to full size and re-use as-is; + note that we also know that there must *be* a segment + for the count check to pass + */ + endSegmentCommitted = 0; + endSegmentLength = endSegment!.Untrim(expandBackwards: true); + DebugAssertValid(0); + DebugCounters.OnDiscardFull(count); + } + else if (count == 0) + { + // nothing to do + } + else + { + DiscardCommittedSlow(count); + } + } + + public void DiscardCommitted(long count) + { + DebugAssertValid(); + // optimize for most common case, where we consume everything + if (ReferenceEquals(startSegment, endSegment) + & count == endSegmentCommitted + & count > 0) // checks sign *and* non-trimmed + { + // see for logic + endSegmentCommitted = 0; + endSegmentLength = endSegment!.Untrim(expandBackwards: true); + DebugAssertValid(0); + DebugCounters.OnDiscardFull(count); + } + else if (count == 0) + { + // nothing to do + } + else + { + DiscardCommittedSlow(count); + } + } + + private void DiscardCommittedSlow(long count) + { + DebugCounters.OnDiscardPartial(count); +#if DEBUG + var originalLength = GetCommittedLength(); + var originalCount = count; + var expectedLength = originalLength - originalCount; + string blame = nameof(DiscardCommittedSlow); +#endif + while (count > 0) + { + DebugAssertValid(); + var segment = startSegment; + if (segment is null) break; + if (ReferenceEquals(segment, endSegment)) + { + // first==final==only segment + if (count == endSegmentCommitted) + { + endSegmentLength = startSegment!.Untrim(); + endSegmentCommitted = 0; // = untrimmed and unused +#if DEBUG + blame += ",full-final (t)"; +#endif + } + else + { + // discard from the start + int count32 = checked((int)count); + segment.TrimStart(count32); + endSegmentLength -= count32; + endSegmentCommitted -= count32; +#if DEBUG + blame += ",partial-final"; +#endif + } + + count = 0; + break; + } + else if (count < segment.Length) + { + // multiple, but can take some (not all) of the first buffer +#if DEBUG + var len = segment.Length; +#endif + segment.TrimStart((int)count); + Debug.Assert(segment.Length > 0, "parial trim should have left non-empty segment"); +#if DEBUG + Debug.Assert(segment.Length == len - count, "trim failure"); + blame += ",partial-first"; +#endif + count = 0; + break; + } + else + { + // multiple; discard the entire first segment + count -= segment.Length; + startSegment = + segment.ResetAndGetNext(); // we already did a ref-check, so we know this isn't going past endSegment + endSegment!.AppendOrRecycle(segment, maxDepth: 2); + DebugAssertValid(); +#if DEBUG + blame += ",full-first"; +#endif + } + } + + if (count != 0) ThrowCount(); +#if DEBUG + DebugAssertValid(expectedLength, blame); + _ = originalLength; + _ = originalCount; +#endif + + [DoesNotReturn] + static void ThrowCount() => throw new ArgumentOutOfRangeException(nameof(count)); + } + + [Conditional("DEBUG")] + private void DebugAssertValid(long expectedCommittedLength, [CallerMemberName] string caller = "") + { + DebugAssertValid(); + var actual = GetCommittedLength(); + Debug.Assert( + expectedCommittedLength >= 0, + $"Expected committed length is just... wrong: {expectedCommittedLength} (from {caller})"); + Debug.Assert( + expectedCommittedLength == actual, + $"Committed length mismatch: expected {expectedCommittedLength}, got {actual} (from {caller})"); + } + + [Conditional("DEBUG")] + private void DebugAssertValid() + { + if (startSegment is null) + { + Debug.Assert( + endSegmentLength == 0 & endSegmentCommitted == 0, + "un-init state should be zero"); + return; + } + + Debug.Assert(endSegment is not null, "end segment must not be null if start segment exists"); + Debug.Assert( + endSegmentLength == endSegment!.Length, + $"end segment length is incorrect - expected {endSegmentLength}, got {endSegment.Length}"); + Debug.Assert(endSegmentCommitted <= endSegmentLength, $"end segment is over-committed - {endSegmentCommitted} of {endSegmentLength}"); + + // check running indices + startSegment?.DebugAssertValidChain(); + } + + public long GetCommittedLength() + { + DebugAssertValid(); + if (ReferenceEquals(startSegment, endSegment)) + { + return endSegmentCommitted; + } + + // note that the start-segment is pre-trimmed; we don't need to account for an offset on the left + return (endSegment!.RunningIndex + endSegmentCommitted) - startSegment!.RunningIndex; + } + + /// + /// When used with , this means "any non-empty buffer". + /// + public const int GetAnything = 0; + + /// + /// When used with , this means "any full buffer". + /// + public const int GetFullPagesOnly = -1; + + public bool TryGetFirstCommittedSpan(int minBytes, out ReadOnlySpan span) + { + DebugAssertValid(); + if (TryGetFirstCommittedMemory(minBytes, out var memory)) + { + span = memory.Span; + return true; + } + + span = default; + return false; + } + + /// + /// The minLength arg: -ve means "full segments only" (useful when buffering outbound network data to avoid + /// packet fragmentation); otherwise, it is the minimum length we want. + /// + public bool TryGetFirstCommittedMemory(int minBytes, out ReadOnlyMemory memory) + { + if (minBytes == 0) minBytes = 1; // success always means "at least something" + DebugAssertValid(); + if (ReferenceEquals(startSegment, endSegment)) + { + // single page + var available = endSegmentCommitted; + if (available == 0) + { + // empty (includes uninitialized) + memory = default; + return false; + } + + memory = startSegment!.Memory; + var memLength = memory.Length; + if (available == memLength) + { + // full segment; is it enough to make the caller happy? + return available >= minBytes; + } + + // partial segment (and we know it isn't empty) + memory = memory.Slice(start: 0, length: available); + return available >= minBytes & minBytes > 0; // last check here applies the -ve logic + } + + // multi-page; hand out the first page (which is, by definition: full) + memory = startSegment!.Memory; + return memory.Length >= minBytes; + } + + /// + /// Note that this chain is invalidated by any other operations; no concurrency. + /// + public ReadOnlySequence GetAllCommitted() + { + if (ReferenceEquals(startSegment, endSegment)) + { + // single segment, fine + return startSegment is null + ? default + : new ReadOnlySequence(startSegment.Memory.Slice(start: 0, length: endSegmentCommitted)); + } + +#if PARSE_DETAIL + long length = GetCommittedLength(); +#endif + ReadOnlySequence ros = new(startSegment!, 0, endSegment!, endSegmentCommitted); +#if PARSE_DETAIL + Debug.Assert(ros.Length == length, $"length mismatch: calculated {length}, actual {ros.Length}"); +#endif + return ros; + } + + private Segment GetNextSegment() + { + DebugAssertValid(); + if (endSegment is not null) + { + endSegment.TrimEnd(endSegmentCommitted); + Debug.Assert(endSegment.Length == endSegmentCommitted, "trim failure"); + endSegmentLength = endSegmentCommitted; + DebugAssertValid(); + + var spare = endSegment.Next; + if (spare is not null) + { + // we already have a dangling segment; just update state + endSegment.DebugAssertValidChain(); + endSegment = spare; + endSegmentCommitted = 0; + endSegmentLength = spare.Length; + DebugAssertValid(); + return spare; + } + } + + Segment newSegment = Segment.Create(Pool.Rent(PageSize)); + if (endSegment is null) + { + // tabula rasa + endSegmentLength = newSegment.Length; + endSegment = startSegment = newSegment; + DebugAssertValid(); + return newSegment; + } + + endSegment.Append(newSegment); + endSegmentCommitted = 0; + endSegmentLength = newSegment.Length; + endSegment = newSegment; + DebugAssertValid(); + return newSegment; + } + + /// + /// Gets a scratch area for new data; this compares to . + /// + public Span GetUncommittedSpan(int hint = 0) + => GetUncommittedMemory(hint).Span; + + /// + /// Gets a scratch area for new data; this compares to . + /// + public Memory GetUncommittedMemory(int hint = 0) + { + DebugAssertValid(); + var segment = endSegment; + if (segment is not null) + { + var memory = segment.Memory; + if (endSegmentCommitted != 0) memory = memory.Slice(start: endSegmentCommitted); + if (hint <= 0) // allow anything non-empty + { + if (!memory.IsEmpty) return MemoryMarshal.AsMemory(memory); + } + else if (memory.Length >= Math.Min(hint, PageSize >> 2)) // respect the hint up to 1/4 of the page size + { + return MemoryMarshal.AsMemory(memory); + } + } + + // new segment, will always be entire + return MemoryMarshal.AsMemory(GetNextSegment().Memory); + } + + public int UncommittedAvailable + { + get + { + DebugAssertValid(); + return endSegmentLength - endSegmentCommitted; + } + } + + private sealed class Segment : ReadOnlySequenceSegment + { + private Segment() { } + private IMemoryOwner _lease = NullLease.Instance; + private static Segment? _spare; + private Flags _flags; + + [Flags] + private enum Flags + { + None = 0, + StartTrim = 1 << 0, + EndTrim = 1 << 2, + } + + public static Segment Create(IMemoryOwner lease) + { + Debug.Assert(lease is not null, "null lease"); + var memory = lease!.Memory; + if (memory.IsEmpty) ThrowEmpty(); + + var obj = Interlocked.Exchange(ref _spare, null) ?? new(); + return obj.Init(lease, memory); + static void ThrowEmpty() => throw new InvalidOperationException("leased segment is empty"); + } + + private Segment Init(IMemoryOwner lease, Memory memory) + { + _lease = lease; + Memory = memory; + return this; + } + + public int Length => Memory.Length; + + public void Append(Segment next) + { + Debug.Assert(Next is null, "current segment already has a next"); + Debug.Assert(next.Next is null && next.RunningIndex == 0, "inbound next segment is already in a chain"); + next.RunningIndex = RunningIndex + Length; + Next = next; + DebugAssertValidChain(); + } + + private void ApplyChainDelta(int delta) + { + if (delta != 0) + { + var node = Next; + while (node is not null) + { + node.RunningIndex += delta; + node = node.Next; + } + } + } + + public void TrimEnd(int newLength) + { + var delta = Length - newLength; + if (delta != 0) + { + // buffer wasn't fully used; trim + _flags |= Flags.EndTrim; + Memory = Memory.Slice(0, newLength); + ApplyChainDelta(-delta); + DebugAssertValidChain(); + } + } + + public void TrimStart(int remove) + { + if (remove != 0) + { + _flags |= Flags.StartTrim; + Memory = Memory.Slice(start: remove); + RunningIndex += remove; // so that ROS length keeps working; note we *don't* need to adjust the chain + DebugAssertValidChain(); + } + } + + public new Segment? Next + { + get => (Segment?)base.Next; + private set => base.Next = value; + } + + public Segment? ResetAndGetNext() + { + var next = Next; + Next = null; + RunningIndex = 0; + _flags = Flags.None; + Memory = _lease.Memory; // reset, in case we trimmed it + DebugAssertValidChain(); + return next; + } + + public void Recycle() + { + var lease = _lease; + _lease = NullLease.Instance; + lease.Dispose(); + Next = null; + Memory = default; + RunningIndex = 0; + _flags = Flags.None; + Interlocked.Exchange(ref _spare, this); + DebugAssertValidChain(); + } + + private sealed class NullLease : IMemoryOwner + { + private NullLease() { } + public static readonly NullLease Instance = new NullLease(); + public void Dispose() { } + + public Memory Memory => default; + } + + /// + /// Undo any trimming, returning the new full capacity. + /// + public int Untrim(bool expandBackwards = false) + { + var fullMemory = _lease.Memory; + var fullLength = fullMemory.Length; + var delta = fullLength - Length; + if (delta != 0) + { + _flags &= ~(Flags.StartTrim | Flags.EndTrim); + Memory = fullMemory; + if (expandBackwards & RunningIndex >= delta) + { + // push our origin earlier; only valid if + // we're the first segment, otherwise + // we break someone-else's chain + RunningIndex -= delta; + } + else + { + // push everyone else later + ApplyChainDelta(delta); + } + + DebugAssertValidChain(); + } + return fullLength; + } + + public bool StartTrimmed => (_flags & Flags.StartTrim) != 0; + public bool EndTrimmed => (_flags & Flags.EndTrim) != 0; + + [Conditional("DEBUG")] + public void DebugAssertValidChain([CallerMemberName] string blame = "") + { + var node = this; + var runningIndex = RunningIndex; + int index = 0; + while (node.Next is { } next) + { + index++; + var nextRunningIndex = runningIndex + node.Length; + if (nextRunningIndex != next.RunningIndex) ThrowRunningIndex(blame, index); + node = next; + runningIndex = nextRunningIndex; + static void ThrowRunningIndex(string blame, int index) => throw new InvalidOperationException( + $"Critical running index corruption in dangling chain, from '{blame}', segment {index}"); + } + } + + public void AppendOrRecycle(Segment segment, int maxDepth) + { + var node = this; + while (maxDepth-- > 0 && node is not null) + { + if (node.Next is null) // found somewhere to attach it + { + if (segment.Untrim() == 0) break; // turned out to be useless + segment.RunningIndex = node.RunningIndex + node.Length; + node.Next = segment; + return; + } + + node = node.Next; + } + + segment.Recycle(); + } + } + + /// + /// Discard all data and buffers. + /// + public void Release() + { + var node = startSegment; + startSegment = endSegment = null; + endSegmentCommitted = endSegmentLength = 0; + while (node is not null) + { + var next = node.Next; + node.Recycle(); + node = next; + } + } +} + +// this can be shared between CycleBuffer and CycleBuffer.Simple +partial struct CycleBuffer +{ + /// + /// Writes a value to the buffer; comparable to . + /// + public void Write(ReadOnlySpan value) + { + int srcLength = value.Length; + while (srcLength != 0) + { + var target = GetUncommittedSpan(hint: srcLength); + var tgtLength = target.Length; + if (tgtLength >= srcLength) + { + value.CopyTo(target); + Commit(srcLength); + return; + } + + value.Slice(0, tgtLength).CopyTo(target); + Commit(tgtLength); + value = value.Slice(tgtLength); + srcLength -= tgtLength; + } + } + + /// + /// Writes a value to the buffer; comparable to . + /// + public void Write(in ReadOnlySequence value) + { + if (value.IsSingleSegment) + { +#if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1 + Write(value.FirstSpan); +#else + Write(value.First.Span); +#endif + } + else + { + WriteMultiSegment(ref this, in value); + } + + static void WriteMultiSegment(ref CycleBuffer @this, in ReadOnlySequence value) + { + foreach (var segment in value) + { +#if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1 + @this.Write(value.FirstSpan); +#else + @this.Write(value.First.Span); +#endif + } + } + } +} diff --git a/src/RESPite/Internal/DebugCounters.cs b/src/RESPite/Internal/DebugCounters.cs new file mode 100644 index 000000000..3f527978e --- /dev/null +++ b/src/RESPite/Internal/DebugCounters.cs @@ -0,0 +1,182 @@ +using System.Diagnostics; + +namespace RESPite.Internal; +#if DEBUG +public partial class DebugCounters +#else +internal partial class DebugCounters +#endif +{ +#if DEBUG + private static int _tallyReadCount, + _tallyAsyncReadCount, + _tallyAsyncReadInlineCount, + _tallyWriteCount, + _tallyAsyncWriteCount, + _tallyAsyncWriteInlineCount, + _tallyCopyOutCount, + _tallyDiscardFullCount, + _tallyDiscardPartialCount, + _tallyPipelineFullAsyncCount, + _tallyPipelineSendAsyncCount, + _tallyPipelineFullSyncCount, + _tallyBatchWriteCount, + _tallyBatchWriteFullPageCount, + _tallyBatchWritePartialPageCount, + _tallyBatchWriteMessageCount; + + private static long _tallyWriteBytes, _tallyReadBytes, _tallyCopyOutBytes, _tallyDiscardAverage; +#endif + [Conditional("DEBUG")] + internal static void OnRead(int bytes) + { +#if DEBUG + Interlocked.Increment(ref _tallyReadCount); + if (bytes > 0) Interlocked.Add(ref _tallyReadBytes, bytes); +#endif + } + + public static void OnBatchWrite(int messageCount) + { +#if DEBUG + Interlocked.Increment(ref _tallyBatchWriteCount); + if (messageCount != 0) Interlocked.Add(ref _tallyBatchWriteMessageCount, messageCount); +#endif + } + + public static void OnBatchWriteFullPage() + { +#if DEBUG + Interlocked.Increment(ref _tallyBatchWriteFullPageCount); +#endif + } + public static void OnBatchWritePartialPage() + { +#if DEBUG + Interlocked.Increment(ref _tallyBatchWritePartialPageCount); +#endif + } + + [Conditional("DEBUG")] + internal static void OnAsyncRead(int bytes, bool inline) + { +#if DEBUG + Interlocked.Increment(ref inline ? ref _tallyAsyncReadInlineCount : ref _tallyAsyncReadCount); + if (bytes > 0) Interlocked.Add(ref _tallyReadBytes, bytes); +#endif + } + + [Conditional("DEBUG")] + internal static void OnWrite(int bytes) + { +#if DEBUG + Interlocked.Increment(ref _tallyWriteCount); + if (bytes > 0) Interlocked.Add(ref _tallyWriteBytes, bytes); +#endif + } + + [Conditional("DEBUG")] + internal static void OnAsyncWrite(int bytes, bool inline) + { +#if DEBUG + Interlocked.Increment(ref inline ? ref _tallyAsyncWriteInlineCount : ref _tallyAsyncWriteCount); + if (bytes > 0) Interlocked.Add(ref _tallyWriteBytes, bytes); +#endif + } + + [Conditional("DEBUG")] + internal static void OnCopyOut(int bytes) + { +#if DEBUG + Interlocked.Increment(ref _tallyCopyOutCount); + if (bytes > 0) Interlocked.Add(ref _tallyCopyOutBytes, bytes); +#endif + } + + [Conditional("DEBUG")] + public static void OnDiscardFull(long count) + { +#if DEBUG + if (count > 0) + { + Interlocked.Increment(ref _tallyDiscardFullCount); + EstimatedMovingRangeAverage(ref _tallyDiscardAverage, count); + } +#endif + } + + [Conditional("DEBUG")] + public static void OnDiscardPartial(long count) + { +#if DEBUG + if (count > 0) + { + Interlocked.Increment(ref _tallyDiscardPartialCount); + EstimatedMovingRangeAverage(ref _tallyDiscardAverage, count); + } +#endif + } + + [Conditional("DEBUG")] + public static void OnPipelineFullAsync() + { +#if DEBUG + Interlocked.Increment(ref _tallyPipelineFullAsyncCount); +#endif + } + + [Conditional("DEBUG")] + public static void OnPipelineSendAsync() + { +#if DEBUG + Interlocked.Increment(ref _tallyPipelineSendAsyncCount); +#endif + } + + [Conditional("DEBUG")] + public static void OnPipelineFullSync() + { +#if DEBUG + Interlocked.Increment(ref _tallyPipelineFullSyncCount); +#endif + } + + private DebugCounters() + { + } + + public static DebugCounters Flush() => new(); + +#if DEBUG + private static void EstimatedMovingRangeAverage(ref long field, long value) + { + var oldValue = Volatile.Read(ref field); + var delta = (value - oldValue) >> 3; // is is a 7:1 old:new EMRA, using integer/bit math (alplha=0.125) + if (delta != 0) Interlocked.Add(ref field, delta); + // note: strictly conflicting concurrent calls can skew the value incorrectly; this is, however, + // preferable to getting into a CEX squabble or requiring a lock - it is debug-only and just useful data + } + + public int ReadCount { get; } = Interlocked.Exchange(ref _tallyReadCount, 0); + public int AsyncReadCount { get; } = Interlocked.Exchange(ref _tallyAsyncReadCount, 0); + public int AsyncReadInlineCount { get; } = Interlocked.Exchange(ref _tallyAsyncReadInlineCount, 0); + public long ReadBytes { get; } = Interlocked.Exchange(ref _tallyReadBytes, 0); + + public int WriteCount { get; } = Interlocked.Exchange(ref _tallyWriteCount, 0); + public int AsyncWriteCount { get; } = Interlocked.Exchange(ref _tallyAsyncWriteCount, 0); + public int AsyncWriteInlineCount { get; } = Interlocked.Exchange(ref _tallyAsyncWriteInlineCount, 0); + public long WriteBytes { get; } = Interlocked.Exchange(ref _tallyWriteBytes, 0); + public int CopyOutCount { get; } = Interlocked.Exchange(ref _tallyCopyOutCount, 0); + public long CopyOutBytes { get; } = Interlocked.Exchange(ref _tallyCopyOutBytes, 0); + public long DiscardAverage { get; } = Interlocked.Exchange(ref _tallyDiscardAverage, 32); + public int DiscardFullCount { get; } = Interlocked.Exchange(ref _tallyDiscardFullCount, 0); + public int DiscardPartialCount { get; } = Interlocked.Exchange(ref _tallyDiscardPartialCount, 0); + public int PipelineFullAsyncCount { get; } = Interlocked.Exchange(ref _tallyPipelineFullAsyncCount, 0); + public int PipelineSendAsyncCount { get; } = Interlocked.Exchange(ref _tallyPipelineSendAsyncCount, 0); + public int PipelineFullSyncCount { get; } = Interlocked.Exchange(ref _tallyPipelineFullSyncCount, 0); + public int BatchWriteCount { get; } = Interlocked.Exchange(ref _tallyBatchWriteCount, 0); + public int BatchWriteFullPageCount { get; } = Interlocked.Exchange(ref _tallyBatchWriteFullPageCount, 0); + public int BatchWritePartialPageCount { get; } = Interlocked.Exchange(ref _tallyBatchWritePartialPageCount, 0); + public int BatchWriteMessageCount { get; } = Interlocked.Exchange(ref _tallyBatchWriteMessageCount, 0); +#endif +} diff --git a/src/RESPite/Internal/IRespInlineParser.cs b/src/RESPite/Internal/IRespInlineParser.cs new file mode 100644 index 000000000..31b054a40 --- /dev/null +++ b/src/RESPite/Internal/IRespInlineParser.cs @@ -0,0 +1,5 @@ +namespace RESPite.Internal; + +internal interface IRespInlineParser // marker interface for readers safe to use on the IO thread +{ +} diff --git a/src/RESPite/Internal/IRespMessage.cs b/src/RESPite/Internal/IRespMessage.cs new file mode 100644 index 000000000..97c0bcf11 --- /dev/null +++ b/src/RESPite/Internal/IRespMessage.cs @@ -0,0 +1,18 @@ +using System.Buffers; +using System.Threading.Tasks.Sources; + +namespace RESPite.Internal; + +internal interface IRespMessage : IValueTaskSource +{ + void Wait(short token, TimeSpan timeout); + void TrySetCanceled(); // only intended for use from cancellation callbacks + bool TrySetCanceled(short token, CancellationToken cancellationToken = default); + bool TrySetException(short token, Exception exception); + bool TrySetResult(short token, scoped ReadOnlySpan response); + bool TrySetResult(short token, in ReadOnlySequence response); + bool TryReserveRequest(short token, out ReadOnlyMemory payload, bool recordSent = true); + void ReleaseRequest(); + bool AllowInlineParsing { get; } + short Token { get; } +} diff --git a/src/RESPite/Internal/Raw.cs b/src/RESPite/Internal/Raw.cs new file mode 100644 index 000000000..65d0c5059 --- /dev/null +++ b/src/RESPite/Internal/Raw.cs @@ -0,0 +1,138 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +#if NETCOREAPP3_0_OR_GREATER +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif + +namespace RESPite.Internal; + +/// +/// Pre-computed payload fragments, for high-volume scenarios / common values. +/// +/// +/// CPU-endianness applies here; we can't just use "const" - however, modern JITs treat "static readonly" *almost* the same as "const", so: meh. +/// +internal static class Raw +{ + public static ulong Create64(ReadOnlySpan bytes, int length) + { + if (length != bytes.Length) + { + throw new ArgumentException($"Length check failed: {length} vs {bytes.Length}, value: {RespConstants.UTF8.GetString(bytes)}", nameof(length)); + } + if (length < 0 || length > sizeof(ulong)) + { + throw new ArgumentOutOfRangeException(nameof(length), $"Invalid length {length} - must be 0-{sizeof(ulong)}"); + } + + // this *will* be aligned; this approach intentionally chosen for parity with write + Span scratch = stackalloc byte[sizeof(ulong)]; + if (length != sizeof(ulong)) scratch.Slice(length).Clear(); + bytes.CopyTo(scratch); + return Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(scratch)); + } + + public static uint Create32(ReadOnlySpan bytes, int length) + { + if (length != bytes.Length) + { + throw new ArgumentException($"Length check failed: {length} vs {bytes.Length}, value: {RespConstants.UTF8.GetString(bytes)}", nameof(length)); + } + if (length < 0 || length > sizeof(uint)) + { + throw new ArgumentOutOfRangeException(nameof(length), $"Invalid length {length} - must be 0-{sizeof(uint)}"); + } + + // this *will* be aligned; this approach intentionally chosen for parity with write + Span scratch = stackalloc byte[sizeof(uint)]; + if (length != sizeof(uint)) scratch.Slice(length).Clear(); + bytes.CopyTo(scratch); + return Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(scratch)); + } + + public static ulong BulkStringEmpty_6 = Create64("$0\r\n\r\n"u8, 6); + + public static ulong BulkStringInt32_M1_8 = Create64("$2\r\n-1\r\n"u8, 8); + public static ulong BulkStringInt32_0_7 = Create64("$1\r\n0\r\n"u8, 7); + public static ulong BulkStringInt32_1_7 = Create64("$1\r\n1\r\n"u8, 7); + public static ulong BulkStringInt32_2_7 = Create64("$1\r\n2\r\n"u8, 7); + public static ulong BulkStringInt32_3_7 = Create64("$1\r\n3\r\n"u8, 7); + public static ulong BulkStringInt32_4_7 = Create64("$1\r\n4\r\n"u8, 7); + public static ulong BulkStringInt32_5_7 = Create64("$1\r\n5\r\n"u8, 7); + public static ulong BulkStringInt32_6_7 = Create64("$1\r\n6\r\n"u8, 7); + public static ulong BulkStringInt32_7_7 = Create64("$1\r\n7\r\n"u8, 7); + public static ulong BulkStringInt32_8_7 = Create64("$1\r\n8\r\n"u8, 7); + public static ulong BulkStringInt32_9_7 = Create64("$1\r\n9\r\n"u8, 7); + public static ulong BulkStringInt32_10_8 = Create64("$2\r\n10\r\n"u8, 8); + + public static ulong BulkStringPrefix_M1_5 = Create64("$-1\r\n"u8, 5); + public static uint BulkStringPrefix_0_4 = Create32("$0\r\n"u8, 4); + public static uint BulkStringPrefix_1_4 = Create32("$1\r\n"u8, 4); + public static uint BulkStringPrefix_2_4 = Create32("$2\r\n"u8, 4); + public static uint BulkStringPrefix_3_4 = Create32("$3\r\n"u8, 4); + public static uint BulkStringPrefix_4_4 = Create32("$4\r\n"u8, 4); + public static uint BulkStringPrefix_5_4 = Create32("$5\r\n"u8, 4); + public static uint BulkStringPrefix_6_4 = Create32("$6\r\n"u8, 4); + public static uint BulkStringPrefix_7_4 = Create32("$7\r\n"u8, 4); + public static uint BulkStringPrefix_8_4 = Create32("$8\r\n"u8, 4); + public static uint BulkStringPrefix_9_4 = Create32("$9\r\n"u8, 4); + public static ulong BulkStringPrefix_10_5 = Create64("$10\r\n"u8, 5); + + public static ulong ArrayPrefix_M1_5 = Create64("*-1\r\n"u8, 5); + public static uint ArrayPrefix_0_4 = Create32("*0\r\n"u8, 4); + public static uint ArrayPrefix_1_4 = Create32("*1\r\n"u8, 4); + public static uint ArrayPrefix_2_4 = Create32("*2\r\n"u8, 4); + public static uint ArrayPrefix_3_4 = Create32("*3\r\n"u8, 4); + public static uint ArrayPrefix_4_4 = Create32("*4\r\n"u8, 4); + public static uint ArrayPrefix_5_4 = Create32("*5\r\n"u8, 4); + public static uint ArrayPrefix_6_4 = Create32("*6\r\n"u8, 4); + public static uint ArrayPrefix_7_4 = Create32("*7\r\n"u8, 4); + public static uint ArrayPrefix_8_4 = Create32("*8\r\n"u8, 4); + public static uint ArrayPrefix_9_4 = Create32("*9\r\n"u8, 4); + public static ulong ArrayPrefix_10_5 = Create64("*10\r\n"u8, 5); + +#if NETCOREAPP3_0_OR_GREATER + private static uint FirstAndLast(char first, char last) + { + Debug.Assert(first < 128 && last < 128, "ASCII please"); + Span scratch = [(byte)first, 0, 0, (byte)last]; + // this *will* be aligned; this approach intentionally chosen for how we read + return Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(scratch)); + } + + public const int CommonRespIndex_Success = 0; + public const int CommonRespIndex_SingleDigitInteger = 1; + public const int CommonRespIndex_DoubleDigitInteger = 2; + public const int CommonRespIndex_SingleDigitString = 3; + public const int CommonRespIndex_DoubleDigitString = 4; + public const int CommonRespIndex_SingleDigitArray = 5; + public const int CommonRespIndex_DoubleDigitArray = 6; + public const int CommonRespIndex_Error = 7; + + public static readonly Vector256 CommonRespPrefixes = Vector256.Create( + FirstAndLast('+', '\r'), // success +OK\r\n + FirstAndLast(':', '\n'), // single-digit integer :4\r\n + FirstAndLast(':', '\r'), // double-digit integer :42\r\n + FirstAndLast('$', '\n'), // 0-9 char string $0\r\n\r\n + FirstAndLast('$', '\r'), // null/10-99 char string $-1\r\n or $10\r\nABCDEFGHIJ\r\n + FirstAndLast('*', '\n'), // 0-9 length array *0\r\n + FirstAndLast('*', '\r'), // null/10-99 length array *-1\r\n or *10\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n + FirstAndLast('-', 'R')); // common errors -ERR something bad happened + + public static readonly Vector256 FirstLastMask = CreateUInt32(0xFF0000FF); + + private static Vector256 CreateUInt32(uint value) + { +#if NET7_0_OR_GREATER + return Vector256.Create(value); +#else + return Vector256.Create(value, value, value, value, value, value, value, value); +#endif + } + +#endif +} diff --git a/src/RESPite/Internal/RespConstants.cs b/src/RESPite/Internal/RespConstants.cs new file mode 100644 index 000000000..2c5a88786 --- /dev/null +++ b/src/RESPite/Internal/RespConstants.cs @@ -0,0 +1,51 @@ +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace RESPite.Internal; + +internal static class RespConstants +{ + public static readonly UTF8Encoding UTF8 = new(false); + + public static ReadOnlySpan CrlfBytes => "\r\n"u8; + + public static readonly ushort CrLfUInt16 = UnsafeCpuUInt16(CrlfBytes); + + public static ReadOnlySpan OKBytes => "OK"u8; + public static readonly ushort OKUInt16 = UnsafeCpuUInt16(OKBytes); + + public static readonly uint BulkStringStreaming = UnsafeCpuUInt32("$?\r\n"u8); + public static readonly uint BulkStringNull = UnsafeCpuUInt32("$-1\r"u8); + + public static readonly uint ArrayStreaming = UnsafeCpuUInt32("*?\r\n"u8); + public static readonly uint ArrayNull = UnsafeCpuUInt32("*-1\r"u8); + + public static ushort UnsafeCpuUInt16(ReadOnlySpan bytes) + => Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(bytes)); + public static ushort UnsafeCpuUInt16(ReadOnlySpan bytes, int offset) + => Unsafe.ReadUnaligned(ref Unsafe.Add(ref MemoryMarshal.GetReference(bytes), offset)); + public static byte UnsafeCpuByte(ReadOnlySpan bytes, int offset) + => Unsafe.Add(ref MemoryMarshal.GetReference(bytes), offset); + public static uint UnsafeCpuUInt32(ReadOnlySpan bytes) + => Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(bytes)); + public static uint UnsafeCpuUInt32(ReadOnlySpan bytes, int offset) + => Unsafe.ReadUnaligned(ref Unsafe.Add(ref MemoryMarshal.GetReference(bytes), offset)); + public static ulong UnsafeCpuUInt64(ReadOnlySpan bytes) + => Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(bytes)); + public static ushort CpuUInt16(ushort bigEndian) + => BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(bigEndian) : bigEndian; + public static uint CpuUInt32(uint bigEndian) + => BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(bigEndian) : bigEndian; + public static ulong CpuUInt64(ulong bigEndian) + => BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(bigEndian) : bigEndian; + + public const int MaxRawBytesInt32 = 11, // "-2147483648" + MaxRawBytesInt64 = 20, // "-9223372036854775808", + MaxProtocolBytesIntegerInt32 = MaxRawBytesInt32 + 3, // ?X10X\r\n where ? could be $, *, etc - usually a length prefix + MaxProtocolBytesBulkStringIntegerInt32 = MaxRawBytesInt32 + 7, // $NN\r\nX11X\r\n for NN (length) 1-11 + MaxProtocolBytesBulkStringIntegerInt64 = MaxRawBytesInt64 + 7, // $NN\r\nX20X\r\n for NN (length) 1-20 + MaxRawBytesNumber = 20, // note G17 format, allow 20 for payload + MaxProtocolBytesBytesNumber = MaxRawBytesNumber + 7; // $NN\r\nX...X\r\n for NN (length) 1-20 +} diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs new file mode 100644 index 000000000..c903e64c0 --- /dev/null +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -0,0 +1,436 @@ +using System.Buffers; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading.Tasks.Sources; +using RESPite.Messages; + +namespace RESPite.Internal; + +internal abstract class RespMessageBase : IRespMessage, IValueTaskSource +{ + private CancellationToken _cancellationToken; + private CancellationTokenRegistration _cancellationTokenRegistration; + + private ReadOnlyMemory _request; + private object? _requestOwner; + + private int _requestRefCount; + private int _flags; + private ManualResetValueTaskSourceCore _asyncCore; + + private const int + Flag_Sent = 1 << 0, // the request has been sent + Flag_OutcomeKnown = 1 << 1, // controls which code flow gets to set an outcome + Flag_Complete = 1 << 2, // indicates whether all follow-up has completed + Flag_NoPulse = 1 << 4, // don't pulse when completing - either async, or timeout + Flag_Parser = 1 << 5, // we have a parser + Flag_MetadataParser = 1 << 6, // the parser wants to consume metadata + Flag_InlineParser = 1 << 7, // we can safely use the parser on the IO thread + Flag_Doomed = 1 << 8; // something went wrong, do not recyle + + protected abstract TResponse Parse(ref RespReader reader); + + protected void InitParser(object? parser) + { + if (parser is not null) + { + int flags = Flag_Parser; + if (parser is IRespMetadataParser) flags |= Flag_MetadataParser; + if (parser is IRespInlineParser) flags |= Flag_InlineParser; + SetFlag(flags); + } + } + + public bool AllowInlineParsing => HasFlag(Flag_InlineParser); + + [Conditional("DEBUG")] + private void DebugAssertPending() => Debug.Assert( + GetStatus(_asyncCore.Version) == ValueTaskSourceStatus.Pending & !HasFlag(Flag_OutcomeKnown), + "Message should be in a pending state"); + + public bool TrySetResult(short token, scoped ReadOnlySpan response) + { + DebugAssertPending(); + if (HasFlag(Flag_OutcomeKnown) | _asyncCore.Version != token) return false; + var flags = _flags & (Flag_MetadataParser | Flag_Parser); + switch (flags) + { + case Flag_Parser: + case Flag_Parser | Flag_MetadataParser: + try + { + RespReader reader = new(response); + if ((flags & Flag_MetadataParser) == 0) + { + reader.MoveNext(); + } + + return TrySetResult(Parse(ref reader)); + } + catch (Exception ex) + { + return TrySetException(ex); + } + default: + return TrySetResult(default(TResponse)!); + } + } + + public short Token => _asyncCore.Version; + + public bool TrySetResult(short token, in ReadOnlySequence response) + { + DebugAssertPending(); + if (HasFlag(Flag_OutcomeKnown) | _asyncCore.Version != token) return false; + var flags = _flags & (Flag_MetadataParser | Flag_Parser); + switch (flags) + { + case Flag_Parser: + case Flag_Parser | Flag_MetadataParser: + try + { + RespReader reader = new(response); + if ((flags & Flag_MetadataParser) == 0) + { + reader.MoveNext(); + } + + return TrySetResult(Parse(ref reader)); + } + catch (Exception ex) + { + return TrySetException(ex); + } + default: + return TrySetResult(default(TResponse)!); + } + } + + private bool SetFlag(int flag) + { + Debug.Assert(flag != 0, "trying to set a zero flag"); +#if NET5_0_OR_GREATER + return (Interlocked.Or(ref _flags, flag) & flag) == 0; +#else + while (true) + { + var oldValue = Volatile.Read(ref _flags); + var newValue = oldValue | flag; + if (oldValue == newValue || + Interlocked.CompareExchange(ref _flags, newValue, oldValue) == oldValue) + { + return (oldValue & flag) == 0; + } + } +#endif + } + + // in the "any" sense + private bool HasFlag(int flag) => (Volatile.Read(ref _flags) & flag) != 0; + + public RespMessageBase Init(byte[] oversized, int offset, int length, ArrayPool? pool, CancellationToken cancellation) + { + DebugAssertPending(); + Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); + _request = new ReadOnlyMemory(oversized, offset, length); + _requestOwner = pool; + _requestRefCount = 1; + if (cancellation.CanBeCanceled) + { + _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellation); + } + return this; + } + + public RespMessageBase SetRequest(ReadOnlyMemory request, IDisposable? owner, CancellationToken cancellation) + { + DebugAssertPending(); + Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); + _request = request; + _requestOwner = owner; + _requestRefCount = 1; + if (cancellation.CanBeCanceled) + { + _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellation); + } + return this; + } + + private void UnregisterCancellation() + { + _cancellationTokenRegistration.Dispose(); + _cancellationTokenRegistration = default; + _cancellationToken = CancellationToken.None; + } + + public virtual void Reset(bool recycle) + { + Debug.Assert( + !recycle || _asyncCore.GetStatus(_asyncCore.Version) == ValueTaskSourceStatus.Succeeded, + "We should only be recycling completed messages"); + // note we only reset on success, and on + // success we've already unregistered cancellation + _request = default; + _requestOwner = null; + _requestRefCount = 0; + _flags = 0; + _asyncCore.Reset(); + } + + public bool TryReserveRequest(short token, out ReadOnlyMemory payload, bool recordSent = true) + { + while (true) // redo in case of CEX failure + { + Debug.Assert(_asyncCore.GetStatus(_asyncCore.Version) == ValueTaskSourceStatus.Pending); + + var oldCount = Volatile.Read(ref _requestRefCount); + if (oldCount == 0 | token != _asyncCore.Version) + { + payload = default; + return false; + } + if (Interlocked.CompareExchange(ref _requestRefCount, checked(oldCount + 1), oldCount) == oldCount) + { + if (recordSent) SetFlag(Flag_Sent); + + payload = _request; + return true; + } + } + } + + public void ReleaseRequest() + { + if (!TryReleaseRequest()) ThrowReleased(); + + static void ThrowReleased() => + throw new InvalidOperationException("The request payload has already been released"); + } + + private bool + TryReleaseRequest() // bool here means "it wasn't already zero"; it doesn't mean "it became zero" + { + while (true) + { + var oldCount = Volatile.Read(ref _requestRefCount); + if (oldCount == 0) return false; + if (Interlocked.CompareExchange(ref _requestRefCount, oldCount - 1, oldCount) == oldCount) + { + if (oldCount == 1) // we were the last one; recycle + { + if (_requestOwner is ArrayPool pool) + { + if (MemoryMarshal.TryGetArray(_request, out var segment)) + { + pool.Return(segment.Array!); + } + } + + if (_requestOwner is IDisposable owner) + { + owner.Dispose(); + } + + _request = default; + _requestOwner = null; + } + + return true; + } + } + } + + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _asyncCore.GetStatus(token); + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _asyncCore.GetStatus(token); + + /* if they're awaiting our object directly (i.e. we don't need to worry about Task pre-checking things), + then we can tell them that a message hasn't been sent, for example transactions / batches */ + public ValueTaskSourceStatus GetStatus(short token) + { + // we'd rather see a token error, so check that first + // (in reality, we expect the token to be right almost always) + var status = _asyncCore.GetStatus(token); + if (!HasFlag(Flag_Sent)) ThrowNotSent(); + return status; + } + + private void CheckToken(short token) + { + if (token != _asyncCore.Version) // use cheap test + { + _ = _asyncCore.GetStatus(token); // get consistent exception message + } + } + + private static void ThrowNotSent() + => throw new InvalidOperationException( + "This command has not yet been sent; awaiting is not possible. If this is a transaction or batch, you must execute that first."); + + public void OnCompleted( + Action continuation, + object? state, + short token, + ValueTaskSourceOnCompletedFlags flags) + { + _asyncCore.OnCompleted(continuation, state, token, flags); + SetFlag(Flag_NoPulse); // async doesn't need to be pulsed + } + + // spoof untyped on top of typed + void IValueTaskSource.GetResult(short token) => _ = GetResult(token); + void IRespMessage.Wait(short token, TimeSpan timeout) => _ = Wait(token, timeout); + + private bool TrySetOutcomeKnown() + { + DebugAssertPending(); + if (!SetFlag(Flag_OutcomeKnown)) return false; + UnregisterCancellation(); + return true; + } + + public TResponse Wait(short token, TimeSpan timeout) + { + switch (Volatile.Read(ref _flags) & (Flag_Complete | Flag_Sent)) + { + case Flag_Sent: // this is the normal case + break; + case Flag_Complete | Flag_Sent: // already complete + return GetResult(token); + default: + ThrowNotSent(); + break; + } + + bool isTimeout = false; + CheckToken(token); + lock (this) + { + switch (Volatile.Read(ref _flags) & Flag_Complete | Flag_NoPulse) + { + case Flag_NoPulse | Flag_Complete: + case Flag_Complete: + break; // fine, we're complete + case 0: + // THIS IS OUR EXPECTED BRANCH; not complete, and will pulse + if (timeout == TimeSpan.Zero) + { + Monitor.Wait(this); + } + else if (!Monitor.Wait(this, timeout)) + { + isTimeout = true; + SetFlag(Flag_NoPulse); // no point in being woken, we're exiting + } + + break; + case Flag_NoPulse: + ThrowWillNotPulse(); + break; + } + } + + UnregisterCancellation(); + if (isTimeout) TrySetTimeout(); + + return GetResult(token); + + static void ThrowWillNotPulse() => throw new InvalidOperationException( + "This operation cannot be waited because it entered async/await mode - most likely by calling AsTask()"); + } + + private bool TrySetResult(TResponse response) + { + if (!TrySetOutcomeKnown()) return false; + + _asyncCore.SetResult(response); + SetFullyComplete(success: true); + return true; + } + + private bool TrySetTimeout() + { + if (!TrySetOutcomeKnown()) return false; + + _asyncCore.SetException(new TimeoutException()); + SetFullyComplete(success: false); + return true; + } + + public bool TrySetCanceled(short token, CancellationToken cancellationToken = default) + { + if (!cancellationToken.IsCancellationRequested) + { + // use our own token if nothing more specific supplied + cancellationToken = _cancellationToken; + } + return TrySetCanceled(cancellationToken); + } + + // this is the path used by cancellation registration callbacks; always use our own token + void IRespMessage.TrySetCanceled() => TrySetCanceled(_cancellationToken); + + private bool TrySetCanceled(CancellationToken cancellationToken) + { + if (!TrySetOutcomeKnown()) return false; + _asyncCore.SetException(new OperationCanceledException(cancellationToken)); + SetFullyComplete(success: false); + return true; + } + + public bool TrySetException(short token, Exception exception) + => token == _asyncCore.Version && TrySetException(exception); + + private bool TrySetException(Exception exception) + { + if (!TrySetOutcomeKnown()) return false; // first winner only + _asyncCore.SetException(exception); + SetFullyComplete(success: false); + return true; + } + + private void SetFullyComplete(bool success) + { + var pulse = !HasFlag(Flag_NoPulse); + SetFlag(success + ? (Flag_Complete | Flag_NoPulse) + : (Flag_Complete | Flag_NoPulse | Flag_Doomed)); + + // for safety, always take the lock unless we know they've actively exited + if (pulse) + { + lock (this) + { + Monitor.PulseAll(this); + } + } + } + + private TResponse ThrowFailure(short token) + { + try + { + return _asyncCore.GetResult(token); + } + finally + { + // we're not recycling; this is for GC reasons only + Reset(false); + } + } + + public TResponse GetResult(short token) + { + // failure uses some try/catch logic, let's put that to one side + if (HasFlag(Flag_Doomed)) return ThrowFailure(token); + var result = _asyncCore.GetResult(token); + /* + If we get here, we're successful; increment "version"/"token" *immediately*. Technically + we could defer to when it is reused (after recycling), but then repeated calls will appear + to work for a while, which might lead to undetected problems in local builds (without much concurrency), + and we'd rather make people know that there's a problem immediately. This also means that any + continuation primitives (callback/state) are available for GC. + */ + Reset(true); + return result; + } +} diff --git a/src/RESPite/Internal/RespMessageBase_Typed.cs b/src/RESPite/Internal/RespMessageBase_Typed.cs new file mode 100644 index 000000000..1b368bad3 --- /dev/null +++ b/src/RESPite/Internal/RespMessageBase_Typed.cs @@ -0,0 +1,30 @@ +using RESPite.Messages; + +namespace RESPite.Internal; + +internal sealed class RespMessage : RespMessageBase +{ + private IRespParser? _parser; + [ThreadStatic] + // used for object recycling of the async machinery + private static RespMessage? _threadStaticSpare; + + internal static RespMessage Get(IRespParser? parser) + { + RespMessage obj = _threadStaticSpare ?? new(); + _threadStaticSpare = null; + obj._parser = parser; + obj.InitParser(parser); + return obj; + } + + private RespMessage() { } + + protected override TResponse Parse(ref RespReader reader) => _parser!.Parse(ref reader); + + public override void Reset(bool recycle) + { + _parser = null!; + base.Reset(recycle); + } +} diff --git a/src/RESPite/Internal/RespMessageBase_Typed_Stateful.cs b/src/RESPite/Internal/RespMessageBase_Typed_Stateful.cs new file mode 100644 index 000000000..c0a46f363 --- /dev/null +++ b/src/RESPite/Internal/RespMessageBase_Typed_Stateful.cs @@ -0,0 +1,33 @@ +using System.Runtime.CompilerServices; +using RESPite.Messages; + +namespace RESPite.Internal; + +internal sealed class RespMessage : RespMessageBase +{ + private TState _state; + private IRespParser? _parser; + [ThreadStatic] + // used for object recycling of the async machinery + private static RespMessage? _threadStaticSpare; + internal static RespMessage Get(in TState state, IRespParser? parser) + { + RespMessage obj = _threadStaticSpare ?? new(); + _threadStaticSpare = null; + obj._state = state; + obj._parser = parser; + obj.InitParser(parser); + return obj; + } + + private RespMessage() => Unsafe.SkipInit(out _state); + + protected override TResponse Parse(ref RespReader reader) => _parser!.Parse(in _state, ref reader); + + public override void Reset(bool recycle) + { + _state = default!; + _parser = null!; + base.Reset(recycle); + } +} diff --git a/src/RESPite/Internal/RespOperationExtensions.cs b/src/RESPite/Internal/RespOperationExtensions.cs new file mode 100644 index 000000000..23ed0f952 --- /dev/null +++ b/src/RESPite/Internal/RespOperationExtensions.cs @@ -0,0 +1,18 @@ +using System.Runtime.CompilerServices; + +namespace RESPite.Internal; + +public static class RespOperationExtensions +{ +#if PREVIEW_LANGVER + extension(in RespOperation operation) + { + // since this is valid... + public ref readonly RespOperation Self => ref operation; + + // so is this (the types are layout-identical) + public ref readonly RespOperation Untyped => ref Unsafe.As, RespOperation>( + ref Unsafe.AsRef(in operation)); + } +#endif +} diff --git a/src/RESPite/Internal/StreamConnection.cs b/src/RESPite/Internal/StreamConnection.cs new file mode 100644 index 000000000..bc348932b --- /dev/null +++ b/src/RESPite/Internal/StreamConnection.cs @@ -0,0 +1,717 @@ +#define PARSE_DETAIL // additional trace info in CommitAndParseFrames + +#if DEBUG +#define PARSE_DETAIL // always enable this in debug builds +#endif + +using System.Buffers; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using RESPite.Messages; + +namespace RESPite.Internal; + +internal sealed class StreamConnection : IRespConnection +{ + private bool _isDoomed; + private RespScanState _readScanState; + private CycleBuffer _readBuffer, _writeBuffer; + private readonly RespContext _context; + public ref readonly RespContext Context => ref _context; + public bool CanWrite => Volatile.Read(ref _readStatus) == WRITER_AVAILABLE; + + public int Outstanding => _outstanding.Count; + + public Task Reader { get; private set; } = Task.CompletedTask; + + private readonly Stream tail; + private ConcurrentQueue _outstanding = new(); + public RespConfiguration Configuration { get; } + + public StreamConnection(RespConfiguration configuration, Stream tail, bool asyncRead = true) + { + Configuration = configuration; + if (!(tail.CanRead && tail.CanWrite)) Throw(); + this.tail = tail; + var memoryPool = configuration.GetService>(); + _readBuffer = CycleBuffer.Create(memoryPool); + _writeBuffer = CycleBuffer.Create(memoryPool); + if (asyncRead) + { + Reader = Task.Run(ReadAllAsync); + } + else + { + new Thread(ReadAll).Start(); + } + + _context = RespContext.For(this); + + static void Throw() => throw new ArgumentException("Stream must be readable and writable", nameof(tail)); + } + + public RespMode Mode { get; set; } = RespMode.Resp2; + + public enum RespMode + { + Resp2, + Resp2PubSub, + Resp3, + } + + private static byte[]? SharedNoLease; + + private bool CommitAndParseFrames(int bytesRead) + { + if (bytesRead <= 0) + { + return false; + } + + // let's bypass a bunch of ldarg0 by hoisting the field-refs (this is **NOT** a struct copy; emphasis "ref") + ref RespScanState state = ref _readScanState; + ref CycleBuffer readBuffer = ref _readBuffer; + +#if PARSE_DETAIL + string src = $"parse {bytesRead}"; + try +#endif + { + Debug.Assert(readBuffer.GetCommittedLength() >= 0, "multi-segment running-indices are corrupt"); +#if PARSE_DETAIL + src += $" ({readBuffer.GetCommittedLength()}+{bytesRead}-{state.TotalBytes})"; +#endif + Debug.Assert( + bytesRead <= readBuffer.UncommittedAvailable, + $"Insufficient bytes in {nameof(CommitAndParseFrames)}; got {bytesRead}, Available={readBuffer.UncommittedAvailable}"); + readBuffer.Commit(bytesRead); +#if PARSE_DETAIL + src += $",total {readBuffer.GetCommittedLength()}"; +#endif + var scanner = RespFrameScanner.Default; + + OperationStatus status = OperationStatus.NeedMoreData; + if (readBuffer.TryGetCommitted(out var fullSpan)) + { + int fullyConsumed = 0; + var toParse = fullSpan.Slice((int)state.TotalBytes); // skip what we've already parsed + + Debug.Assert(!toParse.IsEmpty); + while (true) + { +#if PARSE_DETAIL + src += $",span {toParse.Length}"; +#endif + int totalBytesBefore = (int)state.TotalBytes; + if (toParse.Length < RespScanState.MinBytes + || (status = scanner.TryRead(ref state, toParse)) != OperationStatus.Done) + { + break; + } + + Debug.Assert( + state is + { + IsComplete: true, TotalBytes: >= RespScanState.MinBytes, Prefix: not RespPrefix.None + }, + "Invalid RESP read state"); + + // extract the frame + var bytes = (int)state.TotalBytes; +#if PARSE_DETAIL + src += $",frame {bytes}"; +#endif + // send the frame somewhere (note this is the *full* frame, not just the bit we just parsed) + OnResponseFrame(state.Prefix, fullSpan.Slice(fullyConsumed, bytes), ref SharedNoLease); + + // update our buffers to the unread potions and reset for a new RESP frame + fullyConsumed += bytes; + toParse = toParse.Slice(bytes - totalBytesBefore); // move past the extra bytes we just read + state = default; + status = OperationStatus.NeedMoreData; + } + + readBuffer.DiscardCommitted(fullyConsumed); + } + else // the same thing again, but this time with multi-segment sequence + { + var fullSequence = readBuffer.GetAllCommitted(); + Debug.Assert( + fullSequence is { IsEmpty: false, IsSingleSegment: false }, + "non-trivial sequence expected"); + + long fullyConsumed = 0; + var toParse = fullSequence.Slice((int)state.TotalBytes); // skip what we've already parsed + while (true) + { +#if PARSE_DETAIL + src += $",ros {toParse.Length}"; +#endif + int totalBytesBefore = (int)state.TotalBytes; + if (toParse.Length < RespScanState.MinBytes + || (status = scanner.TryRead(ref state, toParse)) != OperationStatus.Done) + { + break; + } + + Debug.Assert( + state is + { + IsComplete: true, TotalBytes: >= RespScanState.MinBytes, Prefix: not RespPrefix.None + }, + "Invalid RESP read state"); + + // extract the frame + var bytes = (int)state.TotalBytes; +#if PARSE_DETAIL + src += $",frame {bytes}"; +#endif + // send the frame somewhere (note this is the *full* frame, not just the bit we just parsed) + OnResponseFrame(state.Prefix, fullSequence.Slice(fullyConsumed, bytes)); + + // update our buffers to the unread potions and reset for a new RESP frame + fullyConsumed += bytes; + toParse = toParse.Slice(bytes - totalBytesBefore); // move past the extra bytes we just read + state = default; + status = OperationStatus.NeedMoreData; + } + + readBuffer.DiscardCommitted(fullyConsumed); + } + + if (status != OperationStatus.NeedMoreData) + { + ThrowStatus(status); + + static void ThrowStatus(OperationStatus status) => + throw new InvalidOperationException($"Unexpected operation status: {status}"); + } + + return true; + } +#if PARSE_DETAIL + catch (Exception ex) + { + Debug.WriteLine($"{nameof(CommitAndParseFrames)}: {ex.Message}"); + Debug.WriteLine(src); + ActivationHelper.DebugBreak(); + throw new InvalidOperationException($"{src} lead to {ex.Message}", ex); + } +#endif + } + + private async Task ReadAllAsync() + { + try + { + int read; + do + { + var buffer = _readBuffer.GetUncommittedMemory(); + var pending = tail.ReadAsync(buffer, CancellationToken.None); +#if DEBUG + bool inline = pending.IsCompleted; +#endif + read = await pending.ConfigureAwait(false); +#if DEBUG + DebugCounters.OnAsyncRead(read, inline); +#endif + } + // another formatter glitch + while (CommitAndParseFrames(read)); + + Volatile.Write(ref _readStatus, READER_COMPLETED); + _readBuffer.Release(); // clean exit, we can recycle + } + catch (Exception ex) + { + OnReadException(ex); + throw; + } + finally + { + OnReadAllFinally(); + } + } + + private void ReadAll() + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + Reader = tcs.Task; + try + { + int read; + do + { +#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER + var buffer = _readBuffer.GetUncommittedSpan(); + read = tail.Read(buffer); +#else + var buffer = _readBuffer.GetUncommittedMemory(); + read = tail.Read(buffer); +#endif + DebugCounters.OnRead(read); + } + // another formatter glitch + while (CommitAndParseFrames(read)); + + Volatile.Write(ref _readStatus, READER_COMPLETED); + _readBuffer.Release(); // clean exit, we can recycle + tcs.TrySetResult(null); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + OnReadException(ex); + } + finally + { + OnReadAllFinally(); + } + } + + private void OnReadException(Exception ex) + { + _fault ??= ex; + Volatile.Write(ref _readStatus, READER_FAILED); + Debug.WriteLine($"Reader failed: {ex.Message}"); + ActivationHelper.DebugBreak(); + while (_outstanding.TryDequeue(out var pending)) + { + pending.Message.TrySetException(pending.Token, ex); + } + } + + private void OnReadAllFinally() + { + Doom(); + _readBuffer.Release(); + + // abandon anything in the queue + while (_outstanding.TryDequeue(out var pending)) + { + pending.Message.TrySetCanceled(pending.Token, CancellationToken.None); + } + } + + private static readonly ulong + ArrayPong_LC_Bulk = RespConstants.UnsafeCpuUInt64("*2\r\n$4\r\npong\r\n$"u8), + ArrayPong_UC_Bulk = RespConstants.UnsafeCpuUInt64("*2\r\n$4\r\nPONG\r\n$"u8), + ArrayPong_LC_Simple = RespConstants.UnsafeCpuUInt64("*2\r\n+pong\r\n$"u8), + ArrayPong_UC_Simple = RespConstants.UnsafeCpuUInt64("*2\r\n+PONG\r\n$"u8); + + private static readonly uint + pong = RespConstants.UnsafeCpuUInt32("pong"u8), + PONG = RespConstants.UnsafeCpuUInt32("PONG"u8); + + private void OnOutOfBand(ReadOnlySpan payload, ref byte[]? lease) + { + throw new NotImplementedException(nameof(OnOutOfBand)); + } + + private void OnResponseFrame(RespPrefix prefix, ReadOnlySequence payload) + { + if (payload.IsSingleSegment) + { +#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER + OnResponseFrame(prefix, payload.FirstSpan, ref SharedNoLease); +#else + OnResponseFrame(prefix, payload.First.Span, ref SharedNoLease); +#endif + } + else + { + var len = checked((int)payload.Length); + byte[]? oversized = ArrayPool.Shared.Rent(len); + payload.CopyTo(oversized); + OnResponseFrame(prefix, new(oversized, 0, len), ref oversized); + + // the lease could have been claimed by the activation code (to prevent another memcpy); otherwise, free + if (oversized is not null) + { + ArrayPool.Shared.Return(oversized); + } + } + } + + [Conditional("DEBUG")] + private static void DebugValidateSingleFrame(ReadOnlySpan payload) + { + var reader = new RespReader(payload); + reader.MoveNext(); + reader.SkipChildren(); + if (reader.TryMoveNext()) + { + throw new InvalidOperationException($"Unexpected trailing {reader.Prefix}"); + } + + if (reader.ProtocolBytesRemaining != 0) + { + var copy = reader; // leave reader alone for inspection + var prefix = copy.TryMoveNext() ? copy.Prefix : RespPrefix.None; + throw new InvalidOperationException( + $"Unexpected additional {reader.ProtocolBytesRemaining} bytes remaining, {prefix}"); + } + } + + private void OnResponseFrame(RespPrefix prefix, ReadOnlySpan payload, ref byte[]? lease) + { + DebugValidateSingleFrame(payload); + if (prefix == RespPrefix.Push || + (prefix == RespPrefix.Array && Mode is RespMode.Resp2PubSub && !IsArrayPong(payload))) + { + // out-of-band; pub/sub etc + OnOutOfBand(payload, ref lease); + return; + } + + // request/response; match to inbound + if (_outstanding.TryDequeue(out var pending)) + { + ActivationHelper.ProcessResponse(pending, payload, ref lease); + } + else + { + Debug.Fail("Unexpected response without pending message!"); + } + + static bool IsArrayPong(ReadOnlySpan payload) + { + if (payload.Length >= sizeof(ulong)) + { + var raw = RespConstants.UnsafeCpuUInt64(payload); + if (raw == ArrayPong_LC_Bulk + || raw == ArrayPong_UC_Bulk + || raw == ArrayPong_LC_Simple + || raw == ArrayPong_UC_Simple) + { + var reader = new RespReader(payload); + return reader.TryMoveNext() // have root + && reader.Prefix == RespPrefix.Array // root is array + && reader.TryMoveNext() // have first child + && (reader.IsInlneCpuUInt32(pong) || reader.IsInlneCpuUInt32(PONG)); // pong + } + } + + return false; + } + } + + private int _writeStatus, _readStatus; + private const int WRITER_AVAILABLE = 0, WRITER_TAKEN = 1, WRITER_DOOMED = 2; + private const int READER_ACTIVE = 0, READER_FAILED = 1, READER_COMPLETED = 2; + + private void TakeWriter() + { + var status = Interlocked.CompareExchange(ref _writeStatus, WRITER_TAKEN, WRITER_AVAILABLE); + if (status != WRITER_AVAILABLE) ThrowWriterNotAvailable(); + Debug.Assert(Volatile.Read(ref _writeStatus) == WRITER_TAKEN, "writer should be taken"); + } + + private void ThrowWriterNotAvailable() + { + var fault = Volatile.Read(ref _fault); + var status = Volatile.Read(ref _writeStatus); + var msg = status switch + { + WRITER_TAKEN => "A write operation is already in progress; concurrent writes are not supported.", + WRITER_DOOMED when fault is not null => "This connection is terminated; no further writes are possible: " + + fault.Message, + WRITER_DOOMED => "This connection is terminated; no further writes are possible.", + _ => $"Unexpected writer status: {status}", + }; + throw fault is null ? new InvalidOperationException(msg) : new InvalidOperationException(msg, fault); + } + + private Exception? _fault; + + private void ReleaseWriter(int status = WRITER_AVAILABLE) + { + if (status == WRITER_AVAILABLE && _isDoomed) + { + status = WRITER_DOOMED; + } + + Interlocked.CompareExchange(ref _writeStatus, status, WRITER_TAKEN); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void OnRequestUnavailable(in RespOperation message) + { + if (!message.IsCompleted) + { + // make sure they know something is wrong + message.Message.TrySetException(message.Token, new InvalidOperationException("Request is not available")); + } + } + + public void Send(in RespOperation message) + { + bool releaseRequest = message.Message.TryReserveRequest(message.Token, out var bytes); + if (!releaseRequest) + { + OnRequestUnavailable(message); + return; + } + + DebugValidateSingleFrame(bytes.Span); + TakeWriter(); + try + { + _outstanding.Enqueue(message); + releaseRequest = false; // once we write, only release on success +#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER + tail.Write(bytes.Span); +#else + tail.Write(bytes); +#endif + DebugCounters.OnWrite(bytes.Length); + ReleaseWriter(); + message.Message.ReleaseRequest(); + } + catch (Exception ex) + { + Debug.WriteLine($"Writer failed: {ex.Message}"); + ActivationHelper.DebugBreak(); + ReleaseWriter(WRITER_DOOMED); + if (releaseRequest) message.Message.ReleaseRequest(); + throw; + } + } + + public void Send(ReadOnlySpan messages) + { + switch (messages.Length) + { + case 0: + return; + case 1: + Send(messages[0]); + return; + } + + TakeWriter(); + IRespMessage? toRelease = null; + try + { + foreach (var message in messages) + { + if (message.Message.TryReserveRequest(message.Token, out var bytes)) + { + toRelease = message.Message; + } + else + { + OnRequestUnavailable(message); + continue; + } + + DebugValidateSingleFrame(bytes.Span); + _outstanding.Enqueue(message); + toRelease = null; // once we write, only release on success +#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER + tail.Write(bytes.Span); +#else + tail.Write(bytes); +#endif + DebugCounters.OnWrite(bytes.Length); + ReleaseWriter(); + message.Message.ReleaseRequest(); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Writer failed: {ex.Message}"); + ActivationHelper.DebugBreak(); + ReleaseWriter(WRITER_DOOMED); + toRelease?.ReleaseRequest(); + foreach (var message in messages) + { + // assume all bad + message.Message.TrySetException(message.Token, ex); + } + + throw; + } + } + + public Task SendAsync(in RespOperation message) + { + bool releaseRequest = message.Message.TryReserveRequest(message.Token, out var bytes); + if (!releaseRequest) + { + OnRequestUnavailable(message); + return Task.CompletedTask; + } + + DebugValidateSingleFrame(bytes.Span); + TakeWriter(); + try + { + _outstanding.Enqueue(message); + releaseRequest = false; // once we write, only release on success + var pendingWrite = tail.WriteAsync(bytes, CancellationToken.None); + if (!pendingWrite.IsCompleted) + { + return AwaitedSingleWithToken( + this, + pendingWrite, +#if DEBUG + bytes.Length, +#endif + message.Message); + } + + pendingWrite.GetAwaiter().GetResult(); + DebugCounters.OnAsyncWrite(bytes.Length, true); + ReleaseWriter(); + message.Message.ReleaseRequest(); + return Task.CompletedTask; + } + catch (Exception ex) + { + Debug.WriteLine($"Writer failed: {ex.Message}"); + ActivationHelper.DebugBreak(); + ReleaseWriter(WRITER_DOOMED); + if (releaseRequest) message.Message.ReleaseRequest(); + throw; + } + + static async Task AwaitedSingleWithToken( + StreamConnection @this, + ValueTask pendingWrite, +#if DEBUG + int length, +#endif + IRespMessage message) + { + try + { + await pendingWrite.ConfigureAwait(false); +#if DEBUG + DebugCounters.OnAsyncWrite(length, false); +#endif + @this.ReleaseWriter(); + message.ReleaseRequest(); + } + catch + { + @this.ReleaseWriter(WRITER_DOOMED); + throw; + } + } + } + + public Task SendAsync(ReadOnlyMemory messages) + { + switch (messages.Length) + { + case 0: + return Task.CompletedTask; + case 1: + return SendAsync(messages.Span[0]); + default: + return CombineAndSendMultipleAsync(this, messages); + } + } + + private async Task CombineAndSendMultipleAsync(StreamConnection @this, ReadOnlyMemory messages) + { + TakeWriter(); + IRespMessage? toRelease = null; + int definitelySent = 0; + try + { + int length = messages.Length; + for (int i = 0; i < length; i++) + { + var message = messages.Span[i]; + if (!message.Message.TryReserveRequest(message.Token, out var bytes)) + { + OnRequestUnavailable(message); + continue; // skip this message + } + + toRelease = message.Message; + // append to the scratch and consider written (even though we haven't actually) + _writeBuffer.Write(bytes.Span); + toRelease = null; + message.Message.ReleaseRequest(); + @this._outstanding.Enqueue(message); + + // do we have any full segments? if so, write them and narrow "messages" + if (_writeBuffer.TryGetFirstCommittedMemory(CycleBuffer.GetFullPagesOnly, out var memory)) + { + do + { + var pending = tail.WriteAsync(memory, CancellationToken.None); + DebugCounters.OnAsyncWrite(memory.Length, inline: pending.IsCompleted); + await pending.ConfigureAwait(false); + DebugCounters.OnBatchWriteFullPage(); + + _writeBuffer.DiscardCommitted(memory.Length); // mark the data as no longer needed + } + // and if one buffer was full, we might have multiple (think: "large BLOB outbound") + while (_writeBuffer.TryGetFirstCommittedMemory(CycleBuffer.GetFullPagesOnly, out memory)); + + definitelySent = i + 1; // for exception handling: no need to doom these if later fails + } + } + + // and send any remaining data + while (_writeBuffer.TryGetFirstCommittedMemory(CycleBuffer.GetAnything, out var memory)) + { + var pending = tail.WriteAsync(memory, CancellationToken.None); + DebugCounters.OnAsyncWrite(memory.Length, inline: pending.IsCompleted); + await pending.ConfigureAwait(false); + DebugCounters.OnBatchWritePartialPage(); + + _writeBuffer.DiscardCommitted(memory.Length); // mark the data as no longer needed + } + + Debug.Assert(_writeBuffer.CommittedIsEmpty, "should have written everything"); + + ReleaseWriter(); + DebugCounters.OnBatchWrite(messages.Length); + } + catch (Exception ex) + { + Debug.WriteLine($"Writer failed: {ex.Message}"); + ActivationHelper.DebugBreak(); + ReleaseWriter(WRITER_DOOMED); + toRelease?.ReleaseRequest(); + foreach (var message in messages.Span.Slice(start: definitelySent)) + { + message.Message.TrySetException(message.Token, ex); + } + + throw; + } + } + + private void Doom() + { + _isDoomed = true; // without a reader, there's no point writing + Interlocked.CompareExchange(ref _writeStatus, WRITER_DOOMED, WRITER_AVAILABLE); + } + + public void Dispose() + { + _fault ??= new ObjectDisposedException(ToString()); + Doom(); + tail.Dispose(); + } + + public override string ToString() => nameof(StreamConnection); + + public ValueTask DisposeAsync() + { +#if COREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + return tail.DisposeAsync().AsTask(); +#else + Dispose(); + return default; +#endif + } +} diff --git a/src/RESPite/Messages/IRespMetadataParser.cs b/src/RESPite/Messages/IRespMetadataParser.cs new file mode 100644 index 000000000..4d7943a4e --- /dev/null +++ b/src/RESPite/Messages/IRespMetadataParser.cs @@ -0,0 +1,10 @@ +namespace RESPite.Messages; + +/// +/// When implemented by a or , +/// indicates that the reader should not be pre-initialized to the first node - which would otherwise +/// consume attributes and errors. +/// +public interface IRespMetadataParser +{ +} diff --git a/src/RESPite/Messages/IRespParser_Typed.cs b/src/RESPite/Messages/IRespParser_Typed.cs new file mode 100644 index 000000000..25f96ec3b --- /dev/null +++ b/src/RESPite/Messages/IRespParser_Typed.cs @@ -0,0 +1,13 @@ +namespace RESPite.Messages; + +/// +/// Parses a RESP response into a typed value of type . +/// +/// The type of value being parsed. +public interface IRespParser +{ + /// + /// Parse into a . + /// + TResponse Parse(ref RespReader reader); +} diff --git a/src/RESPite/Messages/IRespParser_Typed_Stateful.cs b/src/RESPite/Messages/IRespParser_Typed_Stateful.cs new file mode 100644 index 000000000..3e549e92c --- /dev/null +++ b/src/RESPite/Messages/IRespParser_Typed_Stateful.cs @@ -0,0 +1,12 @@ +namespace RESPite.Messages; + +public interface IRespParser +{ + /// + /// Parse into a , + /// using the state from . + /// + /// The state to use when parsing. + /// The reader to parse. + TResponse Parse(in TState state, ref RespReader reader); +} diff --git a/src/RESPite/Messages/RespAttributeReader.cs b/src/RESPite/Messages/RespAttributeReader.cs new file mode 100644 index 000000000..699d70a49 --- /dev/null +++ b/src/RESPite/Messages/RespAttributeReader.cs @@ -0,0 +1,68 @@ +namespace RESPite.Messages; + +/// +/// Allows attribute data to be parsed conveniently. +/// +/// The type of data represented by this reader. +public abstract class RespAttributeReader +{ + /// + /// Parse a group of attributes. + /// + public virtual void Read(ref RespReader reader, ref T value) + { + reader.Demand(RespPrefix.Attribute); + _ = ReadKeyValuePairs(ref reader, ref value); + } + + /// + /// Parse an aggregate as a set of key/value pairs. + /// + /// The number of pairs successfully processed. + protected virtual int ReadKeyValuePairs(ref RespReader reader, ref T value) + { + var iterator = reader.AggregateChildren(); + + byte[] pooledBuffer = []; + Span localBuffer = stackalloc byte[128]; + int count = 0; + while (iterator.MoveNext() && iterator.Value.TryReadNext()) + { + if (iterator.Value.IsScalar) + { + var key = iterator.Value.Buffer(ref pooledBuffer, localBuffer); + + if (iterator.MoveNext() && iterator.Value.TryReadNext()) + { + if (ReadKeyValuePair(key, ref iterator.Value, ref value)) + { + count++; + } + } + else + { + break; // no matching value for this key + } + } + else + { + if (iterator.MoveNext() && iterator.Value.TryReadNext()) + { + // we won't try to handle aggregate keys; skip the value + } + else + { + break; // no matching value for this key + } + } + } + iterator.MovePast(out reader); + return count; + } + + /// + /// Parse an individual key/value pair. + /// + /// True if the pair was successfully processed. + public virtual bool ReadKeyValuePair(scoped ReadOnlySpan key, ref RespReader reader, ref T value) => false; +} diff --git a/src/RESPite/Messages/RespFrameScanner.cs b/src/RESPite/Messages/RespFrameScanner.cs new file mode 100644 index 000000000..322bfa5e9 --- /dev/null +++ b/src/RESPite/Messages/RespFrameScanner.cs @@ -0,0 +1,193 @@ +using System.Buffers; +using RESPite.Messages; +using static RESPite.Internal.RespConstants; +namespace RESPite.Internal; + +/// +/// Scans RESP frames. +/// . +public sealed class RespFrameScanner // : IFrameSacanner, IFrameValidator +{ + /// + /// Gets a frame scanner for RESP2 request/response connections, or RESP3 connections. + /// + public static RespFrameScanner Default { get; } = new(false); + + /// + /// Gets a frame scanner that identifies RESP2 pub/sub messages. + /// + public static RespFrameScanner Subscription { get; } = new(true); + private RespFrameScanner(bool pubsub) => _pubsub = pubsub; + private readonly bool _pubsub; + + private static readonly uint FastNull = UnsafeCpuUInt32("_\r\n\0"u8), + SingleCharScalarMask = CpuUInt32(0xFF00FFFF), + SingleDigitInteger = UnsafeCpuUInt32(":\0\r\n"u8), + EitherBoolean = UnsafeCpuUInt32("#\0\r\n"u8), + FirstThree = CpuUInt32(0xFFFFFF00); + private static readonly ulong OK = UnsafeCpuUInt64("+OK\r\n\0\0\0"u8), + PONG = UnsafeCpuUInt64("+PONG\r\n\0"u8), + DoubleCharScalarMask = CpuUInt64(0xFF0000FFFF000000), + DoubleDigitInteger = UnsafeCpuUInt64(":\0\0\r\n"u8), + FirstFive = CpuUInt64(0xFFFFFFFFFF000000), + FirstSeven = CpuUInt64(0xFFFFFFFFFFFFFF00); + + private const OperationStatus UseReader = (OperationStatus)(-1); + private static OperationStatus TryFastRead(ReadOnlySpan data, ref RespScanState info) + { + // use silly math to detect the most common short patterns without needing + // to access a reader, or use indexof etc; handles: + // +OK\r\n + // +PONG\r\n + // :N\r\n for any single-digit N (integer) + // :NN\r\n for any double-digit N (integer) + // #N\r\n for any single-digit N (boolean) + // _\r\n (null) + uint hi, lo; + switch (data.Length) + { + case 0: + case 1: + case 2: + return OperationStatus.NeedMoreData; + case 3: + hi = (((uint)UnsafeCpuUInt16(data)) << 16) | (((uint)UnsafeCpuByte(data, 2)) << 8); + break; + default: + hi = UnsafeCpuUInt32(data); + break; + } + if ((hi & FirstThree) == FastNull) + { + info.SetComplete(3, RespPrefix.Null); + return OperationStatus.Done; + } + + var masked = hi & SingleCharScalarMask; + if (masked == SingleDigitInteger) + { + info.SetComplete(4, RespPrefix.Integer); + return OperationStatus.Done; + } + else if (masked == EitherBoolean) + { + info.SetComplete(4, RespPrefix.Boolean); + return OperationStatus.Done; + } + + switch (data.Length) + { + case 3: + return OperationStatus.NeedMoreData; + case 4: + return UseReader; + case 5: + lo = ((uint)data[4]) << 24; + break; + case 6: + lo = ((uint)UnsafeCpuUInt16(data, 4)) << 16; + break; + case 7: + lo = ((uint)UnsafeCpuUInt16(data, 4)) << 16 | ((uint)UnsafeCpuByte(data, 6)) << 8; + break; + default: + lo = UnsafeCpuUInt32(data, 4); + break; + } + var u64 = BitConverter.IsLittleEndian ? ((((ulong)lo) << 32) | hi) : ((((ulong)hi) << 32) | lo); + if (((u64 & FirstFive) == OK) | ((u64 & DoubleCharScalarMask) == DoubleDigitInteger)) + { + info.SetComplete(5, RespPrefix.SimpleString); + return OperationStatus.Done; + } + if ((u64 & FirstSeven) == PONG) + { + info.SetComplete(7, RespPrefix.SimpleString); + return OperationStatus.Done; + } + return UseReader; + } + + /// + /// Attempt to read more data as part of the current frame. + /// + public OperationStatus TryRead(ref RespScanState state, in ReadOnlySequence data) + { + if (!_pubsub & state.TotalBytes == 0 & data.IsSingleSegment) + { +#if NETCOREAPP3_1_OR_GREATER + var status = TryFastRead(data.FirstSpan, ref state); +#else + var status = TryFastRead(data.First.Span, ref state); +#endif + if (status != UseReader) return status; + } + + return TryReadViaReader(ref state, in data); + + static OperationStatus TryReadViaReader(ref RespScanState state, in ReadOnlySequence data) + { + var reader = new RespReader(in data); + var complete = state.TryRead(ref reader, out var consumed); + if (complete) + { + return OperationStatus.Done; + } + return OperationStatus.NeedMoreData; + } + } + + /// + /// Attempt to read more data as part of the current frame. + /// + public OperationStatus TryRead(ref RespScanState state, ReadOnlySpan data) + { + if (!_pubsub & state.TotalBytes == 0) + { +#if NETCOREAPP3_1_OR_GREATER + var status = TryFastRead(data, ref state); +#else + var status = TryFastRead(data, ref state); +#endif + if (status != UseReader) return status; + } + + return TryReadViaReader(ref state, data); + + static OperationStatus TryReadViaReader(ref RespScanState state, ReadOnlySpan data) + { + var reader = new RespReader(data); + var complete = state.TryRead(ref reader, out var consumed); + if (complete) + { + return OperationStatus.Done; + } + return OperationStatus.NeedMoreData; + } + } + + /// + /// Validate that the supplied message is a valid RESP request, specifically: that it contains a single + /// top-level array payload with bulk-string elements, the first of which is non-empty (the command). + /// + public void ValidateRequest(in ReadOnlySequence message) + { + if (message.IsEmpty) Throw("Empty RESP frame"); + RespReader reader = new(in message); + reader.MoveNext(RespPrefix.Array); + reader.DemandNotNull(); + if (reader.IsStreaming) Throw("Streaming is not supported in this context"); + var count = reader.AggregateLength(); + for (int i = 0; i < count; i++) + { + reader.MoveNext(RespPrefix.BulkString); + reader.DemandNotNull(); + if (reader.IsStreaming) Throw("Streaming is not supported in this context"); + + if (i == 0 && reader.ScalarIsEmpty()) Throw("command must be non-empty"); + } + reader.DemandEnd(); + + static void Throw(string message) => throw new InvalidOperationException(message); + } +} diff --git a/src/RESPite/Messages/RespPrefix.cs b/src/RESPite/Messages/RespPrefix.cs new file mode 100644 index 000000000..09fa5e5d8 --- /dev/null +++ b/src/RESPite/Messages/RespPrefix.cs @@ -0,0 +1,97 @@ +namespace RESPite.Messages; + +/// +/// RESP protocol prefix. +/// +public enum RespPrefix : byte +{ + /// + /// Invalid. + /// + None = 0, + + /// + /// Simple strings: +OK\r\n. + /// + SimpleString = (byte)'+', + + /// + /// Simple errors: -ERR message\r\n. + /// + SimpleError = (byte)'-', + + /// + /// Integers: :123\r\n. + /// + Integer = (byte)':', + + /// + /// String with support for binary data: $7\r\nmessage\r\n. + /// + BulkString = (byte)'$', + + /// + /// Multiple inner messages: *1\r\n+message\r\n. + /// + Array = (byte)'*', + + /// + /// Null strings/arrays: _\r\n. + /// + Null = (byte)'_', + + /// + /// Boolean values: #T\r\n. + /// + Boolean = (byte)'#', + + /// + /// Floating-point number: ,123.45\r\n. + /// + Double = (byte)',', + + /// + /// Large integer number: (12...89\r\n. + /// + BigInteger = (byte)'(', + + /// + /// Error with support for binary data: !7\r\nmessage\r\n. + /// + BulkError = (byte)'!', + + /// + /// String that should be interpreted verbatim: =11\r\ntxt:message\r\n. + /// + VerbatimString = (byte)'=', + + /// + /// Multiple sub-items that represent a map. + /// + Map = (byte)'%', + + /// + /// Multiple sub-items that represent a set. + /// + Set = (byte)'~', + + /// + /// Out-of band messages. + /// + Push = (byte)'>', + + /// + /// Continuation of streaming scalar values. + /// + StreamContinuation = (byte)';', + + /// + /// End sentinel for streaming aggregate values. + /// + StreamTerminator = (byte)'.', + + /// + /// Metadata about the next element. + /// + Attribute = (byte)'|', +} diff --git a/src/RESPite/Messages/RespReader.AggregateEnumerator.cs b/src/RESPite/Messages/RespReader.AggregateEnumerator.cs new file mode 100644 index 000000000..99e3aa2ca --- /dev/null +++ b/src/RESPite/Messages/RespReader.AggregateEnumerator.cs @@ -0,0 +1,193 @@ +using System.Collections; +using System.ComponentModel; + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace RESPite.Messages; + +public ref partial struct RespReader +{ + /// + /// Reads the sub-elements associated with an aggregate value. + /// + public readonly AggregateEnumerator AggregateChildren() => new(in this); + + /// + /// Reads the sub-elements associated with an aggregate value. + /// + public ref struct AggregateEnumerator + { + // Note that _reader is the overall reader that can see outside this aggregate, as opposed + // to Current which is the sub-tree of the current element *only* + private RespReader _reader; + private int _remaining; + + /// + /// Create a new enumerator for the specified . + /// + /// The reader containing the data for this operation. + public AggregateEnumerator(scoped in RespReader reader) + { + reader.DemandAggregate(); + _remaining = reader.IsStreaming ? -1 : reader._length; + _reader = reader; + Value = default; + } + + /// + public readonly AggregateEnumerator GetEnumerator() => this; + + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public RespReader Current => Value; + + /// + /// Gets the current element associated with this reader. + /// + public RespReader Value; // intentionally a field, because of ref-semantics + + /// + /// Move to the next child if possible, and move the child element into the next node. + /// + public bool MoveNext(RespPrefix prefix) + { + bool result = MoveNext(); + if (result) + { + Value.MoveNext(prefix); + } + return result; + } + + /// + /// Move to the next child if possible, and move the child element into the next node. + /// + /// The type of data represented by this reader. + public bool MoveNext(RespPrefix prefix, RespAttributeReader respAttributeReader, ref T attributes) + { + bool result = MoveNext(respAttributeReader, ref attributes); + if (result) + { + Value.MoveNext(prefix); + } + return result; + } + + /// > + public bool MoveNext() + { + object? attributes = null; + return MoveNextCore(null, ref attributes); + } + + /// > + /// The type of data represented by this reader. + public bool MoveNext(RespAttributeReader respAttributeReader, ref T attributes) + => MoveNextCore(respAttributeReader, ref attributes); + + /// > + private bool MoveNextCore(RespAttributeReader? attributeReader, ref T attributes) + { + if (_remaining == 0) + { + Value = default; + return false; + } + + // in order to provide access to attributes etc, we want Current to be positioned + // *before* the next element; for that, we'll take a snapshot before we read + _reader.MovePastCurrent(); + var snapshot = _reader.Clone(); + + if (attributeReader is null) + { + _reader.MoveNext(); + } + else + { + _reader.MoveNext(attributeReader, ref attributes); + } + if (_remaining > 0) + { + // non-streaming, decrement + _remaining--; + } + else if (_reader.Prefix == RespPrefix.StreamTerminator) + { + // end of streaming aggregate + _remaining = 0; + Value = default; + return false; + } + + // move past that sub-tree and trim the "snapshot" state, giving + // us a scoped reader that is *just* that sub-tree + _reader.SkipChildren(); + snapshot.TrimToTotal(_reader.BytesConsumed); + + Value = snapshot; + return true; + } + + /// + /// Move to the end of this aggregate and export the state of the . + /// + /// The reader positioned at the end of the data; this is commonly + /// used to update a tree reader, to get to the next data after the aggregate. + public void MovePast(out RespReader reader) + { + while (MoveNext()) { } + reader = _reader; + } + + public void DemandNext() + { + if (!MoveNext()) ThrowEOF(); + Value.MoveNext(); // skip any attributes etc + } + + public T ReadOne(Projection projection) + { + DemandNext(); + return projection(ref Value); + } + + public void FillAll(scoped Span target, Projection projection) + { + for (int i = 0; i < target.Length; i++) + { + if (!MoveNext()) ThrowEOF(); + + Value.MoveNext(); // skip any attributes etc + target[i] = projection(ref Value); + } + } + } + + internal void TrimToTotal(long length) => TrimToRemaining(length - BytesConsumed); + + internal void TrimToRemaining(long bytes) + { + if (_prefix != RespPrefix.None || bytes < 0) Throw(); + + var current = CurrentAvailable; + if (bytes <= current) + { + UnsafeTrimCurrentBy(current - (int)bytes); + _remainingTailLength = 0; + return; + } + + bytes -= current; + if (bytes <= _remainingTailLength) + { + _remainingTailLength = bytes; + return; + } + + Throw(); + static void Throw() => throw new ArgumentOutOfRangeException(nameof(bytes)); + } +} diff --git a/src/RESPite/Messages/RespReader.Debug.cs b/src/RESPite/Messages/RespReader.Debug.cs new file mode 100644 index 000000000..3f471bbd1 --- /dev/null +++ b/src/RESPite/Messages/RespReader.Debug.cs @@ -0,0 +1,33 @@ +using System.Diagnostics; + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace RESPite.Messages; + +[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] +public ref partial struct RespReader +{ + internal bool DebugEquals(in RespReader other) + => _prefix == other._prefix + && _length == other._length + && _flags == other._flags + && _bufferIndex == other._bufferIndex + && _positionBase == other._positionBase + && _remainingTailLength == other._remainingTailLength; + + internal new string ToString() => $"{Prefix} ({_flags}); length {_length}, {TotalAvailable} remaining"; + + internal void DebugReset() + { + _bufferIndex = 0; + _length = 0; + _flags = 0; + _prefix = RespPrefix.None; + } + +#if DEBUG + internal bool VectorizeDisabled { get; set; } +#endif +} diff --git a/src/RESPite/Messages/RespReader.ScalarEnumerator.cs b/src/RESPite/Messages/RespReader.ScalarEnumerator.cs new file mode 100644 index 000000000..a2894393d --- /dev/null +++ b/src/RESPite/Messages/RespReader.ScalarEnumerator.cs @@ -0,0 +1,105 @@ +using System.Buffers; +using System.Collections; + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace RESPite.Messages; + +public ref partial struct RespReader +{ + /// + /// Gets the chunks associated with a scalar value. + /// + public readonly ScalarEnumerator ScalarChunks() => new(in this); + + /// + /// Allows enumeration of chunks in a scalar value; this includes simple values + /// that span multiple segments, and streaming + /// scalar RESP values. + /// + public ref struct ScalarEnumerator + { + /// + public readonly ScalarEnumerator GetEnumerator() => this; + + private RespReader _reader; + + private ReadOnlySpan _current; + private ReadOnlySequenceSegment? _tail; + private int _offset, _remaining; + + /// + /// Create a new enumerator for the specified . + /// + /// The reader containing the data for this operation. + public ScalarEnumerator(scoped in RespReader reader) + { + reader.DemandScalar(); + _reader = reader; + InitSegment(); + } + + private void InitSegment() + { + _current = _reader.CurrentSpan(); + _tail = _reader._tail; + _offset = CurrentLength = 0; + _remaining = _reader._length; + if (_reader.TotalAvailable < _remaining) ThrowEOF(); + } + + /// + public bool MoveNext() + { + while (true) // for each streaming element + { + _offset += CurrentLength; + while (_remaining > 0) // for each span in the current element + { + // look in the active span + var take = Math.Min(_remaining, _current.Length - _offset); + if (take > 0) // more in the current chunk + { + _remaining -= take; + CurrentLength = take; + return true; + } + + // otherwise, we expect more tail data + if (_tail is null) ThrowEOF(); + + _current = _tail.Memory.Span; + _offset = 0; + _tail = _tail.Next; + } + + if (!_reader.MoveNextStreamingScalar()) break; + InitSegment(); + } + + CurrentLength = 0; + return false; + } + + /// + public readonly ReadOnlySpan Current => _current.Slice(_offset, CurrentLength); + + /// + /// Gets the or . + /// + public int CurrentLength { readonly get; private set; } + + /// + /// Move to the end of this aggregate and export the state of the . + /// + /// The reader positioned at the end of the data; this is commonly + /// used to update a tree reader, to get to the next data after the aggregate. + public void MovePast(out RespReader reader) + { + while (MoveNext()) { } + reader = _reader; + } + } +} diff --git a/src/RESPite/Messages/RespReader.Span.cs b/src/RESPite/Messages/RespReader.Span.cs new file mode 100644 index 000000000..fd3870ef3 --- /dev/null +++ b/src/RESPite/Messages/RespReader.Span.cs @@ -0,0 +1,84 @@ +#define USE_UNSAFE_SPAN + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace RESPite.Messages; + +/* + How we actually implement the underlying buffer depends on the capabilities of the runtime. + */ + +#if NET7_0_OR_GREATER && USE_UNSAFE_SPAN + +public ref partial struct RespReader +{ + // intent: avoid lots of slicing by dealing with everything manually, and accepting the "don't get it wrong" rule + private ref byte _bufferRoot; + private int _bufferLength; + + private partial void UnsafeTrimCurrentBy(int count) + { + Debug.Assert(count >= 0 && count <= _bufferLength, "Unsafe trim length"); + _bufferLength -= count; + } + + private readonly partial ref byte UnsafeCurrent + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.Add(ref _bufferRoot, _bufferIndex); + } + + private readonly partial int CurrentLength + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _bufferLength; + } + + private readonly partial ReadOnlySpan CurrentSpan() => MemoryMarshal.CreateReadOnlySpan( + ref UnsafeCurrent, CurrentAvailable); + + private readonly partial ReadOnlySpan UnsafePastPrefix() => MemoryMarshal.CreateReadOnlySpan( + ref Unsafe.Add(ref _bufferRoot, _bufferIndex + 1), + _bufferLength - (_bufferIndex + 1)); + + private partial void SetCurrent(ReadOnlySpan value) + { + _bufferRoot = ref MemoryMarshal.GetReference(value); + _bufferLength = value.Length; + } +} +#else +public ref partial struct RespReader // much more conservative - uses slices etc +{ + private ReadOnlySpan _buffer; + + private partial void UnsafeTrimCurrentBy(int count) + { + _buffer = _buffer.Slice(0, _buffer.Length - count); + } + + private readonly partial ref byte UnsafeCurrent + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in _buffer[_bufferIndex]); // hack around CS8333 + } + + private readonly partial int CurrentLength + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _buffer.Length; + } + + private readonly partial ReadOnlySpan UnsafePastPrefix() => _buffer.Slice(_bufferIndex + 1); + + private readonly partial ReadOnlySpan CurrentSpan() => _buffer.Slice(_bufferIndex); + + private partial void SetCurrent(ReadOnlySpan value) => _buffer = value; +} +#endif diff --git a/src/RESPite/Messages/RespReader.Utils.cs b/src/RESPite/Messages/RespReader.Utils.cs new file mode 100644 index 000000000..01caecd6a --- /dev/null +++ b/src/RESPite/Messages/RespReader.Utils.cs @@ -0,0 +1,317 @@ +using System.Buffers.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using RESPite.Internal; + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace RESPite.Messages; + +public ref partial struct RespReader +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UnsafeAssertClLf(int offset) => UnsafeAssertClLf(ref UnsafeCurrent, offset); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void UnsafeAssertClLf(scoped ref byte source, int offset) + { + if (Unsafe.ReadUnaligned(ref Unsafe.Add(ref source, offset)) != RespConstants.CrLfUInt16) + { + ThrowProtocolFailure("Expected CR/LF"); + } + } + + private enum LengthPrefixResult + { + NeedMoreData, + Length, + Null, + Streaming, + } + + /// + /// Asserts that the current element is a scalar type. + /// + public readonly void DemandScalar() + { + if (!IsScalar) Throw(Prefix); + static void Throw(RespPrefix prefix) => throw new InvalidOperationException($"This operation requires a scalar element, got {prefix}"); + } + + /// + /// Asserts that the current element is a scalar type. + /// + public readonly void DemandAggregate() + { + if (!IsAggregate) Throw(Prefix); + static void Throw(RespPrefix prefix) => throw new InvalidOperationException($"This operation requires an aggregate element, got {prefix}"); + } + + private static LengthPrefixResult TryReadLengthPrefix(ReadOnlySpan bytes, out int value, out int byteCount) + { + var end = bytes.IndexOf(RespConstants.CrlfBytes); + if (end < 0) + { + byteCount = value = 0; + if (bytes.Length >= RespConstants.MaxRawBytesInt32 + 2) + { + ThrowProtocolFailure("Unterminated or over-length integer"); // should have failed; report failure to prevent infinite loop + } + return LengthPrefixResult.NeedMoreData; + } + byteCount = end + 2; + switch (end) + { + case 0: + ThrowProtocolFailure("Length prefix expected"); + goto case default; // not reached, just satisfying definite assignment + case 1 when bytes[0] == (byte)'?': + value = 0; + return LengthPrefixResult.Streaming; + default: + if (end > RespConstants.MaxRawBytesInt32 || !(Utf8Parser.TryParse(bytes, out value, out var consumed) && consumed == end)) + { + ThrowProtocolFailure("Unable to parse integer"); + value = 0; + } + if (value < 0) + { + if (value == -1) + { + value = 0; + return LengthPrefixResult.Null; + } + ThrowProtocolFailure("Invalid negative length prefix"); + } + return LengthPrefixResult.Length; + } + } + + private readonly RespReader Clone() => this; // useful for performing streaming operations without moving the primary + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + private static void ThrowProtocolFailure(string message) + => throw new InvalidOperationException("RESP protocol failure: " + message); // protocol exception? + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + internal static void ThrowEOF() => throw new EndOfStreamException(); + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + private static void ThrowFormatException() => throw new FormatException(); + + private int RawTryReadByte() + { + if (_bufferIndex < CurrentLength || TryMoveToNextSegment()) + { + var result = UnsafeCurrent; + _bufferIndex++; + return result; + } + return -1; + } + + private int RawPeekByte() + { + return (CurrentLength < _bufferIndex || TryMoveToNextSegment()) ? UnsafeCurrent : -1; + } + + private bool RawAssertCrLf() + { + if (CurrentAvailable >= 2) + { + UnsafeAssertClLf(0); + _bufferIndex += 2; + return true; + } + else + { + int next = RawTryReadByte(); + if (next < 0) return false; + if (next == '\r') + { + next = RawTryReadByte(); + if (next < 0) return false; + if (next == '\n') return true; + } + ThrowProtocolFailure("Expected CR/LF"); + return false; + } + } + + private LengthPrefixResult RawTryReadLengthPrefix() + { + _length = 0; + if (!RawTryFindCrLf(out int end)) + { + if (TotalAvailable >= RespConstants.MaxRawBytesInt32 + 2) + { + ThrowProtocolFailure("Unterminated or over-length integer"); // should have failed; report failure to prevent infinite loop + } + return LengthPrefixResult.NeedMoreData; + } + + switch (end) + { + case 0: + ThrowProtocolFailure("Length prefix expected"); + goto case default; // not reached, just satisfying definite assignment + case 1: + var b = (byte)RawTryReadByte(); + RawAssertCrLf(); + if (b == '?') + { + return LengthPrefixResult.Streaming; + } + else + { + _length = ParseSingleDigit(b); + return LengthPrefixResult.Length; + } + default: + if (end > RespConstants.MaxRawBytesInt32) + { + ThrowProtocolFailure("Unable to parse integer"); + } + Span bytes = stackalloc byte[end]; + RawFillBytes(bytes); + RawAssertCrLf(); + if (!(Utf8Parser.TryParse(bytes, out _length, out var consumed) && consumed == end)) + { + ThrowProtocolFailure("Unable to parse integer"); + } + + if (_length < 0) + { + if (_length == -1) + { + _length = 0; + return LengthPrefixResult.Null; + } + ThrowProtocolFailure("Invalid negative length prefix"); + } + + return LengthPrefixResult.Length; + } + } + + private void RawFillBytes(scoped Span target) + { + do + { + var current = CurrentSpan(); + if (current.Length >= target.Length) + { + // more than enough, need to trim + current.Slice(0, target.Length).CopyTo(target); + _bufferIndex += target.Length; + return; // we're done + } + else + { + // take what we can + current.CopyTo(target); + target = target.Slice(current.Length); + // we could move _bufferIndex here, but we're about to trash that in TryMoveToNextSegment + } + } + while (TryMoveToNextSegment()); + ThrowEOF(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ParseSingleDigit(byte value) + { + return value switch + { + (byte)'0' or (byte)'1' or (byte)'2' or (byte)'3' or (byte)'4' or (byte)'5' or (byte)'6' or (byte)'7' or (byte)'8' or (byte)'9' => value - (byte)'0', + _ => Invalid(value), + }; + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + static int Invalid(byte value) => throw new FormatException($"Unable to parse integer: '{(char)value}'"); + } + + private readonly bool RawTryAssertInlineScalarPayloadCrLf() + { + Debug.Assert(IsInlineScalar, "should be inline scalar"); + + var reader = Clone(); + var len = reader._length; + if (len == 0) return reader.RawAssertCrLf(); + + do + { + var current = reader.CurrentSpan(); + if (current.Length >= len) + { + reader._bufferIndex += len; + return reader.RawAssertCrLf(); // we're done + } + else + { + // take what we can + len -= current.Length; + // we could move _bufferIndex here, but we're about to trash that in TryMoveToNextSegment + } + } + while (reader.TryMoveToNextSegment()); + return false; // EOF + } + + private readonly bool RawTryFindCrLf(out int length) + { + length = 0; + RespReader reader = Clone(); + do + { + var span = reader.CurrentSpan(); + var index = span.IndexOf((byte)'\r'); + if (index >= 0) + { + checked + { + length += index; + } + // move past the CR and assert the LF + reader._bufferIndex += index + 1; + var next = reader.RawTryReadByte(); + if (next < 0) break; // we don't know + if (next != '\n') ThrowProtocolFailure("CR/LF expected"); + + return true; + } + checked + { + length += span.Length; + } + } + while (reader.TryMoveToNextSegment()); + length = 0; + return false; + } + + private string GetDebuggerDisplay() + { + return ToString(); + } + + internal int GetInitialScanCount(out ushort streamingAggregateDepth) + { + // this is *similar* to GetDelta, but: without any discount for attributes + switch (_flags & (RespFlags.IsAggregate | RespFlags.IsStreaming)) + { + case RespFlags.IsAggregate: + streamingAggregateDepth = 0; + return _length - 1; + case RespFlags.IsAggregate | RespFlags.IsStreaming: + streamingAggregateDepth = 1; + return 0; + default: + streamingAggregateDepth = 0; + return -1; + } + } +} diff --git a/src/RESPite/Messages/RespReader.cs b/src/RESPite/Messages/RespReader.cs new file mode 100644 index 000000000..c96ad43ce --- /dev/null +++ b/src/RESPite/Messages/RespReader.cs @@ -0,0 +1,1598 @@ +using System.Buffers; +using System.Buffers.Text; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; +using RESPite.Internal; + +#if NETCOREAPP3_0_OR_GREATER +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace RESPite.Messages; + +/// +/// Provides low level RESP parsing functionality. +/// +public ref partial struct RespReader +{ + [Flags] + private enum RespFlags : byte + { + None = 0, + IsScalar = 1 << 0, // simple strings, bulk strings, etc + IsAggregate = 1 << 1, // arrays, maps, sets, etc + IsNull = 1 << 2, // explicit null RESP types, or bulk-strings/aggregates with length -1 + IsInlineScalar = 1 << 3, // a non-null scalar, i.e. with payload+CrLf + IsAttribute = 1 << 4, // is metadata for following elements + IsStreaming = 1 << 5, // unknown length + IsError = 1 << 6, // an explicit error reported inside the protocol + } + + // relates to the element we're currently reading + private RespFlags _flags; + private RespPrefix _prefix; + + private int _length; // for null: 0; for scalars: the length of the payload; for aggregates: the child count + + // the current buffer that we're observing + private int _bufferIndex; // after TryRead, this should be positioned immediately before the actual data + + // the position in a multi-segment payload + private long _positionBase; // total data we've already moved past in *previous* buffers + private ReadOnlySequenceSegment? _tail; // the next tail node + private long _remainingTailLength; // how much more can we consume from the tail? + + public long ProtocolBytesRemaining => TotalAvailable; + + private readonly int CurrentAvailable => CurrentLength - _bufferIndex; + + private readonly long TotalAvailable => CurrentAvailable + _remainingTailLength; + private partial void UnsafeTrimCurrentBy(int count); + private readonly partial ref byte UnsafeCurrent { get; } + private readonly partial int CurrentLength { get; } + private partial void SetCurrent(ReadOnlySpan value); + private RespPrefix UnsafePeekPrefix() => (RespPrefix)UnsafeCurrent; + private readonly partial ReadOnlySpan UnsafePastPrefix(); + private readonly partial ReadOnlySpan CurrentSpan(); + + /// + /// Get the scalar value as a single-segment span. + /// + /// True if this is a non-streaming scalar element that covers a single span only, otherwise False. + /// If a scalar reports False, can be used to iterate the entire payload. + /// When True, the contents of the scalar value. + public readonly bool TryGetSpan(out ReadOnlySpan value) + { + if (IsInlineScalar && CurrentAvailable >= _length) + { + value = CurrentSpan().Slice(0, _length); + return true; + } + + value = default; + return IsNullScalar; + } + + /// + /// Returns the position after the end of the current element. + /// + public readonly long BytesConsumed => _positionBase + _bufferIndex + TrailingLength; + + /// + /// Body length of scalar values, plus any terminating sentinels. + /// + private readonly int TrailingLength => (_flags & RespFlags.IsInlineScalar) == 0 ? 0 : (_length + 2); + + /// + /// Gets the RESP kind of the current element. + /// + public readonly RespPrefix Prefix => _prefix; + + /// + /// The payload length of this scalar element (includes combined length for streaming scalars). + /// + public readonly int ScalarLength() => IsInlineScalar ? _length : IsNullScalar ? 0 : checked((int)ScalarLengthSlow()); + + /// + /// Indicates whether this scalar value is zero-length. + /// + public readonly bool ScalarIsEmpty() => IsInlineScalar ? _length == 0 : (IsNullScalar || !ScalarChunks().MoveNext()); + + /// + /// The payload length of this scalar element (includes combined length for streaming scalars). + /// + public readonly long ScalarLongLength() => IsInlineScalar ? _length : IsNullScalar ? 0 : ScalarLengthSlow(); + + private readonly long ScalarLengthSlow() + { + DemandScalar(); + long length = 0; + var iterator = ScalarChunks(); + while (iterator.MoveNext()) + { + length += iterator.CurrentLength; + } + return length; + } + + /// + /// The number of child elements associated with an aggregate. + /// + /// For + /// and aggregates, this is twice the value reported in the RESP protocol, + /// i.e. a map of the form %2\r\n... will report 4 as the length. + /// Note that if the data could be streaming (), it may be preferable to use + /// the API, using the API to update the outer reader. + public readonly int AggregateLength() => (_flags & (RespFlags.IsAggregate | RespFlags.IsStreaming)) == RespFlags.IsAggregate + ? _length : AggregateLengthSlow(); + + public delegate T Projection(ref RespReader value); + + public void FillAll(scoped Span target, Projection projection) + { + DemandNotNull(); + AggregateChildren().FillAll(target, projection); + } + + private readonly int AggregateLengthSlow() + { + switch (_flags & (RespFlags.IsAggregate | RespFlags.IsStreaming)) + { + case RespFlags.IsAggregate: + return _length; + case RespFlags.IsAggregate | RespFlags.IsStreaming: + break; + default: + DemandAggregate(); // we expect this to throw + break; + } + + int count = 0; + var reader = Clone(); + while (true) + { + if (!reader.TryMoveNext()) ThrowEOF(); + if (reader.Prefix == RespPrefix.StreamTerminator) + { + return count; + } + reader.SkipChildren(); + count++; + } + } + + /// + /// Indicates whether this is a scalar value, i.e. with a potential payload body. + /// + public readonly bool IsScalar => (_flags & RespFlags.IsScalar) != 0; + + internal readonly bool IsInlineScalar => (_flags & RespFlags.IsInlineScalar) != 0; + + internal readonly bool IsNullScalar => (_flags & (RespFlags.IsScalar | RespFlags.IsNull)) == (RespFlags.IsScalar | RespFlags.IsNull); + + /// + /// Indicates whether this is an aggregate value, i.e. represents a collection of sub-values. + /// + public readonly bool IsAggregate => (_flags & RespFlags.IsAggregate) != 0; + + /// + /// Indicates whether this is a null value; this could be an explicit , + /// or a scalar or aggregate a negative reported length. + /// + public readonly bool IsNull => (_flags & RespFlags.IsNull) != 0; + + /// + /// Indicates whether this is an attribute value, i.e. metadata relating to later element data. + /// + public readonly bool IsAttribute => (_flags & RespFlags.IsAttribute) != 0; + + /// + /// Indicates whether this represents streaming content, where the or is not known in advance. + /// + public readonly bool IsStreaming => (_flags & RespFlags.IsStreaming) != 0; + + /// + /// Equivalent to both and . + /// + internal readonly bool IsStreamingScalar => (_flags & (RespFlags.IsScalar | RespFlags.IsStreaming)) == (RespFlags.IsScalar | RespFlags.IsStreaming); + + /// + /// Indicates errors reported inside the protocol. + /// + public readonly bool IsError => (_flags & RespFlags.IsError) != 0; + + /// + /// Gets the effective change (in terms of how many RESP nodes we expect to see) from consuming this element. + /// For simple scalars, this is -1 because we have one less node to read; for simple aggregates, this is + /// AggregateLength-1 because we will have consumed one element, but now need to read the additional + /// child elements. Attributes report 0, since they supplement data + /// we still need to consume. The final terminator for streaming data reports a delta of -1, otherwise: 0. + /// + /// This does not account for being nested inside a streaming aggregate; the caller must deal with that manually. + internal int Delta() => (_flags & (RespFlags.IsScalar | RespFlags.IsAggregate | RespFlags.IsStreaming | RespFlags.IsAttribute)) switch + { + RespFlags.IsScalar => -1, + RespFlags.IsAggregate => _length - 1, + RespFlags.IsAggregate | RespFlags.IsAttribute => _length, + _ => 0, + }; + + /// + /// Assert that this is the final element in the current payload. + /// + /// If additional elements are available. + public void DemandEnd() + { + while (IsStreamingScalar) + { + if (!TryReadNext()) ThrowEOF(); + } + if (TryReadNext()) + { + Throw(Prefix); + } + static void Throw(RespPrefix prefix) => throw new InvalidOperationException($"Expected end of payload, but found {prefix}"); + } + + private bool TryReadNextSkipAttributes() + { + while (TryReadNext()) + { + if (IsAttribute) + { + SkipChildren(); + } + else + { + return true; + } + } + return false; + } + + private bool TryReadNextProcessAttributes(RespAttributeReader respAttributeReader, ref T attributes) + { + while (TryReadNext()) + { + if (IsAttribute) + { + respAttributeReader.Read(ref this, ref attributes); + } + else + { + return true; + } + } + return false; + } + + /// + /// Move to the next content element; this skips attribute metadata, checking for RESP error messages by default. + /// + /// If the data is exhausted before a streaming scalar is exhausted. + /// If the data contains an explicit error element. + public bool TryMoveNext() + { + while (IsStreamingScalar) // close out the current streaming scalar + { + if (!TryReadNextSkipAttributes()) ThrowEOF(); + } + + if (TryReadNextSkipAttributes()) + { + if (IsError) ThrowError(); + return true; + } + return false; + } + + /// + /// Move to the next content element; this skips attribute metadata, checking for RESP error messages by default. + /// + /// Whether to check and throw for error messages. + /// If the data is exhausted before a streaming scalar is exhausted. + /// If the data contains an explicit error element. + public bool TryMoveNext(bool checkError) + { + while (IsStreamingScalar) // close out the current streaming scalar + { + if (!TryReadNextSkipAttributes()) ThrowEOF(); + } + + if (TryReadNextSkipAttributes()) + { + if (checkError && IsError) ThrowError(); + return true; + } + return false; + } + + /// + /// Move to the next content element; this skips attribute metadata, checking for RESP error messages by default. + /// + /// Parser for attribute data preceding the data. + /// The state for attributes encountered. + /// If the data is exhausted before a streaming scalar is exhausted. + /// If the data contains an explicit error element. + /// The type of data represented by this reader. + public bool TryMoveNext(RespAttributeReader respAttributeReader, ref T attributes) + { + while (IsStreamingScalar) // close out the current streaming scalar + { + if (!TryReadNextSkipAttributes()) ThrowEOF(); + } + + if (TryReadNextProcessAttributes(respAttributeReader, ref attributes)) + { + if (IsError) ThrowError(); + return true; + } + return false; + } + + /// + /// Move to the next content element, asserting that it is of the expected type; this skips attribute metadata, checking for RESP error messages by default. + /// + /// The expected data type. + /// If the data is exhausted before a streaming scalar is exhausted. + /// If the data contains an explicit error element. + /// If the data is not of the expected type. + public bool TryMoveNext(RespPrefix prefix) + { + bool result = TryMoveNext(); + if (result) Demand(prefix); + return result; + } + + /// + /// Move to the next content element; this skips attribute metadata, checking for RESP error messages by default. + /// + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + public void MoveNext() + { + if (!TryMoveNext()) ThrowEOF(); + } + + /// + /// Move to the next content element; this skips attribute metadata, checking for RESP error messages by default. + /// + /// Parser for attribute data preceding the data. + /// The state for attributes encountered. + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + /// The type of data represented by this reader. + public void MoveNext(RespAttributeReader respAttributeReader, ref T attributes) + { + if (!TryMoveNext(respAttributeReader, ref attributes)) ThrowEOF(); + } + + private bool MoveNextStreamingScalar() + { + if (IsStreamingScalar) + { + while (TryReadNext()) + { + if (IsAttribute) + { + SkipChildren(); + } + else + { + if (Prefix != RespPrefix.StreamContinuation) ThrowProtocolFailure("Streaming continuation expected"); + return _length > 0; + } + } + ThrowEOF(); // we should have found something! + } + return false; + } + + /// + /// Move to the next content element () and assert that it is a scalar (). + /// + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + /// If the data is not a scalar type. + public void MoveNextScalar() + { + MoveNext(); + DemandScalar(); + } + + /// + /// Move to the next content element () and assert that it is an aggregate (). + /// + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + /// If the data is not an aggregate type. + public void MoveNextAggregate() + { + MoveNext(); + DemandAggregate(); + } + + /// + /// Move to the next content element () and assert that it of type specified + /// in . + /// + /// The expected data type. + /// Parser for attribute data preceding the data. + /// The state for attributes encountered. + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + /// If the data is not of the expected type. + /// The type of data represented by this reader. + public void MoveNext(RespPrefix prefix, RespAttributeReader respAttributeReader, ref T attributes) + { + MoveNext(respAttributeReader, ref attributes); + Demand(prefix); + } + + /// + /// Move to the next content element () and assert that it of type specified + /// in . + /// + /// The expected data type. + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + /// If the data is not of the expected type. + public void MoveNext(RespPrefix prefix) + { + MoveNext(); + Demand(prefix); + } + + internal void Demand(RespPrefix prefix) + { + if (Prefix != prefix) Throw(prefix, Prefix); + static void Throw(RespPrefix expected, RespPrefix actual) => throw new InvalidOperationException($"Expected {expected} element, but found {actual}."); + } + + private readonly void ThrowError() => throw new RespException(ReadString()!); + + /// + /// Skip all sub elements of the current node; this includes both aggregate children and scalar streaming elements. + /// + public void SkipChildren() + { + // if this is a simple non-streaming scalar, then: there's nothing complex to do; otherwise, re-use the + // frame scanner logic to seek past the noise (this way, we avoid recursion etc) + switch (_flags & (RespFlags.IsScalar | RespFlags.IsAggregate | RespFlags.IsStreaming)) + { + case RespFlags.None: + // no current element + break; + case RespFlags.IsScalar: + // simple scalar + MovePastCurrent(); + break; + default: + // something more complex + RespScanState state = new(in this); + if (!state.TryRead(ref this, out _)) ThrowEOF(); + break; + } + } + + /// + /// Reads the current element as a string value. + /// + public readonly string? ReadString() => ReadString(out _); + + /// + /// Reads the current element as a string value. + /// + public readonly string? ReadString(out string prefix) + { + byte[] pooled = []; + try + { + var span = Buffer(ref pooled, stackalloc byte[256]); + prefix = ""; + if (span.IsEmpty) + { + return IsNull ? null : ""; + } + if (Prefix == RespPrefix.VerbatimString + && span.Length >= 4 && span[3] == ':') + { + // "the first three bytes provide information about the format of the following string, + // which can be txt for plain text, or mkd for markdown. The fourth byte is always :. + // Then the real string follows." + var prefixValue = RespConstants.UnsafeCpuUInt32(span); + if (prefixValue == PrefixTxt) + { + prefix = "txt"; + } + else if (prefixValue == PrefixMkd) + { + prefix = "mkd"; + } + else + { + prefix = RespConstants.UTF8.GetString(span.Slice(0, 3)); + } + span = span.Slice(4); + } + return RespConstants.UTF8.GetString(span); + } + finally + { + ArrayPool.Shared.Return(pooled); + } + } + + private static readonly uint + PrefixTxt = RespConstants.UnsafeCpuUInt32("txt:"u8), + PrefixMkd = RespConstants.UnsafeCpuUInt32("mkd:"u8); + + /// + /// Reads the current element as a string value. + /// + public readonly byte[]? ReadByteArray() + { + byte[] pooled = []; + try + { + var span = Buffer(ref pooled, stackalloc byte[256]); + if (span.IsEmpty) + { + return IsNull ? null : []; + } + return span.ToArray(); + } + finally + { + ArrayPool.Shared.Return(pooled); + } + } + + /// + /// Reads the current element using a general purpose text parser. + /// + /// The type of data being parsed. + public readonly T ParseBytes(Parser parser) + { + byte[] pooled = []; + var span = Buffer(ref pooled, stackalloc byte[256]); + try + { + return parser(span); + } + finally + { + ArrayPool.Shared.Return(pooled); + } + } + + /// + /// Reads the current element using a general purpose text parser. + /// + /// The type of data being parsed. + /// State required by the parser. + public readonly T ParseBytes(Parser parser, TState? state) + { + byte[] pooled = []; + var span = Buffer(ref pooled, stackalloc byte[256]); + try + { + return parser(span, default); + } + finally + { + ArrayPool.Shared.Return(pooled); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly ReadOnlySpan Buffer(Span target) + { + if (TryGetSpan(out var simple)) + { + return simple; + } + +#if NET6_0_OR_GREATER + return BufferSlow(ref Unsafe.NullRef(), target, usePool: false); +#else + byte[] pooled = []; + return BufferSlow(ref pooled, target, usePool: false); +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly ReadOnlySpan Buffer(scoped ref byte[] pooled, Span target = default) + => TryGetSpan(out var simple) ? simple : BufferSlow(ref pooled, target, true); + + [MethodImpl(MethodImplOptions.NoInlining)] + private readonly ReadOnlySpan BufferSlow(scoped ref byte[] pooled, Span target, bool usePool) + { + DemandScalar(); + + if (IsInlineScalar && usePool) + { + // grow to the correct size in advance, if needed + var length = ScalarLength(); + if (length > target.Length) + { + var bigger = ArrayPool.Shared.Rent(length); + ArrayPool.Shared.Return(pooled); + target = pooled = bigger; + } + } + + var iterator = ScalarChunks(); + ReadOnlySpan current; + int offset = 0; + while (iterator.MoveNext()) + { + // will the current chunk fit? + current = iterator.Current; + if (current.TryCopyTo(target.Slice(offset))) + { + // fits into the current buffer + offset += current.Length; + } + else if (!usePool) + { + // rent disallowed; fill what we can + var available = target.Slice(offset); + current.Slice(0, available.Length).CopyTo(available); + return target; // we filled it + } + else + { + // rent a bigger buffer, copy and recycle + var bigger = ArrayPool.Shared.Rent(offset + current.Length); + if (offset != 0) + { + target.Slice(0, offset).CopyTo(bigger); + } + ArrayPool.Shared.Return(pooled); + target = pooled = bigger; + current.CopyTo(target.Slice(offset)); + } + } + return target.Slice(0, offset); + } + + /// + /// Reads the current element using a general purpose byte parser. + /// + /// The type of data being parsed. + public readonly T ParseChars(Parser parser) + { + byte[] bArr = []; + char[] cArr = []; + try + { + var bSpan = Buffer(ref bArr, stackalloc byte[128]); + var maxChars = RespConstants.UTF8.GetMaxCharCount(bSpan.Length); + Span cSpan = maxChars <= 128 ? stackalloc char[128] : (cArr = ArrayPool.Shared.Rent(maxChars)); + int chars = RespConstants.UTF8.GetChars(bSpan, cSpan); + return parser(cSpan.Slice(0, chars)); + } + finally + { + ArrayPool.Shared.Return(bArr); + ArrayPool.Shared.Return(cArr); + } + } + + /// + /// Reads the current element using a general purpose byte parser. + /// + /// The type of data being parsed. + /// State required by the parser. + public readonly T ParseChars(Parser parser, TState? state) + { + byte[] bArr = []; + char[] cArr = []; + try + { + var bSpan = Buffer(ref bArr, stackalloc byte[128]); + var maxChars = RespConstants.UTF8.GetMaxCharCount(bSpan.Length); + Span cSpan = maxChars <= 128 ? stackalloc char[128] : (cArr = ArrayPool.Shared.Rent(maxChars)); + int chars = RespConstants.UTF8.GetChars(bSpan, cSpan); + return parser(cSpan.Slice(0, chars), state); + } + finally + { + ArrayPool.Shared.Return(bArr); + ArrayPool.Shared.Return(cArr); + } + } + +#if NET7_0_OR_GREATER + /// + /// Reads the current element using . + /// + /// The type of data being parsed. +#pragma warning disable RS0016, RS0027 // back-compat overload + public readonly T ParseChars(IFormatProvider? formatProvider = null) where T : ISpanParsable +#pragma warning restore RS0016, RS0027 // back-compat overload + { + byte[] bArr = []; + char[] cArr = []; + try + { + var bSpan = Buffer(ref bArr, stackalloc byte[128]); + var maxChars = RespConstants.UTF8.GetMaxCharCount(bSpan.Length); + Span cSpan = maxChars <= 128 ? stackalloc char[128] : (cArr = ArrayPool.Shared.Rent(maxChars)); + int chars = RespConstants.UTF8.GetChars(bSpan, cSpan); + return T.Parse(cSpan.Slice(0, chars), formatProvider ?? CultureInfo.InvariantCulture); + } + finally + { + ArrayPool.Shared.Return(bArr); + ArrayPool.Shared.Return(cArr); + } + } +#endif + +#if NET8_0_OR_GREATER + /// + /// Reads the current element using . + /// + /// The type of data being parsed. +#pragma warning disable RS0016, RS0027 // back-compat overload + public readonly T ParseBytes(IFormatProvider? formatProvider = null) where T : IUtf8SpanParsable +#pragma warning restore RS0016, RS0027 // back-compat overload + { + byte[] bArr = []; + try + { + var bSpan = Buffer(ref bArr, stackalloc byte[128]); + return T.Parse(bSpan, formatProvider ?? CultureInfo.InvariantCulture); + } + finally + { + ArrayPool.Shared.Return(bArr); + } + } +#endif + + /// + /// General purpose parsing callback. + /// + /// The type of source data being parsed. + /// State required by the parser. + /// The output type of data being parsed. + public delegate TValue Parser(ReadOnlySpan value, TState? state); + + /// + /// General purpose parsing callback. + /// + /// The type of source data being parsed. + /// The output type of data being parsed. + public delegate TValue Parser(ReadOnlySpan value); + + /// + /// Initializes a new instance of the struct. + /// + /// The raw contents to parse with this instance. + public RespReader(ReadOnlySpan value) + { + _length = 0; + _flags = RespFlags.None; + _prefix = RespPrefix.None; + SetCurrent(value); + + _remainingTailLength = _positionBase = 0; + _tail = null; + } + + private void MovePastCurrent() + { + // skip past the trailing portion of a value, if any + var skip = TrailingLength; + if (_bufferIndex + skip <= CurrentLength) + { + _bufferIndex += skip; // available in the current buffer + } + else + { + AdvanceSlow(skip); + } + + // reset the current state + _length = 0; + _flags = 0; + _prefix = RespPrefix.None; + } + + /// + public RespReader(scoped in ReadOnlySequence value) +#if NETCOREAPP3_0_OR_GREATER + : this(value.FirstSpan) +#else + : this(value.First.Span) +#endif + { + if (!value.IsSingleSegment) + { + _remainingTailLength = value.Length - CurrentLength; + _tail = (value.Start.GetObject() as ReadOnlySequenceSegment)?.Next ?? MissingNext(); + } + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + static ReadOnlySequenceSegment MissingNext() => throw new ArgumentException("Unable to extract tail segment", nameof(value)); + } + + /// + /// Attempt to move to the next RESP element. + /// + /// Unless you are intentionally handling errors, attributes and streaming data, should be preferred. + [EditorBrowsable(EditorBrowsableState.Never), Browsable(false)] + public unsafe bool TryReadNext() + { + MovePastCurrent(); + +#if NETCOREAPP3_0_OR_GREATER + // check what we have available; don't worry about zero/fetching the next segment; this is only + // for SIMD lookup, and zero would only apply when data ends exactly on segment boundaries, which + // is incredible niche + var available = CurrentAvailable; + + if (Avx2.IsSupported && Bmi1.IsSupported && available >= sizeof(uint)) + { + // read the first 4 bytes + ref byte origin = ref UnsafeCurrent; + var comparand = Unsafe.ReadUnaligned(ref origin); + + // broadcast those 4 bytes into a vector, mask to get just the first and last byte, and apply a SIMD equality test with our known cases + var eqs = Avx2.CompareEqual(Avx2.And(Avx2.BroadcastScalarToVector256(&comparand), Raw.FirstLastMask), Raw.CommonRespPrefixes); + + // reinterpret that as floats, and pick out the sign bits (which will be 1 for "equal", 0 for "not equal"); since the + // test cases are mutually exclusive, we expect zero or one matches, so: lzcount tells us which matched + var index = Bmi1.TrailingZeroCount((uint)Avx.MoveMask(Unsafe.As, Vector256>(ref eqs))); + int len; +#if DEBUG + if (VectorizeDisabled) index = uint.MaxValue; // just to break the switch +#endif + switch (index) + { + case Raw.CommonRespIndex_Success when available >= 5 && Unsafe.Add(ref origin, 4) == (byte)'\n': + _prefix = RespPrefix.SimpleString; + _length = 2; + _bufferIndex++; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + return true; + case Raw.CommonRespIndex_SingleDigitInteger when Unsafe.Add(ref origin, 2) == (byte)'\r': + _prefix = RespPrefix.Integer; + _length = 1; + _bufferIndex++; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + return true; + case Raw.CommonRespIndex_DoubleDigitInteger when available >= 5 && Unsafe.Add(ref origin, 4) == (byte)'\n': + _prefix = RespPrefix.Integer; + _length = 2; + _bufferIndex++; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + return true; + case Raw.CommonRespIndex_SingleDigitString when Unsafe.Add(ref origin, 2) == (byte)'\r': + if (comparand == RespConstants.BulkStringStreaming) + { + _flags = RespFlags.IsScalar | RespFlags.IsStreaming; + } + else + { + len = ParseSingleDigit(Unsafe.Add(ref origin, 1)); + if (available < len + 6) break; // need more data + + UnsafeAssertClLf(4 + len); + _length = len; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + } + _prefix = RespPrefix.BulkString; + _bufferIndex += 4; + return true; + case Raw.CommonRespIndex_DoubleDigitString when available >= 5 && Unsafe.Add(ref origin, 4) == (byte)'\n': + if (comparand == RespConstants.BulkStringNull) + { + _length = 0; + _flags = RespFlags.IsScalar | RespFlags.IsNull; + } + else + { + len = ParseDoubleDigitsNonNegative(ref Unsafe.Add(ref origin, 1)); + if (available < len + 7) break; // need more data + + UnsafeAssertClLf(5 + len); + _length = len; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + } + _prefix = RespPrefix.BulkString; + _bufferIndex += 5; + return true; + case Raw.CommonRespIndex_SingleDigitArray when Unsafe.Add(ref origin, 2) == (byte)'\r': + if (comparand == RespConstants.ArrayStreaming) + { + _flags = RespFlags.IsAggregate | RespFlags.IsStreaming; + } + else + { + _flags = RespFlags.IsAggregate; + _length = ParseSingleDigit(Unsafe.Add(ref origin, 1)); + } + _prefix = RespPrefix.Array; + _bufferIndex += 4; + return true; + case Raw.CommonRespIndex_DoubleDigitArray when available >= 5 && Unsafe.Add(ref origin, 4) == (byte)'\n': + if (comparand == RespConstants.ArrayNull) + { + _flags = RespFlags.IsAggregate | RespFlags.IsNull; + } + else + { + _length = ParseDoubleDigitsNonNegative(ref Unsafe.Add(ref origin, 1)); + _flags = RespFlags.IsAggregate; + } + _prefix = RespPrefix.Array; + _bufferIndex += 5; + return true; + case Raw.CommonRespIndex_Error: + len = UnsafePastPrefix().IndexOf(RespConstants.CrlfBytes); + if (len < 0) break; // need more data + + _prefix = RespPrefix.SimpleError; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar | RespFlags.IsError; + _length = len; + _bufferIndex++; + return true; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static int ParseDoubleDigitsNonNegative(ref byte value) => (10 * ParseSingleDigit(value)) + ParseSingleDigit(Unsafe.Add(ref value, 1)); +#endif + + // no fancy vectorization, but: we can still try to find the payload the fast way in a single segment + if (_bufferIndex + 3 <= CurrentLength) // shortest possible RESP fragment is length 3 + { + var remaining = UnsafePastPrefix(); + switch (_prefix = UnsafePeekPrefix()) + { + case RespPrefix.SimpleString: + case RespPrefix.SimpleError: + case RespPrefix.Integer: + case RespPrefix.Boolean: + case RespPrefix.Double: + case RespPrefix.BigInteger: + // CRLF-terminated + _length = remaining.IndexOf(RespConstants.CrlfBytes); + if (_length < 0) break; // can't find, need more data + _bufferIndex++; // payload follows prefix directly + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + if (_prefix == RespPrefix.SimpleError) _flags |= RespFlags.IsError; + return true; + case RespPrefix.BulkError: + case RespPrefix.BulkString: + case RespPrefix.VerbatimString: + // length prefix with value payload; first, the length + switch (TryReadLengthPrefix(remaining, out _length, out int consumed)) + { + case LengthPrefixResult.Length: + // still need to valid terminating CRLF + if (remaining.Length < consumed + _length + 2) break; // need more data + UnsafeAssertClLf(1 + consumed + _length); + + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + break; + case LengthPrefixResult.Null: + _flags = RespFlags.IsScalar | RespFlags.IsNull; + break; + case LengthPrefixResult.Streaming: + _flags = RespFlags.IsScalar | RespFlags.IsStreaming; + break; + } + if (_flags == 0) break; // will need more data to know + if (_prefix == RespPrefix.BulkError) _flags |= RespFlags.IsError; + _bufferIndex += 1 + consumed; + return true; + case RespPrefix.StreamContinuation: + // length prefix, possibly with value payload; first, the length + switch (TryReadLengthPrefix(remaining, out _length, out consumed)) + { + case LengthPrefixResult.Length when _length == 0: + // EOF, no payload + _flags = RespFlags.IsScalar; // don't claim as streaming, we want this to count towards delta-decrement + break; + case LengthPrefixResult.Length: + // still need to valid terminating CRLF + if (remaining.Length < consumed + _length + 2) break; // need more data + UnsafeAssertClLf(1 + consumed + _length); + + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar | RespFlags.IsStreaming; + break; + case LengthPrefixResult.Null: + case LengthPrefixResult.Streaming: + ThrowProtocolFailure("Invalid streaming scalar length prefix"); + break; + } + if (_flags == 0) break; // will need more data to know + _bufferIndex += 1 + consumed; + return true; + case RespPrefix.Array: + case RespPrefix.Set: + case RespPrefix.Map: + case RespPrefix.Push: + case RespPrefix.Attribute: + // length prefix without value payload (child values follow) + switch (TryReadLengthPrefix(remaining, out _length, out consumed)) + { + case LengthPrefixResult.Length: + _flags = RespFlags.IsAggregate; + if (AggregateLengthNeedsDoubling()) _length *= 2; + break; + case LengthPrefixResult.Null: + _flags = RespFlags.IsAggregate | RespFlags.IsNull; + break; + case LengthPrefixResult.Streaming: + _flags = RespFlags.IsAggregate | RespFlags.IsStreaming; + break; + } + if (_flags == 0) break; // will need more data to know + if (_prefix is RespPrefix.Attribute) _flags |= RespFlags.IsAttribute; + _bufferIndex += consumed + 1; + return true; + case RespPrefix.Null: // null + // note we already checked we had 3 bytes + UnsafeAssertClLf(1); + _flags = RespFlags.IsScalar | RespFlags.IsNull; + _bufferIndex += 3; // skip prefix+terminator + return true; + case RespPrefix.StreamTerminator: + // note we already checked we had 3 bytes + UnsafeAssertClLf(1); + _flags = RespFlags.IsAggregate; // don't claim as streaming - this counts towards delta + _bufferIndex += 3; // skip prefix+terminator + return true; + default: + ThrowProtocolFailure("Unexpected protocol prefix: " + _prefix); + return false; + } + } + + return TryReadNextSlow(ref this); + } + + private static bool TryReadNextSlow(ref RespReader live) + { + // in the case of failure, we don't want to apply any changes, + // so we work against an isolated copy until we're happy + live.MovePastCurrent(); + RespReader isolated = live; + + int next = isolated.RawTryReadByte(); + if (next < 0) return false; + + switch (isolated._prefix = (RespPrefix)next) + { + case RespPrefix.SimpleString: + case RespPrefix.SimpleError: + case RespPrefix.Integer: + case RespPrefix.Boolean: + case RespPrefix.Double: + case RespPrefix.BigInteger: + // CRLF-terminated + if (!isolated.RawTryFindCrLf(out isolated._length)) return false; + isolated._flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + if (isolated._prefix == RespPrefix.SimpleError) isolated._flags |= RespFlags.IsError; + break; + case RespPrefix.BulkError: + case RespPrefix.BulkString: + case RespPrefix.VerbatimString: + // length prefix with value payload + switch (isolated.RawTryReadLengthPrefix()) + { + case LengthPrefixResult.Length: + // still need to valid terminating CRLF + isolated._flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + if (!isolated.RawTryAssertInlineScalarPayloadCrLf()) return false; + break; + case LengthPrefixResult.Null: + isolated._flags = RespFlags.IsScalar | RespFlags.IsNull; + break; + case LengthPrefixResult.Streaming: + isolated._flags = RespFlags.IsScalar | RespFlags.IsStreaming; + break; + case LengthPrefixResult.NeedMoreData: + return false; + default: + ThrowProtocolFailure("Unexpected length prefix"); + return false; + } + if (isolated._prefix == RespPrefix.BulkError) isolated._flags |= RespFlags.IsError; + break; + case RespPrefix.Array: + case RespPrefix.Set: + case RespPrefix.Map: + case RespPrefix.Push: + case RespPrefix.Attribute: + // length prefix without value payload (child values follow) + switch (isolated.RawTryReadLengthPrefix()) + { + case LengthPrefixResult.Length: + isolated._flags = RespFlags.IsAggregate; + if (isolated.AggregateLengthNeedsDoubling()) isolated._length *= 2; + break; + case LengthPrefixResult.Null: + isolated._flags = RespFlags.IsAggregate | RespFlags.IsNull; + break; + case LengthPrefixResult.Streaming: + isolated._flags = RespFlags.IsAggregate | RespFlags.IsStreaming; + break; + case LengthPrefixResult.NeedMoreData: + return false; + default: + ThrowProtocolFailure("Unexpected length prefix"); + return false; + } + if (isolated._prefix is RespPrefix.Attribute) isolated._flags |= RespFlags.IsAttribute; + break; + case RespPrefix.Null: // null + if (!isolated.RawAssertCrLf()) return false; + isolated._flags = RespFlags.IsScalar | RespFlags.IsNull; + break; + case RespPrefix.StreamTerminator: + if (!isolated.RawAssertCrLf()) return false; + isolated._flags = RespFlags.IsAggregate; // don't claim as streaming - this counts towards delta + break; + case RespPrefix.StreamContinuation: + // length prefix, possibly with value payload; first, the length + switch (isolated.RawTryReadLengthPrefix()) + { + case LengthPrefixResult.Length when isolated._length == 0: + // EOF, no payload + isolated._flags = RespFlags.IsScalar; // don't claim as streaming, we want this to count towards delta-decrement + break; + case LengthPrefixResult.Length: + // still need to valid terminating CRLF + isolated._flags = RespFlags.IsScalar | RespFlags.IsInlineScalar | RespFlags.IsStreaming; + if (!isolated.RawTryAssertInlineScalarPayloadCrLf()) return false; // need more data + break; + case LengthPrefixResult.Null: + case LengthPrefixResult.Streaming: + ThrowProtocolFailure("Invalid streaming scalar length prefix"); + break; + case LengthPrefixResult.NeedMoreData: + default: + return false; + } + break; + default: + ThrowProtocolFailure("Unexpected protocol prefix: " + isolated._prefix); + return false; + } + // commit the speculative changes back, and accept + live = isolated; + return true; + } + + private void AdvanceSlow(long bytes) + { + while (bytes > 0) + { + var available = CurrentLength - _bufferIndex; + if (bytes <= available) + { + _bufferIndex += (int)bytes; + return; + } + bytes -= available; + + if (!TryMoveToNextSegment()) Throw(); + } + + [DoesNotReturn] + static void Throw() => throw new EndOfStreamException("Unexpected end of payload; this is unexpected because we already validated that it was available!"); + } + + private bool AggregateLengthNeedsDoubling() => _prefix is RespPrefix.Map or RespPrefix.Attribute; + + private bool TryMoveToNextSegment() + { + while (_tail is not null && _remainingTailLength > 0) + { + var memory = _tail.Memory; + _tail = _tail.Next; + if (!memory.IsEmpty) + { + var span = memory.Span; // check we can get this before mutating anything + _positionBase += CurrentLength; + if (span.Length > _remainingTailLength) + { + span = span.Slice(0, (int)_remainingTailLength); + _remainingTailLength = 0; + } + else + { + _remainingTailLength -= span.Length; + } + SetCurrent(span); + _bufferIndex = 0; + return true; + } + } + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly bool IsOK() // go mad with this, because it is used so often + { + return TryGetSpan(out var span) && span.Length == 2 + ? Unsafe.ReadUnaligned(ref UnsafeCurrent) == RespConstants.OKUInt16 + : IsSlow(RespConstants.OKBytes); + } + + /// + /// Indicates whether the current element is a scalar with a value that matches the provided . + /// + /// The payload value to verify. + public readonly bool Is(ReadOnlySpan value) + => TryGetSpan(out var span) ? span.SequenceEqual(value) : IsSlow(value); + + internal readonly bool IsInlneCpuUInt32(uint value) + { + if (IsInlineScalar && _length == sizeof(uint)) + { + return CurrentAvailable >= sizeof(uint) + ? Unsafe.ReadUnaligned(ref UnsafeCurrent) == value + : SlowIsInlneCpuUInt32(value); + } + + return false; + } + + private readonly bool SlowIsInlneCpuUInt32(uint value) + { + Debug.Assert(IsInlineScalar && _length == sizeof(uint), "should be inline scalar of length 4"); + Span buffer = stackalloc byte[sizeof(uint)]; + var copy = this; + copy.RawFillBytes(buffer); + return RespConstants.UnsafeCpuUInt32(buffer) == value; + } + + /// + /// Indicates whether the current element is a scalar with a value that matches the provided . + /// + /// The payload value to verify. + public readonly bool Is(byte value) + { + if (IsInlineScalar && _length == 1 && CurrentAvailable >= 1) + { + return UnsafeCurrent == value; + } + + ReadOnlySpan span = [value]; + return IsSlow(span); + } + + private readonly bool IsSlow(ReadOnlySpan testValue) + { + DemandScalar(); + if (IsNull) return false; // nothing equals null + if (TotalAvailable < testValue.Length) return false; + + if (!IsStreaming && testValue.Length != ScalarLength()) return false; + + var iterator = ScalarChunks(); + while (true) + { + if (testValue.IsEmpty) + { + // nothing left to test; if also nothing left to read, great! + return !iterator.MoveNext(); + } + if (!iterator.MoveNext()) + { + return false; // test is longer + } + + var current = iterator.Current; + if (testValue.Length < current.Length) return false; // payload is longer + + if (!current.SequenceEqual(testValue.Slice(0, current.Length))) return false; // payload is different + + testValue = testValue.Slice(current.Length); // validated; continue + } + } + + /// + /// Copy the current scalar value out into the supplied , or as much as can be copied. + /// + /// The destination for the copy operation. + /// The number of bytes successfully copied. + public readonly int CopyTo(Span target) + { + if (TryGetSpan(out var value)) + { + if (target.Length < value.Length) value = value.Slice(0, target.Length); + + value.CopyTo(target); + return value.Length; + } + + int totalBytes = 0; + var iterator = ScalarChunks(); + while (iterator.MoveNext()) + { + value = iterator.Current; + if (target.Length <= value.Length) + { + value.Slice(0, target.Length).CopyTo(target); + return totalBytes + target.Length; + } + + value.CopyTo(target); + target = target.Slice(value.Length); + totalBytes += value.Length; + } + return totalBytes; + } + + /// + /// Copy the current scalar value out into the supplied , or as much as can be copied. + /// + /// The destination for the copy operation. + /// The number of bytes successfully copied. + public readonly int CopyTo(IBufferWriter target) + { + if (TryGetSpan(out var value)) + { + target.Write(value); + return value.Length; + } + + int totalBytes = 0; + var iterator = ScalarChunks(); + while (iterator.MoveNext()) + { + value = iterator.Current; + target.Write(value); + totalBytes += value.Length; + } + return totalBytes; + } + + /// + /// Asserts that the current element is not null. + /// + public void DemandNotNull() + { + if (IsNull) Throw(); + static void Throw() => throw new InvalidOperationException("A non-null element was expected"); + } + + /// + /// Read the current element as a value. + /// + [SuppressMessage("Style", "IDE0018:Inline variable declaration", Justification = "No it can't - conditional")] + public readonly long ReadInt64() + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesInt64 + 1]); + long value; + if (!(span.Length <= RespConstants.MaxRawBytesInt64 + && Utf8Parser.TryParse(span, out value, out int bytes) + && bytes == span.Length)) + { + ThrowFormatException(); + value = 0; + } + return value; + } + + /// + /// Try to read the current element as a value. + /// + public readonly bool TryReadInt64(out long value) + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesInt64 + 1]); + if (span.Length <= RespConstants.MaxRawBytesInt64) + { + return Utf8Parser.TryParse(span, out value, out int bytes) & bytes == span.Length; + } + + value = 0; + return false; + } + + /// + /// Read the current element as a value. + /// + [SuppressMessage("Style", "IDE0018:Inline variable declaration", Justification = "No it can't - conditional")] + public readonly int ReadInt32() + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesInt32 + 1]); + int value; + if (!(span.Length <= RespConstants.MaxRawBytesInt32 + && Utf8Parser.TryParse(span, out value, out int bytes) + && bytes == span.Length)) + { + ThrowFormatException(); + value = 0; + } + return value; + } + + /// + /// Try to read the current element as a value. + /// + public readonly bool TryReadInt32(out int value) + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesInt32 + 1]); + if (span.Length <= RespConstants.MaxRawBytesInt32) + { + return Utf8Parser.TryParse(span, out value, out int bytes) & bytes == span.Length; + } + + value = 0; + return false; + } + + /// + /// Read the current element as a value. + /// + public readonly double ReadDouble() + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesNumber + 1]); + + if (span.Length <= RespConstants.MaxRawBytesNumber + && Utf8Parser.TryParse(span, out double value, out int bytes) + && bytes == span.Length) + { + return value; + } + switch (span.Length) + { + case 3 when "inf"u8.SequenceEqual(span): + return double.PositiveInfinity; + case 3 when "nan"u8.SequenceEqual(span): + return double.NaN; + case 4 when "+inf"u8.SequenceEqual(span): // not actually mentioned in spec, but: we'll allow it + return double.PositiveInfinity; + case 4 when "-inf"u8.SequenceEqual(span): + return double.NegativeInfinity; + } + ThrowFormatException(); + return 0; + } + + /// + /// Try to read the current element as a value. + /// + public bool TryReadDouble(out double value, bool allowTokens = true) + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesNumber + 1]); + + if (span.Length <= RespConstants.MaxRawBytesNumber + && Utf8Parser.TryParse(span, out value, out int bytes) + && bytes == span.Length) + { + return true; + } + + if (allowTokens) + { + switch (span.Length) + { + case 3 when "inf"u8.SequenceEqual(span): + value = double.PositiveInfinity; + return true; + case 3 when "nan"u8.SequenceEqual(span): + value = double.NaN; + return true; + case 4 when "+inf"u8.SequenceEqual(span): // not actually mentioned in spec, but: we'll allow it + value = double.PositiveInfinity; + return true; + case 4 when "-inf"u8.SequenceEqual(span): + value = double.NegativeInfinity; + return true; + } + } + + value = 0; + return false; + } + + internal readonly bool TryReadShortAscii(out string value) + { + const int ShortLength = 31; + + var span = Buffer(stackalloc byte[ShortLength + 1]); + value = ""; + if (span.IsEmpty) return true; + + if (span.Length <= ShortLength) + { + // check for anything that looks binary or unicode + foreach (var b in span) + { + // allow [SPACE]-thru-[DEL], plus CR/LF + if (!(b < 127 & (b >= 32 | (b is 12 or 13)))) + { + return false; + } + } + + value = Encoding.UTF8.GetString(span); + return true; + } + + return false; + } + + /// + /// Read the current element as a value. + /// + [SuppressMessage("Style", "IDE0018:Inline variable declaration", Justification = "No it can't - conditional")] + public readonly decimal ReadDecimal() + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesNumber + 1]); + decimal value; + if (!(span.Length <= RespConstants.MaxRawBytesNumber + && Utf8Parser.TryParse(span, out value, out int bytes) + && bytes == span.Length)) + { + ThrowFormatException(); + value = 0; + } + return value; + } + + /// + /// Read the current element as a value. + /// + public readonly bool ReadBoolean() + { + var span = Buffer(stackalloc byte[2]); + if (span.Length == 1) + { + switch (span[0]) + { + case (byte)'0' when Prefix == RespPrefix.Integer: return false; + case (byte)'1' when Prefix == RespPrefix.Integer: return true; + case (byte)'f' when Prefix == RespPrefix.Boolean: return false; + case (byte)'t' when Prefix == RespPrefix.Boolean: return true; + } + } + ThrowFormatException(); + return false; + } + + /// + /// Parse a scalar value as an enum of type . + /// + /// The value to report if the value is not recognized. + /// The type of enum being parsed. + public readonly T ReadEnum(T unknownValue = default) where T : struct, Enum + { +#if NET6_0_OR_GREATER + return ParseChars(static (chars, state) => Enum.TryParse(chars, true, out T value) ? value : state, unknownValue); +#else + return Enum.TryParse(ReadString(), true, out T value) ? value : unknownValue; +#endif + } + + public T[]? ReadArray(Projection projection) + { + DemandAggregate(); + if (IsNull) return null; + var len = AggregateLength(); + if (len == 0) return []; + T[] result = new T[len]; + FillAll(result, projection); + return result; + } +} diff --git a/src/RESPite/Messages/RespScanState.cs b/src/RESPite/Messages/RespScanState.cs new file mode 100644 index 000000000..f40d08b96 --- /dev/null +++ b/src/RESPite/Messages/RespScanState.cs @@ -0,0 +1,160 @@ +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace RESPite.Messages; + +/// +/// Holds state used for RESP frame parsing, i.e. detecting the RESP for an entire top-level message. +/// +public struct RespScanState +{ + /* + The key point of ScanState is to skim over a RESP stream with minimal frame processing, to find the + end of a single top-level RESP message. We start by expecting 1 message, and then just read, with the + rules that the end of a message subtracts one, and aggregates add N. Streaming scalars apply zero offset + until the scalar stream terminator. Attributes also apply zero offset. + Note that streaming aggregates change the rules - when at least one streaming aggregate is in effect, + no offsets are applied until we get back out of the outermost streaming aggregate - we achieve this + by simply counting the streaming aggregate depth, which is usually zero. + Note that in reality streaming (scalar and aggregates) and attributes are non-existent; in addition + to being specific to RESP3, no known server currently implements these parts of the RESP3 specification, + so everything here is theoretical, but: works according to the spec. + */ + private int _delta; // when this becomes -1, we have fully read a top-level message; + private ushort _streamingAggregateDepth; + private RespPrefix _prefix; + + public RespPrefix Prefix => _prefix; + + private long _totalBytes; +#if DEBUG + private int _elementCount; + + /// + public override string ToString() => $"{_prefix}, consumed: {_totalBytes} bytes, {_elementCount} nodes, complete: {IsComplete}"; +#else + /// + public override string ToString() => _prefix.ToString(); +#endif + + /// + public override bool Equals([NotNullWhen(true)] object? obj) => throw new NotSupportedException(); + + /// + public override int GetHashCode() => throw new NotSupportedException(); + + /// + /// Gets whether an entire top-level RESP message has been consumed. + /// + public bool IsComplete => _delta == -1; + + /// + /// Gets the total length of the payload read (or read so far, if it is not yet complete); this combines payloads from multiple + /// TryRead operations. + /// + public long TotalBytes => _totalBytes; + + // used when spotting common replies - we entirely bypass the usual reader/delta mechanism + internal void SetComplete(int totalBytes, RespPrefix prefix) + { + _totalBytes = totalBytes; + _delta = -1; + _prefix = prefix; +#if DEBUG + _elementCount = 1; +#endif + } + + /// + /// The amount of data, in bytes, to read before attempting to read the next frame. + /// + public const int MinBytes = 3; // minimum legal RESP frame is: _\r\n + + /// + /// Create a new value that can parse the supplied node (and subtree). + /// + internal RespScanState(in RespReader reader) + { + Debug.Assert(reader.Prefix != RespPrefix.None, "missing RESP prefix"); + _totalBytes = 0; + _delta = reader.GetInitialScanCount(out _streamingAggregateDepth); + } + + /// + /// Scan as far as possible, stopping when an entire top-level RESP message has been consumed or the data is exhausted. + /// + /// True if a top-level RESP message has been consumed. + public bool TryRead(ref RespReader reader, out long bytesRead) + { + bytesRead = ReadCore(ref reader, reader.BytesConsumed); + return IsComplete; + } + + /// + /// Scan as far as possible, stopping when an entire top-level RESP message has been consumed or the data is exhausted. + /// + /// True if a top-level RESP message has been consumed. + public bool TryRead(ReadOnlySpan value, out int bytesRead) + { + var reader = new RespReader(value); + bytesRead = (int)ReadCore(ref reader); + return IsComplete; + } + + /// + /// Scan as far as possible, stopping when an entire top-level RESP message has been consumed or the data is exhausted. + /// + /// True if a top-level RESP message has been consumed. + public bool TryRead(in ReadOnlySequence value, out long bytesRead) + { + var reader = new RespReader(in value); + bytesRead = ReadCore(ref reader); + return IsComplete; + } + + /// + /// Scan as far as possible, stopping when an entire top-level RESP message has been consumed or the data is exhausted. + /// + /// The number of bytes consumed in this operation. + private long ReadCore(ref RespReader reader, long startOffset = 0) + { + while (_delta >= 0 && reader.TryReadNext()) + { +#if DEBUG + _elementCount++; +#endif + if (!reader.IsAttribute & _prefix == RespPrefix.None) + { + _prefix = reader.Prefix; + } + + if (reader.IsAggregate) ApplyAggregateRules(ref reader); + + if (_streamingAggregateDepth == 0) _delta += reader.Delta(); + } + + var bytesRead = reader.BytesConsumed - startOffset; + _totalBytes += bytesRead; + return bytesRead; + } + + private void ApplyAggregateRules(ref RespReader reader) + { + Debug.Assert(reader.IsAggregate, "RESP aggregate expected"); + if (reader.IsStreaming) + { + // entering an aggregate stream + if (_streamingAggregateDepth == ushort.MaxValue) ThrowTooDeep(); + _streamingAggregateDepth++; + } + else if (reader.Prefix == RespPrefix.StreamTerminator) + { + // exiting an aggregate stream + if (_streamingAggregateDepth == 0) ThrowUnexpectedTerminator(); + _streamingAggregateDepth--; + } + static void ThrowTooDeep() => throw new InvalidOperationException("Maximum streaming aggregate depth exceeded."); + static void ThrowUnexpectedTerminator() => throw new InvalidOperationException("Unexpected streaming aggregate terminator."); + } +} diff --git a/src/RESPite/RESPite.csproj b/src/RESPite/RESPite.csproj new file mode 100644 index 000000000..24de49d05 --- /dev/null +++ b/src/RESPite/RESPite.csproj @@ -0,0 +1,60 @@ + + + + true + net461;netstandard2.0;net472;net6.0;net8.0;net9.0 + enable + enable + $(NoWarn);CS1591 + 2025 - $([System.DateTime]::Now.Year) Marc Gravell + + + + + + + + + + + RespOperation.cs + + + RespMessage.cs + + + RespMessageBase.cs + + + RespMessageBase.cs + + + RespReader.cs + + + RespReader.cs + + + RespReader.cs + + + RespReader.cs + + + RespReader.cs + + + + + + FrameworkShims.cs + + + NullableHacks.cs + + + SkipLocalsInit.cs + + + + diff --git a/src/RESPite/RespCommandMap.cs b/src/RESPite/RespCommandMap.cs new file mode 100644 index 000000000..3d5f28cb1 --- /dev/null +++ b/src/RESPite/RespCommandMap.cs @@ -0,0 +1,25 @@ +namespace RESPite; + +public abstract class RespCommandMap +{ + /// + /// Apply any remapping to the command. + /// + /// The command requested. + /// The remapped command; this can be the original command, a remapped command, or an empty instance if the command is not available. + public abstract ReadOnlySpan Map(ReadOnlySpan command); + + /// + /// Indicates whether the specified command is available. + /// + public virtual bool IsAvailable(ReadOnlySpan command) + => Map(command).Length != 0; + + public static RespCommandMap Default { get; } = new DefaultRespCommandMap(); + + private sealed class DefaultRespCommandMap : RespCommandMap + { + public override ReadOnlySpan Map(ReadOnlySpan command) => command; + public override bool IsAvailable(ReadOnlySpan command) => true; + } +} diff --git a/src/RESPite/RespConfiguration.cs b/src/RESPite/RespConfiguration.cs new file mode 100644 index 000000000..638d94857 --- /dev/null +++ b/src/RESPite/RespConfiguration.cs @@ -0,0 +1,89 @@ +using System.Text; + +namespace RESPite; + +/// +/// Over-arching configuration for a RESP system. +/// +public class RespConfiguration +{ + private static readonly TimeSpan DefaultSyncTimeout = TimeSpan.FromSeconds(10); + + public static RespConfiguration Default { get; } = new( + RespCommandMap.Default, [], DefaultSyncTimeout, NullServiceProvider.Instance); + + public static Builder Create() => default; // for discoverability + + public struct Builder // intentionally mutable + { + public TimeSpan? SyncTimeout { get; set; } + public IServiceProvider? ServiceProvider { get; set; } + public RespCommandMap? CommandMap { get; set; } + public object? KeyPrefix { get; set; } // can be a string or byte[] + + public Builder(RespConfiguration? source) + { + if (source is not null) + { + CommandMap = source.RespCommandMap; + SyncTimeout = source.SyncTimeout; + KeyPrefix = source.KeyPrefix.ToArray(); + ServiceProvider = source.ServiceProvider; + // undo defaults + if (ReferenceEquals(CommandMap, RespCommandMap.Default)) CommandMap = null; + if (ReferenceEquals(ServiceProvider, NullServiceProvider.Instance)) ServiceProvider = null; + } + } + + public RespConfiguration Create() + { + byte[] prefix = KeyPrefix switch + { + null => [], + string { Length: 0 } => [], + string s => Encoding.UTF8.GetBytes(s), + byte[] { Length: 0 } => [], + byte[] b => b.AsSpan().ToArray(), // create isolated copy for mutability reasons + _ => throw new ArgumentException($"{nameof(KeyPrefix)} must be a string or byte[]", nameof(KeyPrefix)), + }; + + if (prefix.Length == 0 & SyncTimeout is null & CommandMap is null & ServiceProvider is null) return Default; + + return new( + CommandMap ?? RespCommandMap.Default, + prefix, + SyncTimeout ?? DefaultSyncTimeout, + ServiceProvider ?? NullServiceProvider.Instance); + } + } + + private RespConfiguration( + RespCommandMap respCommandMap, + byte[] keyPrefix, + TimeSpan syncTimeout, + IServiceProvider serviceProvider) + { + RespCommandMap = respCommandMap; + SyncTimeout = syncTimeout; + _keyPrefix = (byte[])keyPrefix.Clone(); // create isolated copy + ServiceProvider = serviceProvider; + } + + private readonly byte[] _keyPrefix; + public IServiceProvider ServiceProvider { get; } + public RespCommandMap RespCommandMap { get; } + public TimeSpan SyncTimeout { get; } + public ReadOnlySpan KeyPrefix => _keyPrefix; + + public Builder AsBuilder() => new(this); + + private sealed class NullServiceProvider : IServiceProvider + { + public static readonly NullServiceProvider Instance = new(); + private NullServiceProvider() { } + public object? GetService(Type serviceType) => null; + } + + internal T? GetService() where T : class + => ServiceProvider.GetService(typeof(T)) as T; +} diff --git a/src/RESPite/RespContext.cs b/src/RESPite/RespContext.cs new file mode 100644 index 000000000..009ba366c --- /dev/null +++ b/src/RESPite/RespContext.cs @@ -0,0 +1,92 @@ +using System.Runtime.CompilerServices; +using RESPite.Connections; + +namespace RESPite; + +/// +/// Transient state for a RESP operation. +/// +public readonly struct RespContext +{ + private readonly IRespConnection _connection; + private readonly int _database; + private readonly CancellationToken _cancellationToken; + + private const string CtorUsageWarning = $"The context from {nameof(IRespConnection)}.{nameof(IRespConnection.Context)} should be preferred, using {nameof(WithCancellationToken)} etc as necessary."; + + /// + public override string ToString() => _connection?.ToString() ?? "(null)"; + + [Obsolete(CtorUsageWarning)] + public RespContext(IRespConnection connection) : this(connection, -1, CancellationToken.None) + { + } + + [Obsolete(CtorUsageWarning)] + public RespContext(IRespConnection connection, CancellationToken cancellationToken) + : this(connection, -1, cancellationToken) + { + } + + /// + /// Transient state for a RESP operation. + /// + [Obsolete(CtorUsageWarning)] + public RespContext( + IRespConnection connection, + int database = -1, + CancellationToken cancellationToken = default) + { + _connection = connection; + _database = database; + _cancellationToken = cancellationToken; + } + + public IRespConnection Connection => _connection; + public int Database => _database; + public CancellationToken CancellationToken => _cancellationToken; +/* + public RespMessageBuilder Command(ReadOnlySpan command, T value, IRespFormatter formatter) + => new(this, command, value, formatter); + + public RespMessageBuilder Command(ReadOnlySpan command) + => new(this, command, Void.Instance, RespFormatters.Void); + + public RespMessageBuilder Command(ReadOnlySpan command, string value, bool isKey) + => new(this, command, value, RespFormatters.String(isKey)); + + public RespMessageBuilder Command(ReadOnlySpan command, byte[] value, bool isKey) + => new(this, command, value, RespFormatters.ByteArray(isKey)); + */ + + public RespCommandMap RespCommandMap => _connection.Configuration.RespCommandMap; + + public RespContext WithCancellationToken(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + RespContext clone = this; + Unsafe.AsRef(in clone._cancellationToken) = cancellationToken; + return clone; + } + + public RespContext WithDatabase(int database) + { + RespContext clone = this; + Unsafe.AsRef(in clone._database) = database; + return clone; + } + + public RespContext WithConnection(IRespConnection connection) + { + RespContext clone = this; + Unsafe.AsRef(in clone._connection) = connection; + return clone; + } + + public IBatchConnection CreateBatch(int sizeHint = 0) => new BatchConnection(in this, sizeHint); + + internal static RespContext For(IRespConnection connection) +#pragma warning disable CS0618 // Type or member is obsolete + => new(connection); +#pragma warning restore CS0618 // Type or member is obsolete +} diff --git a/src/RESPite/RespException.cs b/src/RESPite/RespException.cs new file mode 100644 index 000000000..86a344577 --- /dev/null +++ b/src/RESPite/RespException.cs @@ -0,0 +1,8 @@ +namespace RESPite; + +/// +/// Represents a RESP error message. +/// +public sealed class RespException(string message) : Exception(message) +{ +} diff --git a/src/RESPite/RespOperation.cs b/src/RESPite/RespOperation.cs new file mode 100644 index 000000000..ba60f2fb9 --- /dev/null +++ b/src/RESPite/RespOperation.cs @@ -0,0 +1,137 @@ +using System.Buffers; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Threading.Tasks.Sources; +using RESPite.Internal; + +namespace RESPite; + +/// +/// Represents a RESP operation that does not return a value (other than to signal completion). +/// This works almost identically to when based on +/// , and the usage semantics are the same. In particular, +/// note that a value can only be consumed once. Unlike , the +/// value can be awaited synchronously if required. +/// +public readonly struct RespOperation : ICriticalNotifyCompletion +{ + // it is important that this layout remains identical between RespOperation and RespOperation + private readonly IRespMessage _message; + private readonly short _token; + private readonly bool _disableCaptureContext; // default is false, so: bypass + + internal RespOperation(IRespMessage message, bool disableCaptureContext = false) + { + _message = message; + _token = message.Token; + _disableCaptureContext = disableCaptureContext; + } + + internal IRespMessage Message => _message ?? ThrowNoMessage(); + + internal static IRespMessage ThrowNoMessage() + => throw new InvalidOperationException($"{nameof(RespOperation)} is not correctly initialized"); + + /// + /// Treats this operation as a . + /// + public static implicit operator ValueTask(in RespOperation operation) + => new(operation.Message, operation._token); + + /// + public Task AsTask() + { + ValueTask vt = this; + return vt.AsTask(); + } + + /// + public void Wait(TimeSpan timeout = default) + => Message.Wait(_token, timeout); + + /// + public bool IsCompleted => Message.GetStatus(_token) != ValueTaskSourceStatus.Pending; + + /// + public bool IsCompletedSuccessfully => Message.GetStatus(_token) == ValueTaskSourceStatus.Succeeded; + + /// + public bool IsFaulted => Message.GetStatus(_token) == ValueTaskSourceStatus.Faulted; + + /// + public bool IsCanceled => Message.GetStatus(_token) == ValueTaskSourceStatus.Canceled; + + internal short Token => _token; + + internal static readonly Action InvokeState = static state => ((Action)state!).Invoke(); + + /// + /// + public void OnCompleted(Action continuation) + { + // UseSchedulingContext === continueOnCapturedContext, always add FlowExecutionContext + var flags = _disableCaptureContext + ? ValueTaskSourceOnCompletedFlags.FlowExecutionContext + : ValueTaskSourceOnCompletedFlags.FlowExecutionContext | + ValueTaskSourceOnCompletedFlags.UseSchedulingContext; + Message.OnCompleted(InvokeState, continuation, _token, flags); + } + + /// + public void UnsafeOnCompleted(Action continuation) + { + // UseSchedulingContext === continueOnCapturedContext + var flags = _disableCaptureContext + ? ValueTaskSourceOnCompletedFlags.None + : ValueTaskSourceOnCompletedFlags.UseSchedulingContext; + Message.OnCompleted(InvokeState, continuation, _token, flags); + } + + /// + public void GetResult() => Message.GetResult(_token); + + /// + public RespOperation GetAwaiter() => this; + + /// + public RespOperation ConfigureAwait(bool continueOnCapturedContext) + { + var clone = this; + Unsafe.AsRef(in clone._disableCaptureContext) = !continueOnCapturedContext; + return clone; + } + + /// + /// Provides a mechanism to control the outcome of a ; this is mostly + /// intended for testing purposes. It is broadly comparable to . + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public readonly struct Remote + { + private readonly IRespMessage _message; + private readonly short _token; + internal Remote(IRespMessage message) + { + _message = message; + _token = message.Token; + } + + /// + public bool TrySetCanceled(CancellationToken cancellationToken = default) + => _message.TrySetCanceled(_token); + + /// + public bool TrySetException(Exception exception) + => _message.TrySetException(_token, exception); + + /// + /// The parser provided during creation is used to process the result. + public bool TrySetResult(scoped ReadOnlySpan response) + => _message.TrySetResult(_token, response); + + /// + /// The parser provided during creation is used to process the result. + public bool TrySetResult(in ReadOnlySequence response) + => _message.TrySetResult(_token, response); + } +} diff --git a/src/RESPite/RespOperationT.cs b/src/RESPite/RespOperationT.cs new file mode 100644 index 000000000..69ad38666 --- /dev/null +++ b/src/RESPite/RespOperationT.cs @@ -0,0 +1,136 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Threading.Tasks.Sources; +using RESPite.Internal; +using RESPite.Messages; + +namespace RESPite; + +/// +/// Represents a RESP operation that returns a value of type . +/// This works almost identically to when based on +/// , and the usage semantics are the same. In particular, +/// note that a value can only be consumed once. Unlike , the +/// value can be awaited synchronously if required. +/// +/// The type of value returned by the operation. +public readonly struct RespOperation +{ + // it is important that this layout remains identical between RespOperation and RespOperation + private readonly RespMessageBase _message; + private readonly short _token; + private readonly bool _disableCaptureContext; + + internal RespOperation(RespMessageBase message, bool disableCaptureContext = false) + { + _message = message; + _token = message.Token; + _disableCaptureContext = disableCaptureContext; + } + + internal IRespMessage Message => _message ?? RespOperation.ThrowNoMessage(); + private RespMessageBase TypedMessage => _message ?? (RespMessageBase)RespOperation.ThrowNoMessage(); + + /// + /// Treats this operation as an untyped . + /// + #if PREVIEW_LANGVER + [Obsolete($"When possible, prefer .Untyped")] + #endif + public static implicit operator RespOperation(in RespOperation operation) + => Unsafe.As, RespOperation>(ref Unsafe.AsRef(in operation)); + + /// + /// Treats this operation as an untyped . + /// + public static implicit operator ValueTask(in RespOperation operation) + => new(operation.TypedMessage, operation._token); + + /// + /// Treats this operation as a . + /// + public static implicit operator ValueTask(in RespOperation operation) + => new(operation.TypedMessage, operation._token); + + /// + public Task AsTask() + { + ValueTask vt = this; + return vt.AsTask(); + } + + /// + public T Wait(TimeSpan timeout = default) + => TypedMessage.Wait(_token, timeout); + + /// + public bool IsCompleted => TypedMessage.GetStatus(_token) != ValueTaskSourceStatus.Pending; + + /// + public bool IsCompletedSuccessfully => TypedMessage.GetStatus(_token) == ValueTaskSourceStatus.Succeeded; + + /// + public bool IsFaulted => TypedMessage.GetStatus(_token) == ValueTaskSourceStatus.Faulted; + + /// + public bool IsCanceled => TypedMessage.GetStatus(_token) == ValueTaskSourceStatus.Canceled; + + /// + /// + public void OnCompleted(Action continuation) + { + // UseSchedulingContext === continueOnCapturedContext, always add FlowExecutionContext + var flags = _disableCaptureContext + ? ValueTaskSourceOnCompletedFlags.FlowExecutionContext + : ValueTaskSourceOnCompletedFlags.FlowExecutionContext | + ValueTaskSourceOnCompletedFlags.UseSchedulingContext; + TypedMessage.OnCompleted(RespOperation.InvokeState, continuation, _token, flags); + } + + /// + public void UnsafeOnCompleted(Action continuation) + { + // UseSchedulingContext === continueOnCapturedContext + var flags = _disableCaptureContext + ? ValueTaskSourceOnCompletedFlags.None + : ValueTaskSourceOnCompletedFlags.UseSchedulingContext; + TypedMessage.OnCompleted(RespOperation.InvokeState, continuation, _token, flags); + } + + /// + public T GetResult() => TypedMessage.GetResult(_token); + + /// + public RespOperation GetAwaiter() => this; + + /// + public RespOperation ConfigureAwait(bool continueOnCapturedContext) + { + var clone = this; + Unsafe.AsRef(in clone._disableCaptureContext) = !continueOnCapturedContext; + return clone; + } + + /// + /// Create a disconnected with a RESP parser; this is only intended for testing purposes. + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static RespOperation Create(IRespParser? parser, out RespOperation.Remote remote) + { + var msg = RespMessage.Get(parser); + remote = new(msg); + return new RespOperation(msg); + } + + /// + /// Create a disconnected with a stateful RESP parser; this is only intended for testing purposes. + /// + /// The state used by the parser. + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static RespOperation Create(in TState state, IRespParser? parser, out RespOperation.Remote remote) + { + var msg = RespMessage.Get(in state, parser); + remote = new(msg); + return new RespOperation(msg); + } +} diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index c0021f024..e9c1d1ac2 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -330,9 +330,9 @@ internal static LocalCertificateSelectionCallback CreatePemUserCertificateCallba { // PEM handshakes not universally supported and causes a runtime error about ephemeral certificates; to avoid, export as PFX using var pem = X509Certificate2.CreateFromPemFile(userCertificatePath, userKeyPath); -#pragma warning disable SYSLIB0057 // Type or member is obsolete +#pragma warning disable SYSLIB0057 // because of TFM support var pfx = new X509Certificate2(pem.Export(X509ContentType.Pfx)); -#pragma warning restore SYSLIB0057 // Type or member is obsolete +#pragma warning restore SYSLIB0057 return (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => pfx; } @@ -340,7 +340,9 @@ internal static LocalCertificateSelectionCallback CreatePemUserCertificateCallba internal static LocalCertificateSelectionCallback CreatePfxUserCertificateCallback(string userCertificatePath, string? password, X509KeyStorageFlags storageFlags = X509KeyStorageFlags.DefaultKeySet) { +#pragma warning disable SYSLIB0057 // because of TFM support var pfx = new X509Certificate2(userCertificatePath, password ?? "", storageFlags); +#pragma warning restore SYSLIB0057 return (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => pfx; } @@ -351,7 +353,9 @@ internal static LocalCertificateSelectionCallback CreatePfxUserCertificateCallba public void TrustIssuer(X509Certificate2 issuer) => CertificateValidationCallback = TrustIssuerCallback(issuer); internal static RemoteCertificateValidationCallback TrustIssuerCallback(string issuerCertificatePath) +#pragma warning disable SYSLIB0057 // because of TFM support => TrustIssuerCallback(new X509Certificate2(issuerCertificatePath)); +#pragma warning restore SYSLIB0057 private static RemoteCertificateValidationCallback TrustIssuerCallback(X509Certificate2 issuer) { if (issuer == null) throw new ArgumentNullException(nameof(issuer)); diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Debug.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Debug.cs index da3f61be9..9b30ac141 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Debug.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Debug.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis; public partial class ConnectionMultiplexer { private static int _collectedWithoutDispose; - internal static int CollectedWithoutDispose => Thread.VolatileRead(ref _collectedWithoutDispose); + internal static int CollectedWithoutDispose => Volatile.Read(ref _collectedWithoutDispose); /// /// Invoked by the garbage collector. diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index bf6b66674..935edaa65 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -58,17 +58,17 @@ public sealed partial class ConnectionMultiplexer : IInternalConnectionMultiplex private int lastReconfigiureTicks = Environment.TickCount; internal long LastReconfigureSecondsAgo => - unchecked(Environment.TickCount - Thread.VolatileRead(ref lastReconfigiureTicks)) / 1000; + unchecked(Environment.TickCount - Volatile.Read(ref lastReconfigiureTicks)) / 1000; private int _activeHeartbeatErrors, lastHeartbeatTicks; internal long LastHeartbeatSecondsAgo => pulse is null ? -1 - : unchecked(Environment.TickCount - Thread.VolatileRead(ref lastHeartbeatTicks)) / 1000; + : unchecked(Environment.TickCount - Volatile.Read(ref lastHeartbeatTicks)) / 1000; private static int lastGlobalHeartbeatTicks = Environment.TickCount; internal static long LastGlobalHeartbeatSecondsAgo => - unchecked(Environment.TickCount - Thread.VolatileRead(ref lastGlobalHeartbeatTicks)) / 1000; + unchecked(Environment.TickCount - Volatile.Read(ref lastGlobalHeartbeatTicks)) / 1000; /// [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludeDetailInExceptions)} instead - this will be removed in 3.0.")] @@ -210,7 +210,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt { throw ExceptionFactory.AdminModeNotEnabled(RawConfig.IncludeDetailInExceptions, cmd, null, server); } - var srv = server.GetRedisServer(null); + var srv = new RedisServer(this, server, null); if (!srv.IsConnected) { throw ExceptionFactory.NoConnectionAvailable(this, null, server, GetServerSnapshot(), command: cmd); @@ -1229,21 +1229,7 @@ public IServer GetServer(EndPoint? endpoint, object? asyncState = null) throw new NotSupportedException($"The server API is not available via {RawConfig.Proxy}"); } var server = servers[endpoint] as ServerEndPoint ?? throw new ArgumentException("The specified endpoint is not defined", nameof(endpoint)); - return server.GetRedisServer(asyncState); - } - - /// -#pragma warning disable RS0026 - public IServer GetServer(RedisKey key, object? asyncState = null, CommandFlags flags = CommandFlags.None) -#pragma warning restore RS0026 - { - // We'll spoof the GET command for this; we're not supporting ad-hoc access to the pub/sub channel, because: bad things. - // Any read-only-replica vs writable-primary concerns should be managed by the caller via "flags"; the default is PreferPrimary. - // Note that ServerSelectionStrategy treats "null" (default) keys as NoSlot, aka Any. - return (SelectServer(RedisCommand.GET, flags, key) ?? Throw()).GetRedisServer(asyncState); - - [DoesNotReturn] - static ServerEndPoint Throw() => throw new InvalidOperationException("It was not possible to resolve a connection to the server owning the specified key"); + return new RedisServer(this, server, asyncState); } /// @@ -1255,7 +1241,7 @@ public IServer[] GetServers() var result = new IServer[snapshot.Length]; for (var i = 0; i < snapshot.Length; i++) { - result[i] = snapshot[i].GetRedisServer(null); + result[i] = new RedisServer(this, snapshot[i], null); } return result; } diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 7e4eca49a..3cfb0268c 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -107,7 +107,7 @@ internal static Exception NoConnectionAvailable( serverSnapshot = new ServerEndPoint[] { server }; } - var innerException = PopulateInnerExceptions(serverSnapshot == default ? multiplexer.GetServerSnapshot() : serverSnapshot); + var innerException = PopulateInnerExceptions(serverSnapshot.IsEmpty ? multiplexer.GetServerSnapshot() : serverSnapshot); // Try to get a useful error message for the user. long attempts = multiplexer._connectAttemptCount, completions = multiplexer._connectCompletedCount; diff --git a/src/StackExchange.Redis/FrameworkShims.cs b/src/StackExchange.Redis/FrameworkShims.cs index 9472df9ae..0c264bf5c 100644 --- a/src/StackExchange.Redis/FrameworkShims.cs +++ b/src/StackExchange.Redis/FrameworkShims.cs @@ -5,8 +5,12 @@ [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))] #else // To support { get; init; } properties +using System.Buffers; using System.ComponentModel; +using System.Runtime.InteropServices; using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace System.Runtime.CompilerServices { @@ -16,12 +20,112 @@ internal static class IsExternalInit { } #endif #if !(NETCOREAPP || NETSTANDARD2_1_OR_GREATER) +namespace System.IO +{ + internal static class StreamExtensions + { + public static void Write(this Stream stream, ReadOnlyMemory value) + { + if (MemoryMarshal.TryGetArray(value, out var segment)) + { + stream.Write(segment.Array!, segment.Offset, segment.Count); + } + else + { + var leased = ArrayPool.Shared.Rent(value.Length); + value.CopyTo(leased); + stream.Write(leased, 0, value.Length); + ArrayPool.Shared.Return(leased); // on success only + } + } + + public static int Read(this Stream stream, Memory value) + { + if (MemoryMarshal.TryGetArray(value, out var segment)) + { + return stream.Read(segment.Array!, segment.Offset, segment.Count); + } + else + { + var leased = ArrayPool.Shared.Rent(value.Length); + int bytes = stream.Read(leased, 0, value.Length); + if (bytes > 0) + { + leased.AsSpan(0, bytes).CopyTo(value.Span); + } + ArrayPool.Shared.Return(leased); // on success only + return bytes; + } + } + public static ValueTask ReadAsync(this Stream stream, Memory value, CancellationToken cancellationToken) + { + if (MemoryMarshal.TryGetArray(value, out var segment)) + { + return new(stream.ReadAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken)); + } + else + { + var leased = ArrayPool.Shared.Rent(value.Length); + var pending = stream.ReadAsync(leased, 0, value.Length, cancellationToken); + if (!pending.IsCompleted) + { + return Awaited(pending, value, leased); + } + + var bytes = pending.GetAwaiter().GetResult(); + if (bytes > 0) + { + leased.AsSpan(0, bytes).CopyTo(value.Span); + } + ArrayPool.Shared.Return(leased); // on success only + return new(bytes); + + static async ValueTask Awaited(Task pending, Memory value, byte[] leased) + { + var bytes = await pending.ConfigureAwait(false); + if (bytes > 0) + { + leased.AsSpan(0, bytes).CopyTo(value.Span); + } + ArrayPool.Shared.Return(leased); // on success only + return bytes; + } + } + } + + public static ValueTask WriteAsync(this Stream stream, ReadOnlyMemory value, CancellationToken cancellationToken) + { + if (MemoryMarshal.TryGetArray(value, out var segment)) + { + return new(stream.WriteAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken)); + } + else + { + var leased = ArrayPool.Shared.Rent(value.Length); + value.CopyTo(leased); + var pending = stream.WriteAsync(leased, 0, value.Length, cancellationToken); + if (!pending.IsCompleted) + { + return Awaited(pending, leased); + } + pending.GetAwaiter().GetResult(); + ArrayPool.Shared.Return(leased); // on success only + return default; + } + static async ValueTask Awaited(Task pending, byte[] leased) + { + await pending.ConfigureAwait(false); + ArrayPool.Shared.Return(leased); // on success only + } + } + } +} namespace System.Text { - internal static class EncodingExtensions + internal static unsafe class EncodingExtensions { - public static unsafe int GetBytes(this Encoding encoding, ReadOnlySpan source, Span destination) + public static int GetBytes(this Encoding encoding, ReadOnlySpan source, Span destination) { fixed (byte* bPtr = destination) { @@ -31,6 +135,42 @@ public static unsafe int GetBytes(this Encoding encoding, ReadOnlySpan sou } } } + public static string GetString(this Encoding encoding, ReadOnlySpan source) + { + fixed (byte* bPtr = source) + { + return encoding.GetString(bPtr, source.Length); + } + } + public static int GetChars(this Encoding encoding, ReadOnlySpan source, Span destination) + { + fixed (byte* bPtr = source) + { + fixed (char* cPtr = destination) + { + return encoding.GetChars(bPtr, source.Length, cPtr, destination.Length); + } + } + } + + public static int GetByteCount(this Encoding encoding, ReadOnlySpan source) + { + fixed (char* cPtr = source) + { + return encoding.GetByteCount(cPtr, source.Length); + } + } + + public static void Convert(this Encoder encoder, ReadOnlySpan source, Span destination, bool flush, out int charsUsed, out int bytesUsed, out bool completed) + { + fixed (char* cPtr = source) + { + fixed (byte* bPtr = destination) + { + encoder.Convert(cPtr, source.Length, bPtr, destination.Length, flush, out charsUsed, out bytesUsed, out completed); + } + } + } } } #endif diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs index 96b4ce8f6..b4bdb0950 100644 --- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs @@ -8,311 +8,299 @@ using StackExchange.Redis.Profiling; using static StackExchange.Redis.ConnectionMultiplexer; -namespace StackExchange.Redis; - -internal interface IInternalConnectionMultiplexer : IConnectionMultiplexer +namespace StackExchange.Redis { - bool AllowConnect { get; set; } - - bool IgnoreConnect { get; set; } - - ReadOnlySpan GetServerSnapshot(); - ServerEndPoint GetServerEndPoint(EndPoint endpoint); - - ConfigurationOptions RawConfig { get; } - - long? GetConnectionId(EndPoint endPoint, ConnectionType type); - - ServerSelectionStrategy ServerSelectionStrategy { get; } - - int GetSubscriptionsCount(); - ConcurrentDictionary GetSubscriptions(); - - ConnectionMultiplexer UnderlyingMultiplexer { get; } -} - -/// -/// Represents the abstract multiplexer API. -/// -public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable -{ - /// - /// Gets the client-name that will be used on all new connections. - /// - string ClientName { get; } - - /// - /// Gets the configuration of the connection. - /// - string Configuration { get; } - - /// - /// Gets the timeout associated with the connections. - /// - int TimeoutMilliseconds { get; } - - /// - /// The number of operations that have been performed on all connections. - /// - long OperationCount { get; } - - /// - /// Gets or sets whether asynchronous operations should be invoked in a way that guarantees their original delivery order. - /// - [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue), false)] - [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - bool PreserveAsyncOrder { get; set; } - - /// - /// Indicates whether any servers are connected. - /// - bool IsConnected { get; } - - /// - /// Indicates whether any servers are connecting. - /// - bool IsConnecting { get; } - - /// - /// Should exceptions include identifiable details? (key names, additional annotations). - /// - [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludeDetailInExceptions)} instead - this will be removed in 3.0.")] - [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - bool IncludeDetailInExceptions { get; set; } - - /// - /// Limit at which to start recording unusual busy patterns (only one log will be retained at a time. - /// Set to a negative value to disable this feature). - /// - int StormLogThreshold { get; set; } - - /// - /// Register a callback to provide an on-demand ambient session provider based on the calling context. - /// The implementing code is responsible for reliably resolving the same provider - /// based on ambient context, or returning null to not profile. - /// - /// The profiling session provider. - void RegisterProfiler(Func profilingSessionProvider); - - /// - /// Get summary statistics associates with this server. - /// - ServerCounters GetCounters(); - - /// - /// A server replied with an error message. - /// - event EventHandler ErrorMessage; - - /// - /// Raised whenever a physical connection fails. - /// - event EventHandler ConnectionFailed; - - /// - /// Raised whenever an internal error occurs (this is primarily for debugging). - /// - event EventHandler InternalError; - - /// - /// Raised whenever a physical connection is established. - /// - event EventHandler ConnectionRestored; - - /// - /// Raised when configuration changes are detected. - /// - event EventHandler ConfigurationChanged; - - /// - /// Raised when nodes are explicitly requested to reconfigure via broadcast. - /// This usually means primary/replica changes. - /// - event EventHandler ConfigurationChangedBroadcast; - - /// - /// Raised when server indicates a maintenance event is going to happen. - /// - event EventHandler ServerMaintenanceEvent; - - /// - /// Gets all endpoints defined on the multiplexer. - /// - /// Whether to return only the explicitly configured endpoints. - EndPoint[] GetEndPoints(bool configuredOnly = false); - - /// - /// Wait for a given asynchronous operation to complete (or timeout). - /// - /// The task to wait on. - void Wait(Task task); - - /// - /// Wait for a given asynchronous operation to complete (or timeout). - /// - /// The type in . - /// The task to wait on. - T Wait(Task task); - - /// - /// Wait for the given asynchronous operations to complete (or timeout). - /// - /// The tasks to wait on. - void WaitAll(params Task[] tasks); - - /// - /// Raised when a hash-slot has been relocated. - /// - event EventHandler HashSlotMoved; - - /// - /// Compute the hash-slot of a specified key. - /// - /// The key to get a slot ID for. - int HashSlot(RedisKey key); - - /// - /// Obtain a pub/sub subscriber connection to the specified server. - /// - /// The async state to pass to the created . - ISubscriber GetSubscriber(object? asyncState = null); - - /// - /// Obtain an interactive connection to a database inside redis. - /// - /// The database ID to get. - /// The async state to pass to the created . - IDatabase GetDatabase(int db = -1, object? asyncState = null); - - /// - /// Obtain a configuration API for an individual server. - /// - /// The host to get a server for. - /// The specific port for to get a server for. - /// The async state to pass to the created . - IServer GetServer(string host, int port, object? asyncState = null); - - /// - /// Obtain a configuration API for an individual server. - /// - /// The "host:port" string to get a server for. - /// The async state to pass to the created . - IServer GetServer(string hostAndPort, object? asyncState = null); - - /// - /// Obtain a configuration API for an individual server. - /// - /// The host to get a server for. - /// The specific port for to get a server for. - IServer GetServer(IPAddress host, int port); - - /// - /// Obtain a configuration API for an individual server. - /// - /// The endpoint to get a server for. - /// The async state to pass to the created . - IServer GetServer(EndPoint endpoint, object? asyncState = null); - - /// - /// Gets a server that would be used for a given key and flags. - /// - /// The endpoint to get a server for. In a non-cluster environment, this parameter is ignored. A key may be specified - /// on cluster, which will return a connection to an arbitrary server matching the specified flags. - /// The async state to pass to the created . - /// The command flags to use. - /// This method is particularly useful when communicating with a cluster environment, to obtain a connection to the server that owns the specified key - /// and ad-hoc commands with unusual routing requirements. Note that provides a connection that automatically routes commands by - /// looking for parameters, so this method is only necessary when used with commands that do not take a parameter, - /// but require consistent routing using key-like semantics. - IServer GetServer(RedisKey key, object? asyncState = null, CommandFlags flags = CommandFlags.None); - - /// - /// Obtain configuration APIs for all servers in this multiplexer. - /// - IServer[] GetServers(); - - /// - /// Reconfigure the current connections based on the existing configuration. - /// - /// The log to write output to. - Task ConfigureAsync(TextWriter? log = null); - - /// - /// Reconfigure the current connections based on the existing configuration. - /// - /// The log to write output to. - bool Configure(TextWriter? log = null); - - /// - /// Provides a text overview of the status of all connections. - /// - string GetStatus(); - - /// - /// Provides a text overview of the status of all connections. - /// - /// The log to write output to. - void GetStatus(TextWriter log); - - /// - /// See . - /// - string ToString(); - - /// - /// Close all connections and release all resources associated with this object. - /// - /// Whether to allow in-queue commands to complete first. - void Close(bool allowCommandsToComplete = true); - - /// - /// Close all connections and release all resources associated with this object. - /// - /// Whether to allow in-queue commands to complete first. - Task CloseAsync(bool allowCommandsToComplete = true); - - /// - /// Obtains the log of unusual busy patterns. - /// - string? GetStormLog(); - - /// - /// Resets the log of unusual busy patterns. - /// - void ResetStormLog(); - - /// - /// Request all compatible clients to reconfigure or reconnect. - /// - /// The command flags to use. - /// The number of instances known to have received the message (however, the actual number can be higher; returns -1 if the operation is pending). - long PublishReconfigure(CommandFlags flags = CommandFlags.None); - - /// - /// Request all compatible clients to reconfigure or reconnect. - /// - /// The command flags to use. - /// The number of instances known to have received the message (however, the actual number can be higher). - Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None); - - /// - /// Get the hash-slot associated with a given key, if applicable; this can be useful for grouping operations. - /// - /// The key to get a the slot for. - int GetHashSlot(RedisKey key); - - /// - /// Write the configuration of all servers to an output stream. - /// - /// The destination stream to write the export to. - /// The options to use for this export. - void ExportConfiguration(Stream destination, ExportOptions options = ExportOptions.All); - - /// - /// Append a usage-specific modifier to the advertised library name; suffixes are de-duplicated - /// and sorted alphabetically (so adding 'a', 'b' and 'a' will result in suffix '-a-b'). - /// Connections will be updated as necessary (RESP2 subscription - /// connections will not show updates until those connections next connect). - /// - void AddLibraryNameSuffix(string suffix); + internal interface IInternalConnectionMultiplexer : IConnectionMultiplexer + { + bool AllowConnect { get; set; } + + bool IgnoreConnect { get; set; } + + ReadOnlySpan GetServerSnapshot(); + ServerEndPoint GetServerEndPoint(EndPoint endpoint); + + ConfigurationOptions RawConfig { get; } + + long? GetConnectionId(EndPoint endPoint, ConnectionType type); + + ServerSelectionStrategy ServerSelectionStrategy { get; } + + int GetSubscriptionsCount(); + ConcurrentDictionary GetSubscriptions(); + + ConnectionMultiplexer UnderlyingMultiplexer { get; } + } + + /// + /// Represents the abstract multiplexer API. + /// + public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable + { + /// + /// Gets the client-name that will be used on all new connections. + /// + string ClientName { get; } + + /// + /// Gets the configuration of the connection. + /// + string Configuration { get; } + + /// + /// Gets the timeout associated with the connections. + /// + int TimeoutMilliseconds { get; } + + /// + /// The number of operations that have been performed on all connections. + /// + long OperationCount { get; } + + /// + /// Gets or sets whether asynchronous operations should be invoked in a way that guarantees their original delivery order. + /// + [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue), false)] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + bool PreserveAsyncOrder { get; set; } + + /// + /// Indicates whether any servers are connected. + /// + bool IsConnected { get; } + + /// + /// Indicates whether any servers are connecting. + /// + bool IsConnecting { get; } + + /// + /// Should exceptions include identifiable details? (key names, additional annotations). + /// + [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludeDetailInExceptions)} instead - this will be removed in 3.0.")] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + bool IncludeDetailInExceptions { get; set; } + + /// + /// Limit at which to start recording unusual busy patterns (only one log will be retained at a time. + /// Set to a negative value to disable this feature). + /// + int StormLogThreshold { get; set; } + + /// + /// Register a callback to provide an on-demand ambient session provider based on the calling context. + /// The implementing code is responsible for reliably resolving the same provider + /// based on ambient context, or returning null to not profile. + /// + /// The profiling session provider. + void RegisterProfiler(Func profilingSessionProvider); + + /// + /// Get summary statistics associates with this server. + /// + ServerCounters GetCounters(); + + /// + /// A server replied with an error message. + /// + event EventHandler ErrorMessage; + + /// + /// Raised whenever a physical connection fails. + /// + event EventHandler ConnectionFailed; + + /// + /// Raised whenever an internal error occurs (this is primarily for debugging). + /// + event EventHandler InternalError; + + /// + /// Raised whenever a physical connection is established. + /// + event EventHandler ConnectionRestored; + + /// + /// Raised when configuration changes are detected. + /// + event EventHandler ConfigurationChanged; + + /// + /// Raised when nodes are explicitly requested to reconfigure via broadcast. + /// This usually means primary/replica changes. + /// + event EventHandler ConfigurationChangedBroadcast; + + /// + /// Raised when server indicates a maintenance event is going to happen. + /// + event EventHandler ServerMaintenanceEvent; + + /// + /// Gets all endpoints defined on the multiplexer. + /// + /// Whether to return only the explicitly configured endpoints. + EndPoint[] GetEndPoints(bool configuredOnly = false); + + /// + /// Wait for a given asynchronous operation to complete (or timeout). + /// + /// The task to wait on. + void Wait(Task task); + + /// + /// Wait for a given asynchronous operation to complete (or timeout). + /// + /// The type in . + /// The task to wait on. + T Wait(Task task); + + /// + /// Wait for the given asynchronous operations to complete (or timeout). + /// + /// The tasks to wait on. + void WaitAll(params Task[] tasks); + + /// + /// Raised when a hash-slot has been relocated. + /// + event EventHandler HashSlotMoved; + + /// + /// Compute the hash-slot of a specified key. + /// + /// The key to get a slot ID for. + int HashSlot(RedisKey key); + + /// + /// Obtain a pub/sub subscriber connection to the specified server. + /// + /// The async state to pass to the created . + ISubscriber GetSubscriber(object? asyncState = null); + + /// + /// Obtain an interactive connection to a database inside redis. + /// + /// The database ID to get. + /// The async state to pass to the created . + IDatabase GetDatabase(int db = -1, object? asyncState = null); + + /// + /// Obtain a configuration API for an individual server. + /// + /// The host to get a server for. + /// The specific port for to get a server for. + /// The async state to pass to the created . + IServer GetServer(string host, int port, object? asyncState = null); + + /// + /// Obtain a configuration API for an individual server. + /// + /// The "host:port" string to get a server for. + /// The async state to pass to the created . + IServer GetServer(string hostAndPort, object? asyncState = null); + + /// + /// Obtain a configuration API for an individual server. + /// + /// The host to get a server for. + /// The specific port for to get a server for. + IServer GetServer(IPAddress host, int port); + + /// + /// Obtain a configuration API for an individual server. + /// + /// The endpoint to get a server for. + /// The async state to pass to the created . + IServer GetServer(EndPoint endpoint, object? asyncState = null); + + /// + /// Obtain configuration APIs for all servers in this multiplexer. + /// + IServer[] GetServers(); + + /// + /// Reconfigure the current connections based on the existing configuration. + /// + /// The log to write output to. + Task ConfigureAsync(TextWriter? log = null); + + /// + /// Reconfigure the current connections based on the existing configuration. + /// + /// The log to write output to. + bool Configure(TextWriter? log = null); + + /// + /// Provides a text overview of the status of all connections. + /// + string GetStatus(); + + /// + /// Provides a text overview of the status of all connections. + /// + /// The log to write output to. + void GetStatus(TextWriter log); + + /// + /// See . + /// + string ToString(); + + /// + /// Close all connections and release all resources associated with this object. + /// + /// Whether to allow in-queue commands to complete first. + void Close(bool allowCommandsToComplete = true); + + /// + /// Close all connections and release all resources associated with this object. + /// + /// Whether to allow in-queue commands to complete first. + Task CloseAsync(bool allowCommandsToComplete = true); + + /// + /// Obtains the log of unusual busy patterns. + /// + string? GetStormLog(); + + /// + /// Resets the log of unusual busy patterns. + /// + void ResetStormLog(); + + /// + /// Request all compatible clients to reconfigure or reconnect. + /// + /// The command flags to use. + /// The number of instances known to have received the message (however, the actual number can be higher; returns -1 if the operation is pending). + long PublishReconfigure(CommandFlags flags = CommandFlags.None); + + /// + /// Request all compatible clients to reconfigure or reconnect. + /// + /// The command flags to use. + /// The number of instances known to have received the message (however, the actual number can be higher). + Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None); + + /// + /// Get the hash-slot associated with a given key, if applicable; this can be useful for grouping operations. + /// + /// The key to get a the slot for. + int GetHashSlot(RedisKey key); + + /// + /// Write the configuration of all servers to an output stream. + /// + /// The destination stream to write the export to. + /// The options to use for this export. + void ExportConfiguration(Stream destination, ExportOptions options = ExportOptions.All); + + /// + /// Append a usage-specific modifier to the advertised library name; suffixes are de-duplicated + /// and sorted alphabetically (so adding 'a', 'b' and 'a' will result in suffix '-a-b'). + /// Connections will be updated as necessary (RESP2 subscription + /// connections will not show updates until those connections next connect). + /// + void AddLibraryNameSuffix(string suffix); + } } diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index 8e4178fc9..4971c7f18 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -266,13 +266,10 @@ public partial interface IServer : IRedis /// Task ExecuteAsync(string command, params object[] args); -#pragma warning disable RS0026, RS0027 // multiple overloads /// /// Execute an arbitrary command against the server; this is primarily intended for /// executing modules, but may also be used to provide access to new features that lack - /// a direct API. The command is assumed to be not database-specific. If this is not the case, - /// should be used to - /// specify the database (using null to use the configured default database). + /// a direct API. /// /// The command to run. /// The arguments to pass for the command. @@ -283,23 +280,6 @@ public partial interface IServer : IRedis /// Task ExecuteAsync(string command, ICollection args, CommandFlags flags = CommandFlags.None); -#pragma warning restore RS0026, RS0027 - - /// - /// Execute an arbitrary database-specific command against the server; this is primarily intended for - /// executing modules, but may also be used to provide access to new features that lack - /// a direct API. - /// - /// The database ID; if , the configured default database is used. - /// The command to run. - /// The arguments to pass for the command. - /// The flags to use for this operation. - /// A dynamic representation of the command's result. - /// This API should be considered an advanced feature; inappropriate use can be harmful. - RedisResult Execute(int? database, string command, ICollection args, CommandFlags flags = CommandFlags.None); - - /// - Task ExecuteAsync(int? database, string command, ICollection args, CommandFlags flags = CommandFlags.None); /// /// Delete all the keys of all databases on the server. diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index c430cf5af..1a38b7d89 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -555,7 +555,7 @@ internal void OnFullyEstablished(PhysicalConnection connection, string source) private bool DueForConnectRetry() { - int connectTimeMilliseconds = unchecked(Environment.TickCount - Thread.VolatileRead(ref connectStartTicks)); + int connectTimeMilliseconds = unchecked(Environment.TickCount - Volatile.Read(ref connectStartTicks)); return Multiplexer.RawConfig.ReconnectRetryPolicy.ShouldRetry(Interlocked.Read(ref connectTimeoutRetryCount), connectTimeMilliseconds); } diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index c587241a0..29c85fe5d 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -261,8 +261,8 @@ private enum ReadMode : byte private readonly WeakReference _bridge; public PhysicalBridge? BridgeCouldBeNull => (PhysicalBridge?)_bridge.Target; - public long LastReadSecondsAgo => unchecked(Environment.TickCount - Thread.VolatileRead(ref lastReadTickCount)) / 1000; - public long LastWriteSecondsAgo => unchecked(Environment.TickCount - Thread.VolatileRead(ref lastWriteTickCount)) / 1000; + public long LastReadSecondsAgo => unchecked(Environment.TickCount - Volatile.Read(ref lastReadTickCount)) / 1000; + public long LastWriteSecondsAgo => unchecked(Environment.TickCount - Volatile.Read(ref lastWriteTickCount)) / 1000; private bool IncludeDetailInExceptions => BridgeCouldBeNull?.Multiplexer.RawConfig.IncludeDetailInExceptions ?? false; @@ -418,8 +418,8 @@ public void RecordConnectionFailed( if (isCurrent && Interlocked.CompareExchange(ref failureReported, 1, 0) == 0) { - int now = Environment.TickCount, lastRead = Thread.VolatileRead(ref lastReadTickCount), lastWrite = Thread.VolatileRead(ref lastWriteTickCount), - lastBeat = Thread.VolatileRead(ref lastBeatTickCount); + int now = Environment.TickCount, lastRead = Volatile.Read(ref lastReadTickCount), lastWrite = Volatile.Read(ref lastWriteTickCount), + lastBeat = Volatile.Read(ref lastBeatTickCount); int unansweredWriteTime = 0; lock (_writtenAwaitingResponse) @@ -434,7 +434,7 @@ public void RecordConnectionFailed( var exMessage = new StringBuilder(failureType.ToString()); // If the reason for the shutdown was we asked for the socket to die, don't log it as an error (only informational) - weAskedForThis = Thread.VolatileRead(ref clientSentQuit) != 0; + weAskedForThis = Volatile.Read(ref clientSentQuit) != 0; var pipe = connectingPipe ?? _ioPipe; if (pipe is SocketConnection sc) @@ -906,7 +906,7 @@ internal void WriteHeader(RedisCommand command, int arguments, CommandBytes comm internal void RecordQuit() { // don't blame redis if we fired the first shot - Thread.VolatileWrite(ref clientSentQuit, 1); + Volatile.Write(ref clientSentQuit, 1); (_ioPipe as SocketConnection)?.TrySetProtocolShutdown(PipeShutdownKind.ProtocolExitClient); } @@ -1959,7 +1959,7 @@ private async Task ReadFromPipe() { _readStatus = ReadStatus.Faulted; // this CEX is just a hardcore "seriously, read the actual value" - there's no - // convenient "Thread.VolatileRead(ref T field) where T : class", and I don't + // convenient "Volatile.Read(ref T field) where T : class", and I don't // want to make the field volatile just for this one place that needs it if (isReading) { diff --git a/src/StackExchange.Redis/Profiling/ProfilingSession.cs b/src/StackExchange.Redis/Profiling/ProfilingSession.cs index f83a49c91..3bc3caf38 100644 --- a/src/StackExchange.Redis/Profiling/ProfilingSession.cs +++ b/src/StackExchange.Redis/Profiling/ProfilingSession.cs @@ -24,7 +24,7 @@ internal void Add(ProfiledCommand command) { if (command == null) return; - object? cur = Thread.VolatileRead(ref _untypedHead); + object? cur = Volatile.Read(ref _untypedHead); while (true) { command.NextElement = (ProfiledCommand?)cur; diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index e82af2bee..66c49976e 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1957,8 +1957,4 @@ StackExchange.Redis.RedisValue.CopyTo(System.Span destination) -> int StackExchange.Redis.RedisValue.GetByteCount() -> int StackExchange.Redis.RedisValue.GetLongByteCount() -> long static StackExchange.Redis.Condition.SortedSetContainsStarting(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue prefix) -> StackExchange.Redis.Condition! -static StackExchange.Redis.Condition.SortedSetNotContainsStarting(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue prefix) -> StackExchange.Redis.Condition! -StackExchange.Redis.ConnectionMultiplexer.GetServer(StackExchange.Redis.RedisKey key, object? asyncState = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.IServer! -StackExchange.Redis.IConnectionMultiplexer.GetServer(StackExchange.Redis.RedisKey key, object? asyncState = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.IServer! -StackExchange.Redis.IServer.Execute(int? database, string! command, System.Collections.Generic.ICollection! args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! -StackExchange.Redis.IServer.ExecuteAsync(int? database, string! command, System.Collections.Generic.ICollection! args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +static StackExchange.Redis.Condition.SortedSetNotContainsStarting(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue prefix) -> StackExchange.Redis.Condition! \ No newline at end of file diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 349864a1b..bf69f25f3 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -4598,12 +4598,13 @@ private Message GetStreamAddMessage(RedisKey key, RedisValue entryId, long? maxL throw new ArgumentOutOfRangeException(nameof(maxLength), "maxLength must be greater than 0."); } + var includeMaxLen = maxLength.HasValue ? 2 : 0; + var includeApproxLen = maxLength.HasValue && useApproximateMaxLength ? 1 : 0; + var totalLength = (streamPairs.Length * 2) // Room for the name/value pairs - + 1 // The stream entry ID - + (maxLength.HasValue ? 2 : 0) // MAXLEN N - + (maxLength.HasValue && useApproximateMaxLength ? 1 : 0) // ~ - + (mode == StreamTrimMode.KeepReferences ? 0 : 1) // relevant trim-mode keyword - + (limit.HasValue ? 2 : 0); // LIMIT N + + 1 // The stream entry ID + + includeMaxLen // 2 or 0 (MAXLEN keyword & the count) + + includeApproxLen; // 1 or 0 var values = new RedisValue[totalLength]; diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index 3bc306c69..af734b0f5 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -16,9 +16,9 @@ internal sealed class RedisServer : RedisBase, IServer { private readonly ServerEndPoint server; - internal RedisServer(ServerEndPoint server, object? asyncState) : base(server.Multiplexer, asyncState) + internal RedisServer(ConnectionMultiplexer multiplexer, ServerEndPoint server, object? asyncState) : base(multiplexer, asyncState) { - this.server = server; // definitely can't be null because .Multiplexer in base call + this.server = server ?? throw new ArgumentNullException(nameof(server)); } int IServer.DatabaseCount => server.Databases; @@ -1045,20 +1045,6 @@ public Task ExecuteAsync(string command, ICollection args, return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } - public RedisResult Execute(int? database, string command, ICollection args, CommandFlags flags = CommandFlags.None) - { - var db = multiplexer.ApplyDefaultDatabase(database ?? -1); - var msg = new RedisDatabase.ExecuteMessage(multiplexer?.CommandMap, db, flags, command, args); - return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); - } - - public Task ExecuteAsync(int? database, string command, ICollection args, CommandFlags flags = CommandFlags.None) - { - var db = multiplexer.ApplyDefaultDatabase(database ?? -1); - var msg = new RedisDatabase.ExecuteMessage(multiplexer?.CommandMap, db, flags, command, args); - return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); - } - /// /// For testing only. /// diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index af98af0f7..b5f7cbb4c 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -71,12 +71,6 @@ public ServerEndPoint(ConnectionMultiplexer multiplexer, EndPoint endpoint) } } - private RedisServer? _defaultServer; - public RedisServer GetRedisServer(object? asyncState) - => asyncState is null - ? (_defaultServer ??= new RedisServer(this, null)) // reuse and memoize - : new RedisServer(this, asyncState); - public EndPoint EndPoint { get; } public ClusterConfiguration? ClusterConfiguration { get; private set; } @@ -719,7 +713,7 @@ internal void OnFullyEstablished(PhysicalConnection connection, string source) } internal int LastInfoReplicationCheckSecondsAgo => - unchecked(Environment.TickCount - Thread.VolatileRead(ref lastInfoReplicationCheckTicks)) / 1000; + unchecked(Environment.TickCount - Volatile.Read(ref lastInfoReplicationCheckTicks)) / 1000; private EndPoint? primaryEndPoint; public EndPoint? PrimaryEndPoint diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index 44efe09be..2ef94f330 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -2,7 +2,7 @@ enable - net461;netstandard2.0;net472;netcoreapp3.1;net6.0;net8.0 + net461;netstandard2.0;net472;net6.0;net8.0;net9.0 High performance Redis client, incorporating both synchronous and asynchronous usage. StackExchange.Redis StackExchange.Redis diff --git a/tests/BasicTest/BasicTest.csproj b/tests/BasicTest/BasicTest.csproj index 593d26619..3cb3dc5e4 100644 --- a/tests/BasicTest/BasicTest.csproj +++ b/tests/BasicTest/BasicTest.csproj @@ -2,11 +2,10 @@ StackExchange.Redis.BasicTest .NET Core - net472;net8.0 + net472;net8.0;net9.0 BasicTest Exe BasicTest - @@ -15,6 +14,10 @@ + + + + diff --git a/tests/BasicTest/BenchmarkBase.cs b/tests/BasicTest/BenchmarkBase.cs new file mode 100644 index 000000000..f8adcfad5 --- /dev/null +++ b/tests/BasicTest/BenchmarkBase.cs @@ -0,0 +1,539 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +#if TEST_BASELINE +using Void = BasicTest.Void; + +#else +using Resp; +using Void = Resp.Void; +#endif + +// influenced by redis-benchmark, see .md file +namespace BasicTest; + +#if TEST_BASELINE +public readonly struct Void +{ + private static readonly Void _instance; + public static ref readonly Void Instance => ref _instance; +} +#endif +public abstract class BenchmarkBase : IDisposable +{ + protected const string + _getSetKey = "key:__rand_int__", + _counterKey = "counter:__rand_int__", + _listKey = "mylist", + _setKey = "myset", + _hashKey = "myhash", + _sortedSetKey = "myzset", + _streamKey = "mystream"; + + public PipelineStrategy PipelineMode { get; } = + PipelineStrategy.Batch; // the default, for parity with how redis-benchmark works + + public enum PipelineStrategy + { + /// + /// Build a batch of operations, send them all at once. + /// + Batch, + + /// + /// Use a queue to pipeline operations - when we hit the pipeline depth, we pop one, push one, await the popped + /// + Queue, + } + + private readonly HashSet _tests = new(StringComparer.OrdinalIgnoreCase); + protected bool RunTest(string name) => _tests.Count == 0 || _tests.Contains(name); + public virtual void Dispose() { } + public int Port { get; } = 6379; + public int PipelineDepth { get; } = 1; + public bool Multiplexed { get; } + public bool SupportCancel { get; } + public bool Loop { get; } + public bool Quiet { get; } + public int ClientCount { get; } = 50; + public int OperationsPerClient { get; } + + public int TotalOperations => OperationsPerClient * ClientCount; + + protected readonly byte[] _payload; + + public BenchmarkBase(string[] args) + { + int operations = 100_000; + + string tests = ""; + for (int i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "-p" when i != args.Length - 1 && int.TryParse(args[++i], out int tmp) && tmp > 0: + Port = tmp; + break; + case "-c" when i != args.Length - 1 && int.TryParse(args[++i], out int tmp) && tmp > 0: + ClientCount = tmp; + break; + case "-n" when i != args.Length - 1 && int.TryParse(args[++i], out int tmp) && tmp > 0: + operations = tmp; + break; + case "-P" when i != args.Length - 1 && int.TryParse(args[++i], out int tmp) && tmp > 0: + PipelineDepth = tmp; + break; + case "+m": + Multiplexed = true; + break; + case "-m": + Multiplexed = false; + break; + case "+x": + SupportCancel = true; + break; + case "-c": + SupportCancel = false; + break; + case "-l": + Loop = true; + break; + case "-q": + Quiet = true; + break; + case "-t" when i != args.Length - 1: + tests = args[++i]; + break; + case "--batch": + PipelineMode = PipelineStrategy.Batch; + break; + case "--queue": + PipelineMode = PipelineStrategy.Queue; + break; + } + } + + if (!string.IsNullOrWhiteSpace(tests)) + { + foreach (var test in tests.Split(',')) + { + var t = test.Trim(); + if (!string.IsNullOrWhiteSpace(t)) _tests.Add(t); + } + } + + OperationsPerClient = operations / ClientCount; + + _payload = "abc"u8.ToArray(); + } + + public abstract Task RunAll(); + + protected static readonly Func + NoFlush = () => throw new NotSupportedException("Not a batch; cannot flush"); + + protected Task Pipeline(Func operation, Func flush) => + Pipeline(() => new ValueTask(operation()), flush); + + protected Task Pipeline(Func> operation, Func flush) => + Pipeline(() => new ValueTask(operation()), flush); + + protected async Task Pipeline(Func operation, Func flush) + { + var opsPerClient = OperationsPerClient; + int i = 0; + try + { + if (PipelineDepth <= 1) + { + for (; i < opsPerClient; i++) + { + await operation().ConfigureAwait(false); + } + } + else if (PipelineMode == PipelineStrategy.Queue) + { + var queue = new Queue(opsPerClient); + for (; i < opsPerClient; i++) + { + if (queue.Count == opsPerClient) + { + await queue.Dequeue().ConfigureAwait(false); + } + + queue.Enqueue(operation()); + } + + while (queue.Count > 0) + { + await queue.Dequeue().ConfigureAwait(false); + } + } + else if (PipelineMode == PipelineStrategy.Batch) + { + int count = 0; + if (flush is null) throw new InvalidOperationException("Flush is required for batch mode"); + var oversized = ArrayPool.Shared.Rent(PipelineDepth); + for (; i < opsPerClient; i++) + { + oversized[count++] = operation(); + if (count == PipelineDepth) + { + await flush().ConfigureAwait(false); + for (int j = 0; j < count; j++) + { + await oversized[j].ConfigureAwait(false); + } + + count = 0; + } + } + + await flush().ConfigureAwait(false); + for (int j = 0; j < count; j++) + { + await oversized[j].ConfigureAwait(false); + } + + ArrayPool.Shared.Return(oversized); + } + else + { + throw new InvalidOperationException($"Unexpected pipeline mode: {PipelineMode}"); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"{operation.Method.Name} failed after {i} operations"); + Program.WriteException(ex); + } + + return Void.Instance; + } + + protected async Task Pipeline(Func> operation, Func flush) + { + var opsPerClient = OperationsPerClient; + int i = 0; + T result = default; + try + { + if (PipelineDepth == 1) + { + for (; i < opsPerClient; i++) + { + result = await operation().ConfigureAwait(false); + } + } + else if (PipelineMode == PipelineStrategy.Queue) + { + var queue = new Queue>(opsPerClient); + for (; i < opsPerClient; i++) + { + if (queue.Count == opsPerClient) + { + _ = await queue.Dequeue().ConfigureAwait(false); + } + + queue.Enqueue(operation()); + } + + while (queue.Count > 0) + { + result = await queue.Dequeue().ConfigureAwait(false); + } + } + else if (PipelineMode == PipelineStrategy.Batch) + { + int count = 0; + if (flush is null) throw new InvalidOperationException("Flush is required for batch mode"); + var oversized = ArrayPool>.Shared.Rent(PipelineDepth); + for (; i < opsPerClient; i++) + { + oversized[count++] = operation(); + if (count == PipelineDepth) + { + await flush().ConfigureAwait(false); + for (int j = 0; j < count; j++) + { + result = await oversized[j].ConfigureAwait(false); + } + + count = 0; + } + } + + await flush().ConfigureAwait(false); + for (int j = 0; j < count; j++) + { + result = await oversized[j].ConfigureAwait(false); + } + + ArrayPool>.Shared.Return(oversized); + } + else + { + throw new InvalidOperationException($"Unexpected pipeline mode: {PipelineMode}"); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"{operation.Method.Name} failed after {i} operations"); + Program.WriteException(ex); + } + + return result; + } +} + +public abstract class BenchmarkBase(string[] args) : BenchmarkBase(args) +{ + protected virtual Task OnCleanupAsync(TClient client) => Task.CompletedTask; + + protected virtual Task InitAsync(TClient client) => Task.CompletedTask; + + protected virtual Func GetFlush(TClient client) => NoFlush; + + public async Task CleanupAsync() + { + try + { + var client = GetClient(0); + await Delete(client, _getSetKey).ConfigureAwait(false); + await Delete(client, _counterKey).ConfigureAwait(false); + await Delete(client, _listKey).ConfigureAwait(false); + await Delete(client, _setKey).ConfigureAwait(false); + await Delete(client, _hashKey).ConfigureAwait(false); + await Delete(client, _sortedSetKey).ConfigureAwait(false); + await Delete(client, _streamKey).ConfigureAwait(false); + await OnCleanupAsync(client).ConfigureAwait(false); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Cleanup: {ex.Message}"); + } + } + + public async Task InitAsync() + { + for (int i = 0; i < ClientCount; i++) + { + await InitAsync(GetClient(i)).ConfigureAwait(false); + } + } + + protected abstract TClient GetClient(int index); + protected virtual TClient WithCancellation(TClient client, CancellationToken cancellationToken) => client; + protected abstract Task Delete(TClient client, string key); + + protected abstract TClient CreateBatch(TClient client); + + protected async Task RunAsync( + string key, + Func, Task> action, + Func init = null, + string format = "") + { + string name = action.Method.Name; + + if (action.Method.GetCustomAttribute(typeof(DisplayNameAttribute)) is DisplayNameAttribute + { + DisplayName: { Length: > 0 } + } dna) + { + name = dna.DisplayName; + } + + // skip test if not needed + if (!RunTest(name)) return; + + // include additional test metadata + string description = ""; + if (action.Method.GetCustomAttribute(typeof(DescriptionAttribute)) is DescriptionAttribute + { + Description: { Length: > 0 } + } da) + { + description = $" ({da.Description})"; + } + + if (Quiet) + { + Console.Write($"{name}:"); + } + else + { + Console.Write( + $"====== {name}{description} ====== (clients: {ClientCount:#,##0}, ops: {TotalOperations:#,##0}"); + if (Multiplexed) + { + Console.Write(", mux"); + } + + if (SupportCancel) + { + Console.Write(", cancel"); + } + + if (PipelineDepth > 1) + { + Console.Write($", {PipelineMode}: {PipelineDepth:#,##0}"); + } + + Console.WriteLine(")"); + } + + try + { + if (key is not null) + { + await Delete(GetClient(0), key).ConfigureAwait(false); + } + + if (init is not null) + { + await init(GetClient(0)).ConfigureAwait(false); + } + + var pending = new Task[ClientCount]; + int index = 0; +#if DEBUG && !TEST_BASELINE + DebugCounters.Flush(); +#endif + // optionally support cancellation, applied per-test + CancellationToken cancellationToken = CancellationToken.None; + using var cts = SupportCancel ? new CancellationTokenSource(TimeSpan.FromSeconds(20)) : null; + if (SupportCancel) cancellationToken = cts!.Token; + + var watch = Stopwatch.StartNew(); + for (int i = 0; i < ClientCount; i++) + { + var client = GetClient(i); + if (PipelineMode == PipelineStrategy.Batch && PipelineDepth > 1) + { + client = CreateBatch(client); + } + + var flush = GetFlush(client); + pending[index++] = Task.Run(() => action(WithCancellation(client, cancellationToken), flush)); + } + + await Task.WhenAll(pending).ConfigureAwait(false); + watch.Stop(); + + var seconds = watch.Elapsed.TotalSeconds; + var rate = TotalOperations / seconds; + if (Quiet) + { + Console.WriteLine($"\t{rate:###,###,##0} requests per second"); + return; + } + else + { + Console.WriteLine( + $"{TotalOperations:###,###,##0} requests completed in {seconds:0.00} seconds, {rate:###,###,##0} ops/sec"); + } + + if (typeof(T) != typeof(Void) && !Quiet) + { + if (string.IsNullOrWhiteSpace(format)) + { + format = "Typical result: {0}"; + } + + T result = await pending[pending.Length - 1]; + Console.WriteLine(format, result); + } + } + catch (Exception ex) + { + if (Quiet) Console.WriteLine(); + Console.Error.WriteLine(ex.Message); + } + finally + { +#if DEBUG && !TEST_BASELINE + var counters = DebugCounters.Flush(); // flush even if not showing + if (!Quiet) + { + if (counters.WriteBytes != 0) + { + Console.Write($"Write: {FormatBytes(counters.WriteBytes)}"); + if (counters.WriteCount != 0) Console.Write($"; {counters.WriteCount:#,##0} sync"); + if (counters.AsyncWriteInlineCount != 0) + Console.Write($"; {counters.AsyncWriteInlineCount:#,##0} async-inline"); + if (counters.AsyncWriteCount != 0) Console.Write($"; {counters.AsyncWriteCount:#,##0} full-async"); + Console.WriteLine(); + } + + if (counters.ReadBytes != 0) + { + Console.Write($"Read: {FormatBytes(counters.ReadBytes)}"); + if (counters.ReadCount != 0) Console.Write($"; {counters.ReadCount:#,##0} sync"); + if (counters.AsyncReadInlineCount != 0) + Console.Write($"; {counters.AsyncReadInlineCount:#,##0} async-inline"); + if (counters.AsyncReadCount != 0) Console.Write($"; {counters.AsyncReadCount:#,##0} full-async"); + Console.WriteLine(); + } + + if (counters.DiscardFullCount + counters.DiscardPartialCount != 0) + { + Console.Write($"Discard average: {FormatBytes(counters.DiscardAverage)}"); + if (counters.DiscardFullCount != 0) Console.Write($"; {counters.DiscardFullCount} full"); + if (counters.DiscardPartialCount != 0) Console.Write($"; {counters.DiscardPartialCount} partial"); + Console.WriteLine(); + } + + if (counters.CopyOutCount != 0) + { + Console.WriteLine( + $"Copy out: {FormatBytes(counters.CopyOutBytes)}; {counters.CopyOutCount:#,##0} times"); + } + + if (counters.PipelineFullAsyncCount != 0 + | counters.PipelineSendAsyncCount != 0 + | counters.PipelineFullSyncCount != 0) + { + Console.Write("Pipelining"); + if (counters.PipelineFullSyncCount != 0) + Console.Write($"; full sync: {counters.PipelineFullSyncCount:#,##0}"); + if (counters.PipelineSendAsyncCount != 0) + Console.Write($"; send async: {counters.PipelineSendAsyncCount:#,##0}"); + if (counters.PipelineFullAsyncCount != 0) + Console.Write($"; full async: {counters.PipelineFullAsyncCount:#,##0}"); + Console.WriteLine(); + } + + if (counters.BatchWriteCount != 0) + { + Console.Write($"Batching; {counters.BatchWriteCount:#,##0} batches"); + if (counters.BatchWriteFullPageCount != 0) + Console.Write($"; {counters.BatchWriteFullPageCount:#,###,##0} full pages"); + if (counters.BatchWritePartialPageCount != 0) + Console.Write($"; {counters.BatchWritePartialPageCount:#,###,##0} partial pages"); + if (counters.BatchWriteMessageCount != 0) + Console.Write($"; {counters.BatchWriteMessageCount:#,###,##0} messages"); + Console.WriteLine(); + } + + static string FormatBytes(long bytes) + { + const long K = 1024, M = K * K, G = M * K, T = G * K; + + if (bytes < K) return $"{bytes:#,##0} B"; + if (bytes < M) return $"{bytes / (double)K:#,##0.00} KiB"; + if (bytes < G) return $"{bytes / (double)M:#,##0.00} MiB"; + if (bytes < T) return $"{bytes / (double)G:#,##0.00} GiB"; + return $"{bytes / (double)T:#,##0.00} TiB"; // I think we can stop there... + } + } +#endif + if (!Quiet) Console.WriteLine(); + } + } +} diff --git a/tests/BasicTest/CustomConfig.cs b/tests/BasicTest/CustomConfig.cs new file mode 100644 index 000000000..d062f0f1f --- /dev/null +++ b/tests/BasicTest/CustomConfig.cs @@ -0,0 +1,30 @@ +using System.Runtime.InteropServices; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Validators; + +namespace BasicTest; + +internal class CustomConfig : ManualConfig +{ + protected virtual Job Configure(Job j) + => j.WithGcMode(new GcMode { Force = true }) + // .With(InProcessToolchain.Instance) + ; + + public CustomConfig() + { + AddDiagnoser(MemoryDiagnoser.Default); + AddColumn(StatisticColumn.OperationsPerSecond); + AddValidator(JitOptimizationsValidator.FailOnError); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + AddJob(Configure(Job.Default.WithRuntime(ClrRuntime.Net472))); + } + + AddJob(Configure(Job.Default.WithRuntime(CoreRuntime.Core80))); + } +} diff --git a/tests/BasicTest/Issue898.cs b/tests/BasicTest/Issue898.cs new file mode 100644 index 000000000..00f449e16 --- /dev/null +++ b/tests/BasicTest/Issue898.cs @@ -0,0 +1,79 @@ +using System; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using StackExchange.Redis; + +namespace BasicTest; + +[Config(typeof(SlowConfig))] +public class Issue898 : IDisposable +{ + private readonly ConnectionMultiplexer mux; + private readonly IDatabase db; + + public void Dispose() + { + mux?.Dispose(); + GC.SuppressFinalize(this); + } + + public Issue898() + { + mux = ConnectionMultiplexer.Connect("127.0.0.1:6379"); + db = mux.GetDatabase(); + } + + private const int Max = 100000; + + [Benchmark(OperationsPerInvoke = Max)] + public void Load() + { + for (int i = 0; i < Max; ++i) + { + db.StringSet(i.ToString(), i); + } + } + + [Benchmark(OperationsPerInvoke = Max)] + public async Task LoadAsync() + { + for (int i = 0; i < Max; ++i) + { + await db.StringSetAsync(i.ToString(), i).ConfigureAwait(false); + } + } + + [Benchmark(OperationsPerInvoke = Max)] + public void Sample() + { + var rnd = new Random(); + + for (int i = 0; i < Max; ++i) + { + var r = rnd.Next(0, Max - 1); + + var rv = db.StringGet(r.ToString()); + if (rv != r) + { + throw new Exception($"Unexpected {rv}, expected {r}"); + } + } + } + + [Benchmark(OperationsPerInvoke = Max)] + public async Task SampleAsync() + { + var rnd = new Random(); + + for (int i = 0; i < Max; ++i) + { + var r = rnd.Next(0, Max - 1); + + var rv = await db.StringGetAsync(r.ToString()).ConfigureAwait(false); + if (rv != r) + { + throw new Exception($"Unexpected {rv}, expected {r}"); + } + } + } +} diff --git a/tests/BasicTest/NewCoreBenchmark.cs b/tests/BasicTest/NewCoreBenchmark.cs new file mode 100644 index 000000000..4faf3bde0 --- /dev/null +++ b/tests/BasicTest/NewCoreBenchmark.cs @@ -0,0 +1,338 @@ +#if !TEST_BASELINE +using System; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using Resp; +using Void = Resp.Void; + +namespace BasicTest; + +public sealed class NewCoreBenchmark : BenchmarkBase +{ + public override string ToString() => "new IO core"; + + private readonly RespConnectionPool _connectionPool; + + private readonly RespContext[] _clients; + private readonly (string Key, byte[] Value)[] _pairs; + + protected override RespContext GetClient(int index) => _clients[index]; + + protected override Task Delete(RespContext client, string key) => client.DelAsync(key).AsTask(); + + protected override RespContext WithCancellation(RespContext client, CancellationToken cancellationToken) + => client.WithCancellationToken(cancellationToken); + + protected override Task InitAsync(RespContext client) => client.PingAsync().AsTask(); + + public NewCoreBenchmark(string[] args) : base(args) + { + _clients = new RespContext[ClientCount]; + + _connectionPool = new(count: Multiplexed ? 1 : ClientCount); + _pairs = new (string, byte[])[10]; + + for (var i = 0; i < 10; i++) + { + _pairs[i] = ($"{"key:__rand_int__"}{i}", _payload); + } + + if (Multiplexed) + { + var conn = _connectionPool.GetConnection().ForPipeline(); + var ctx = conn.Context; + for (int i = 0; i < ClientCount; i++) // init all + { + _clients[i] = ctx; + } + } + else + { + for (int i = 0; i < ClientCount; i++) // init all + { + var conn = _connectionPool.GetConnection(); + if (PipelineDepth > 1) + { + conn = conn.ForPipeline(); + } + + _clients[i] = conn.Context; + } + } + } + + public override void Dispose() + { + _connectionPool.Dispose(); + foreach (var client in _clients) + { + client.Connection.Dispose(); + } + } + + protected override async Task OnCleanupAsync(RespContext client) + { + foreach (var pair in _pairs) + { + await client.DelAsync(pair.Key).ConfigureAwait(false); + } + } + + public override async Task RunAll() + { + await InitAsync().ConfigureAwait(false); + // await RunAsync(PingInline).ConfigureAwait(false); + await RunAsync(null, PingBulk).ConfigureAwait(false); + + await RunAsync(_getSetKey, Set).ConfigureAwait(false); + await RunAsync(_getSetKey, Get, GetInit).ConfigureAwait(false); + await RunAsync(_counterKey, Incr).ConfigureAwait(false); + await RunAsync(_listKey, LPush).ConfigureAwait(false); + await RunAsync(_listKey, RPush).ConfigureAwait(false); + await RunAsync(_listKey, LPop, LPopInit).ConfigureAwait(false); + await RunAsync(_listKey, RPop, LPopInit).ConfigureAwait(false); + await RunAsync(_setKey, SAdd).ConfigureAwait(false); + await RunAsync(_hashKey, HSet).ConfigureAwait(false); + await RunAsync(_setKey, SPop, SPopInit).ConfigureAwait(false); + await RunAsync(_sortedSetKey, ZAdd).ConfigureAwait(false); + await RunAsync(_sortedSetKey, ZPopMin, ZPopMinInit).ConfigureAwait(false); + await RunAsync(null, MSet).ConfigureAwait(false); + await RunAsync(_streamKey, XAdd).ConfigureAwait(false); + + // leave until last, they're slower + await RunAsync(_listKey, LRange100, LRangeInit).ConfigureAwait(false); + await RunAsync(_listKey, LRange300, LRangeInit).ConfigureAwait(false); + await RunAsync(_listKey, LRange500, LRangeInit).ConfigureAwait(false); + await RunAsync(_listKey, LRange600, LRangeInit).ConfigureAwait(false); + + await CleanupAsync().ConfigureAwait(false); + } + + protected override RespContext CreateBatch(RespContext client) => client.CreateBatch().Context; + + protected override Func GetFlush(RespContext client) + { + if (client.Connection is IBatchConnection batch) + { + return () => + { + return new(batch.FlushAsync()); + }; + } + + return base.GetFlush(client); + } + + [DisplayName("PING_INLINE")] + private Task PingInline(RespContext ctx, Func flush) => Pipeline(() => ctx.PingInlineAsync(_payload), flush); + + [DisplayName("PING_BULK")] + private Task PingBulk(RespContext ctx, Func flush) => Pipeline(() => ctx.PingAsync(_payload), flush); + + [DisplayName("INCR")] + private Task Incr(RespContext ctx, Func flush) => Pipeline(() => ctx.IncrAsync(_counterKey), flush); + + [DisplayName("GET")] + private Task Get(RespContext ctx, Func flush) => Pipeline(() => ctx.GetAsync(_getSetKey), flush); + + private Task GetInit(RespContext ctx) => ctx.SetAsync(_getSetKey, _payload).AsTask(); + + [DisplayName("SET")] + private Task Set(RespContext ctx, Func flush) => Pipeline(() => ctx.SetAsync(_getSetKey, _payload), flush); + + [DisplayName("LPUSH")] + private Task LPush(RespContext ctx, Func flush) => Pipeline(() => ctx.LPushAsync(_listKey, _payload), flush); + + [DisplayName("RPUSH")] + private Task RPush(RespContext ctx, Func flush) => Pipeline(() => ctx.RPushAsync(_listKey, _payload), flush); + + [DisplayName("LRANGE_100")] + private Task LRange100(RespContext ctx, Func flush) => Pipeline(() => ctx.LRangeAsync(_listKey, 0, 99), flush); + + [DisplayName("LRANGE_300")] + private Task LRange300(RespContext ctx, Func flush) => Pipeline(() => ctx.LRangeAsync(_listKey, 0, 299), flush); + + [DisplayName("LRANGE_500")] + private Task LRange500(RespContext ctx, Func flush) => Pipeline(() => ctx.LRangeAsync(_listKey, 0, 499), flush); + + [DisplayName("LRANGE_600")] + private Task LRange600(RespContext ctx, Func flush) => Pipeline(() => ctx.LRangeAsync(_listKey, 0, 599), flush); + + [DisplayName("LPOP")] + private Task LPop(RespContext ctx, Func flush) => Pipeline(() => ctx.LPopAsync(_listKey), flush); + + [DisplayName("RPOP")] + private Task RPop(RespContext ctx, Func flush) => Pipeline(() => ctx.RPopAsync(_listKey), flush); + + private Task LPopInit(RespContext ctx) => ctx.LPushAsync(_listKey, _payload, TotalOperations).AsTask(); + + [DisplayName("SADD")] + private Task SAdd(RespContext ctx, Func flush) => Pipeline(() => ctx.SAddAsync(_setKey, "element:__rand_int__"), flush); + + [DisplayName("HSET")] + private Task HSet(RespContext ctx, Func flush) => + Pipeline(() => ctx.HSetAsync(_hashKey, "element:__rand_int__", _payload), flush); + + [DisplayName("ZADD")] + private Task ZAdd(RespContext ctx, Func flush) => Pipeline(() => ctx.ZAddAsync(_sortedSetKey, 0, "element:__rand_int__"), flush); + + [DisplayName("ZPOPMIN")] + private Task ZPopMin(RespContext ctx, Func flush) => Pipeline(() => ctx.ZPopMinAsync(_sortedSetKey), flush); + + private async Task ZPopMinInit(RespContext ctx) + { + int ops = TotalOperations; + var rand = new Random(); + for (int i = 0; i < ops; i++) + { + await ctx.ZAddAsync(_sortedSetKey, (rand.NextDouble() * 2000) - 1000, "element:__rand_int__") + .ConfigureAwait(false); + } + } + + [DisplayName("SPOP")] + private Task SPop(RespContext ctx, Func flush) => Pipeline(() => ctx.SPopAsync(_setKey), flush); + + private async Task SPopInit(RespContext ctx) + { + int ops = TotalOperations; + for (int i = 0; i < ops; i++) + { + await ctx.SAddAsync(_setKey, "element:__rand_int__").ConfigureAwait(false); + } + } + + [DisplayName("MSET"), Description("10 keys")] + private Task MSet(RespContext ctx, Func flush) => Pipeline(() => ctx.MSetAsync(_pairs), flush); + + private Task LRangeInit(RespContext ctx) => ctx.LPushAsync(_listKey, _payload, TotalOperations).AsTask(); + + [DisplayName("XADD")] + private Task XAdd(RespContext ctx, Func flush) => + Pipeline(() => ctx.XAddAsync(_streamKey, "*", "myfield", _payload), flush); +} + +internal static partial class RedisCommands +{ + [RespCommand] + internal static partial ResponseSummary Ping(this in RespContext ctx); + + [RespCommand] + internal static partial ResponseSummary SPop(this in RespContext ctx, string key); + + [RespCommand] + internal static partial int SAdd(this in RespContext ctx, string key, string payload); + + [RespCommand] + internal static partial ResponseSummary Set(this in RespContext ctx, string key, byte[] payload); + + [RespCommand] + internal static partial int LPush(this in RespContext ctx, string key, byte[] payload); + + [RespCommand(Formatter = "LPushFormatter.Instance")] + internal static partial void LPush(this in RespContext ctx, string key, byte[] payload, int count); + + private sealed class LPushFormatter : IRespFormatter<(string Key, byte[] Payload, int Count)> + { + private LPushFormatter() { } + public static readonly LPushFormatter Instance = new(); + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (string Key, byte[] Payload, int Count) request) + { + writer.WriteCommand(command, request.Count + 1); + writer.WriteKey(request.Key); + for (int i = 0; i < request.Count; i++) + { + // duplicate for lazy bulk load + writer.WriteBulkString(request.Payload); + } + } + } + + [RespCommand] + internal static partial int RPush(this in RespContext ctx, string key, byte[] payload); + + [RespCommand] + internal static partial ResponseSummary LPop(this in RespContext ctx, string key); + + [RespCommand] + internal static partial ResponseSummary RPop(this in RespContext ctx, string key); + + [RespCommand] + internal static partial ResponseSummary LRange(this in RespContext ctx, string key, int start, int stop); + + [RespCommand] + internal static partial int HSet(this in RespContext ctx, string key, string field, byte[] payload); + + [RespCommand] + internal static partial ResponseSummary Ping(this in RespContext ctx, byte[] payload); + + [RespCommand] + internal static partial int Incr(this in RespContext ctx, string key); + + [RespCommand] + internal static partial ResponseSummary Del(this in RespContext ctx, string key); + + [RespCommand] + internal static partial ResponseSummary ZPopMin(this in RespContext ctx, string key); + + [RespCommand] + internal static partial int ZAdd(this in RespContext ctx, string key, double score, string payload); + + [RespCommand] + internal static partial ResponseSummary XAdd( + this in RespContext ctx, + string key, + string id, + string field, + byte[] value); + + [RespCommand] + internal static partial ResponseSummary Get(this in RespContext ctx, string key); + + [RespCommand(Formatter = "PairsFormatter.Instance")] // custom command formatter + internal static partial void MSet(this in RespContext ctx, (string, byte[])[] pairs); + + internal static ResponseSummary PingInline(this in RespContext ctx, byte[] payload) + => ctx.Command("ping"u8, payload, InlinePingFormatter.Instance).Wait(ResponseSummary.Parser); + + internal static ValueTask PingInlineAsync(this in global::Resp.RespContext ctx, byte[] payload) + => ctx.Command("ping"u8, payload, InlinePingFormatter.Instance) + .AsValueTask(ResponseSummary.Parser); + + private sealed class InlinePingFormatter : IRespFormatter + { + private InlinePingFormatter() { } + public static readonly InlinePingFormatter Instance = new(); + + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in byte[] request) + { + writer.WriteRaw(command); + writer.WriteRaw(" "u8); + writer.WriteRaw(request); + writer.WriteRaw("\r\n"u8); + } + } + + private sealed class PairsFormatter : IRespFormatter<(string Key, byte[] Value)[]> + { + public static readonly PairsFormatter Instance = new PairsFormatter(); + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (string Key, byte[] Value)[] request) + { + writer.WriteCommand(command, 2 * request.Length); + foreach (var pair in request) + { + writer.WriteKey(pair.Key); + writer.WriteBulkString(pair.Value); + } + } + } +} +#endif diff --git a/tests/BasicTest/OldCoreBenchmark.cs b/tests/BasicTest/OldCoreBenchmark.cs new file mode 100644 index 000000000..d6e15c2cc --- /dev/null +++ b/tests/BasicTest/OldCoreBenchmark.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; +using StackExchange.Redis; + +namespace BasicTest; + +public class OldCoreBenchmark : BenchmarkBase +{ + public override string ToString() => "legacy SE.Redis"; + + private readonly IConnectionMultiplexer _connectionMultiplexer; + private readonly IDatabase _client; + private readonly KeyValuePair[] _pairs; + + public OldCoreBenchmark(string[] args) : base(args) + { + _connectionMultiplexer = ConnectionMultiplexer.Connect($"127.0.0.1:{Port}"); + _client = _connectionMultiplexer.GetDatabase(); + _pairs = new KeyValuePair[10]; + + for (var i = 0; i < 10; i++) + { + _pairs[i] = new($"{"key:__rand_int__"}{i}", _payload); + } + } + + protected override async Task OnCleanupAsync(IDatabaseAsync client) + { + foreach (var pair in _pairs) + { + await client.KeyDeleteAsync(pair.Key); + } + } + + protected override Task InitAsync(IDatabaseAsync client) => client.PingAsync(); + + public override void Dispose() + { + _connectionMultiplexer.Dispose(); + } + + protected override IDatabaseAsync GetClient(int index) => _client; + protected override Task Delete(IDatabaseAsync client, string key) => client.KeyDeleteAsync(key); + + public override async Task RunAll() + { + await InitAsync().ConfigureAwait(false); + // await RunAsync(PingInline).ConfigureAwait(false); + await RunAsync(null, PingBulk).ConfigureAwait(false); + + await RunAsync(_getSetKey, Set).ConfigureAwait(false); + await RunAsync(_getSetKey, Get, GetInit).ConfigureAwait(false); + await RunAsync(_counterKey, Incr).ConfigureAwait(false); + await RunAsync(_listKey, LPush).ConfigureAwait(false); + await RunAsync(_listKey, RPush).ConfigureAwait(false); + await RunAsync(_listKey, LPop, LPopInit).ConfigureAwait(false); + await RunAsync(_listKey, RPop, LPopInit).ConfigureAwait(false); + await RunAsync(_setKey, SAdd).ConfigureAwait(false); + await RunAsync(_hashKey, HSet).ConfigureAwait(false); + await RunAsync(_setKey, SPop, SPopInit).ConfigureAwait(false); + await RunAsync(_sortedSetKey, ZAdd).ConfigureAwait(false); + await RunAsync(_sortedSetKey, ZPopMin, ZPopMinInit).ConfigureAwait(false); + await RunAsync(null, MSet).ConfigureAwait(false); + await RunAsync(_streamKey, XAdd).ConfigureAwait(false); + + // leave until last, they're slower + await RunAsync(_listKey, LRange100, LRangeInit).ConfigureAwait(false); + await RunAsync(_listKey, LRange300, LRangeInit).ConfigureAwait(false); + await RunAsync(_listKey, LRange500, LRangeInit).ConfigureAwait(false); + await RunAsync(_listKey, LRange600, LRangeInit).ConfigureAwait(false); + + await CleanupAsync().ConfigureAwait(false); + } + + protected override IDatabaseAsync CreateBatch(IDatabaseAsync client) => ((IDatabase)client).CreateBatch(); + + protected override Func GetFlush(IDatabaseAsync client) + { + if (client is IBatch batch) + { + return () => + { + batch.Execute(); + return default; + }; + } + return GetFlush(client); + } + + [DisplayName("GET")] + private Task Get(IDatabaseAsync client, Func flush) => Pipeline(() => GetAndMeasureString(client), flush); + + private async Task GetAndMeasureString(IDatabaseAsync client) + { + using var lease = await client.StringGetLeaseAsync(_getSetKey).ConfigureAwait(false); + return lease?.Length ?? -1; + } + + [DisplayName("SET")] + private Task Set(IDatabaseAsync client, Func flush) => + Pipeline(() => client.StringSetAsync(_getSetKey, _payload), flush); + + private Task GetInit(IDatabaseAsync client) => client.StringSetAsync(_getSetKey, _payload); + + private Task PingInline(IDatabaseAsync client, Func flush) => Pipeline(() => client.PingAsync(), flush); + + [DisplayName("PING_BULK")] + private Task PingBulk(IDatabaseAsync client, Func flush) => Pipeline(() => client.PingAsync(), flush); + + [DisplayName("INCR")] + private Task Incr(IDatabaseAsync client, Func flush) => + Pipeline(() => client.StringIncrementAsync(_counterKey), flush); + + [DisplayName("HSET")] + private Task HSet(IDatabaseAsync client, Func flush) => + Pipeline(() => client.HashSetAsync(_hashKey, "element:__rand_int__", _payload), flush); + + [DisplayName("SADD")] + private Task SAdd(IDatabaseAsync client, Func flush) => + Pipeline(() => client.SetAddAsync(_setKey, "element:__rand_int__"), flush); + + [DisplayName("LPUSH")] + private Task LPush(IDatabaseAsync client, Func flush) => + Pipeline(() => client.ListLeftPushAsync(_listKey, _payload), flush); + + [DisplayName("RPUSH")] + private Task RPush(IDatabaseAsync client, Func flush) => + Pipeline(() => client.ListRightPushAsync(_listKey, _payload), flush); + + [DisplayName("LPOP")] + private Task LPop(IDatabaseAsync client, Func flush) => + Pipeline(() => client.ListLeftPopAsync(_listKey), flush); + + [DisplayName("RPOP")] + private Task RPop(IDatabaseAsync client, Func flush) => + Pipeline(() => client.ListRightPopAsync(_listKey), flush); + + private Task LPopInit(IDatabaseAsync client) => client.ListLeftPushAsync(_listKey, _payload); + + [DisplayName("SPOP")] + private Task SPop(IDatabaseAsync client, Func flush) => Pipeline(() => client.SetPopAsync(_setKey), flush); + private Task SPopInit(IDatabaseAsync client) => client.SetAddAsync(_setKey, "element:__rand_int__"); + + [DisplayName("ZADD")] + private Task ZAdd(IDatabaseAsync client, Func flush) => + Pipeline(() => client.SortedSetAddAsync(_sortedSetKey, "element:__rand_int__", 0), flush); + + [DisplayName("ZPOPMIN")] + private Task ZPopMin(IDatabaseAsync client, Func flush) => + Pipeline(() => CountAsync(client.SortedSetPopAsync(_sortedSetKey, 1)), flush); + + private Task ZPopMinInit(IDatabaseAsync client) => client.SortedSetAddAsync(_sortedSetKey, "element:__rand_int__", 0); + + [DisplayName("MSET")] + private Task MSet(IDatabaseAsync client, Func flush) => Pipeline(() => client.StringSetAsync(_pairs), flush); + + [DisplayName("XADD")] + private Task XAdd(IDatabaseAsync client, Func flush) => + Pipeline(() => client.StreamAddAsync(_streamKey, "myfield", _payload), flush); + + [DisplayName("LRANGE_100")] + private Task LRange100(IDatabaseAsync client, Func flush) => + Pipeline(() => CountAsync(client.ListRangeAsync(_listKey, 0, 99)), flush); + + [DisplayName("LRANGE_300")] + private Task LRange300(IDatabaseAsync client, Func flush) => + Pipeline(() => CountAsync(client.ListRangeAsync(_listKey, 0, 299)), flush); + + [DisplayName("LRANGE_500")] + private Task LRange500(IDatabaseAsync client, Func flush) => + Pipeline(() => CountAsync(client.ListRangeAsync(_listKey, 0, 499)), flush); + + [DisplayName("LRANGE_600")] + private Task LRange600(IDatabaseAsync client, Func flush) => + Pipeline(() => CountAsync(client.ListRangeAsync(_listKey, 0, 599)), flush); + + private static Task CountAsync(Task task) => + task.ContinueWith(t => t.Result.Length, TaskContinuationOptions.ExecuteSynchronously); + + private async Task LRangeInit(IDatabaseAsync client) + { + var ops = TotalOperations; + for (int i = 0; i < ops; i++) + { + await client.ListLeftPushAsync(_listKey, _payload); + } + } +} diff --git a/tests/BasicTest/Program.cs b/tests/BasicTest/Program.cs index faff1b7d7..06017f476 100644 --- a/tests/BasicTest/Program.cs +++ b/tests/BasicTest/Program.cs @@ -1,276 +1,87 @@ using System; -using System.Reflection; +using System.Collections.Generic; using System.Threading.Tasks; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Columns; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Diagnosers; -using BenchmarkDotNet.Environments; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Running; -using BenchmarkDotNet.Validators; -using StackExchange.Redis; +#if !TEST_BASELINE +using Resp; +using RESPite; +using RESPite.Redis; +#endif namespace BasicTest { internal static class Program { - private static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); - } - internal class CustomConfig : ManualConfig - { - protected virtual Job Configure(Job j) - => j.WithGcMode(new GcMode { Force = true }) - // .With(InProcessToolchain.Instance) - ; - - public CustomConfig() - { - AddDiagnoser(MemoryDiagnoser.Default); - AddColumn(StatisticColumn.OperationsPerSecond); - AddValidator(JitOptimizationsValidator.FailOnError); - - AddJob(Configure(Job.Default.WithRuntime(ClrRuntime.Net472))); - AddJob(Configure(Job.Default.WithRuntime(CoreRuntime.Core50))); - } - } - internal class SlowConfig : CustomConfig - { - protected override Job Configure(Job j) - => j.WithLaunchCount(1) - .WithWarmupCount(1) - .WithIterationCount(5); - } - - [Config(typeof(CustomConfig))] - public class RedisBenchmarks : IDisposable - { - private SocketManager mgr; - private ConnectionMultiplexer connection; - private IDatabase db; - - [GlobalSetup] - public void Setup() - { - // Pipelines.Sockets.Unofficial.SocketConnection.AssertDependencies(); - var options = ConfigurationOptions.Parse("127.0.0.1:6379"); - connection = ConnectionMultiplexer.Connect(options); - db = connection.GetDatabase(3); - - db.KeyDelete(GeoKey); - db.GeoAdd(GeoKey, 13.361389, 38.115556, "Palermo "); - db.GeoAdd(GeoKey, 15.087269, 37.502669, "Catania"); - - db.KeyDelete(HashKey); - for (int i = 0; i < 1000; i++) - { - db.HashSet(HashKey, i, i); - } - } - - private static readonly RedisKey GeoKey = "GeoTest", IncrByKey = "counter", StringKey = "string", HashKey = "hash"; - void IDisposable.Dispose() - { - mgr?.Dispose(); - connection?.Dispose(); - mgr = null; - db = null; - connection = null; - GC.SuppressFinalize(this); - } - - private const int COUNT = 50; - - /// - /// Run INCRBY lots of times. - /// - // [Benchmark(Description = "INCRBY/s", OperationsPerInvoke = COUNT)] - public int ExecuteIncrBy() - { - var rand = new Random(12345); - - db.KeyDelete(IncrByKey, CommandFlags.FireAndForget); - int expected = 0; - for (int i = 0; i < COUNT; i++) - { - int x = rand.Next(50); - expected += x; - db.StringIncrement(IncrByKey, x, CommandFlags.FireAndForget); - } - int actual = (int)db.StringGet(IncrByKey); - if (actual != expected) throw new InvalidOperationException($"expected: {expected}, actual: {actual}"); - return actual; - } - - /// - /// Run INCRBY lots of times. - /// - // [Benchmark(Description = "INCRBY/a", OperationsPerInvoke = COUNT)] - public async Task ExecuteIncrByAsync() - { - var rand = new Random(12345); - - db.KeyDelete(IncrByKey, CommandFlags.FireAndForget); - int expected = 0; - for (int i = 0; i < COUNT; i++) - { - int x = rand.Next(50); - expected += x; - await db.StringIncrementAsync(IncrByKey, x, CommandFlags.FireAndForget).ConfigureAwait(false); - } - int actual = (int)await db.StringGetAsync(IncrByKey).ConfigureAwait(false); - if (actual != expected) throw new InvalidOperationException($"expected: {expected}, actual: {actual}"); - return actual; - } - - /// - /// Run GEORADIUS lots of times. - /// - // [Benchmark(Description = "GEORADIUS/s", OperationsPerInvoke = COUNT)] - public int ExecuteGeoRadius() + private static async Task Main(string[] args) { - int total = 0; - for (int i = 0; i < COUNT; i++) + try { - var results = db.GeoRadius(GeoKey, 15, 37, 200, GeoUnit.Kilometers, options: GeoRadiusOptions.WithCoordinates | GeoRadiusOptions.WithDistance | GeoRadiusOptions.WithGeoHash); - total += results.Length; - } - return total; - } - - /// - /// Run GEORADIUS lots of times. - /// - // [Benchmark(Description = "GEORADIUS/a", OperationsPerInvoke = COUNT)] - public async Task ExecuteGeoRadiusAsync() - { - int total = 0; - for (int i = 0; i < COUNT; i++) - { - var results = await db.GeoRadiusAsync(GeoKey, 15, 37, 200, GeoUnit.Kilometers, options: GeoRadiusOptions.WithCoordinates | GeoRadiusOptions.WithDistance | GeoRadiusOptions.WithGeoHash).ConfigureAwait(false); - total += results.Length; - } - return total; - } + List benchmarks = []; +#if TEST_BASELINE + benchmarks.Add(new OldCoreBenchmark(args)); +#else + foreach (var arg in args) + { + switch (arg) + { + case "--old": + benchmarks.Add(new OldCoreBenchmark(args)); + break; + + case "--new": + benchmarks.Add(new NewCoreBenchmark(args)); + break; + } + } - /// - /// Run StringSet lots of times. - /// - [Benchmark(Description = "StringSet/s", OperationsPerInvoke = COUNT)] - public void StringSet() - { - for (int i = 0; i < COUNT; i++) - { - db.StringSet(StringKey, "hey"); - } - } + if (benchmarks.Count == 0) + { + benchmarks.Add(new NewCoreBenchmark(args)); + } +#endif - /// - /// Run StringGet lots of times. - /// - [Benchmark(Description = "StringGet/s", OperationsPerInvoke = COUNT)] - public void StringGet() - { - for (int i = 0; i < COUNT; i++) - { - db.StringGet(StringKey); - } - } + do + { + foreach (var bench in benchmarks) + { + if (benchmarks.Count > 1) + { + Console.WriteLine($"### {bench} ###"); + } + + await bench.RunAll().ConfigureAwait(false); + } + } + // ReSharper disable once LoopVariableIsNeverChangedInsideLoop + while (benchmarks[0].Loop); - /// - /// Run HashGetAll lots of times. - /// - [Benchmark(Description = "HashGetAll F+F/s", OperationsPerInvoke = COUNT)] - public void HashGetAll_FAF() - { - for (int i = 0; i < COUNT; i++) - { - db.HashGetAll(HashKey, CommandFlags.FireAndForget); - db.Ping(); // to wait for response + return 0; } - } - - /// - /// Run HashGetAll lots of times. - /// - [Benchmark(Description = "HashGetAll F+F/a", OperationsPerInvoke = COUNT)] - - public async Task HashGetAllAsync_FAF() - { - for (int i = 0; i < COUNT; i++) + catch (Exception ex) { - await db.HashGetAllAsync(HashKey, CommandFlags.FireAndForget); - await db.PingAsync(); // to wait for response + WriteException(ex); + return -1; } } - } - - [Config(typeof(SlowConfig))] - public class Issue898 : IDisposable - { - private readonly ConnectionMultiplexer mux; - private readonly IDatabase db; - - public void Dispose() - { - mux?.Dispose(); - GC.SuppressFinalize(this); - } - public Issue898() - { - mux = ConnectionMultiplexer.Connect("127.0.0.1:6379"); - db = mux.GetDatabase(); - } - private const int Max = 100000; - [Benchmark(OperationsPerInvoke = Max)] - public void Load() - { - for (int i = 0; i < Max; ++i) - { - db.StringSet(i.ToString(), i); - } - } - [Benchmark(OperationsPerInvoke = Max)] - public async Task LoadAsync() - { - for (int i = 0; i < Max; ++i) - { - await db.StringSetAsync(i.ToString(), i).ConfigureAwait(false); - } - } - [Benchmark(OperationsPerInvoke = Max)] - public void Sample() + internal static void WriteException(Exception ex) { - var rnd = new Random(); - - for (int i = 0; i < Max; ++i) + while (ex is not null) { - var r = rnd.Next(0, Max - 1); - - var rv = db.StringGet(r.ToString()); - if (rv != r) + Console.Error.WriteLine(); + Console.Error.WriteLine($"{ex.GetType().Name}: {ex.Message}"); + Console.Error.WriteLine($"\t{ex.StackTrace}"); + var data = ex.Data; + if (data is not null) { - throw new Exception($"Unexpected {rv}, expected {r}"); + foreach (var key in data.Keys) + { + Console.Error.WriteLine($"\t{key}: {data[key]}"); + } } - } - } - [Benchmark(OperationsPerInvoke = Max)] - public async Task SampleAsync() - { - var rnd = new Random(); - - for (int i = 0; i < Max; ++i) - { - var r = rnd.Next(0, Max - 1); - - var rv = await db.StringGetAsync(r.ToString()).ConfigureAwait(false); - if (rv != r) - { - throw new Exception($"Unexpected {rv}, expected {r}"); - } + ex = ex.InnerException; } } + // private static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); } } diff --git a/tests/BasicTest/RedisBenchmarks.cs b/tests/BasicTest/RedisBenchmarks.cs new file mode 100644 index 000000000..9df2c5a56 --- /dev/null +++ b/tests/BasicTest/RedisBenchmarks.cs @@ -0,0 +1,460 @@ +using System; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +#if !TEST_BASELINE +using Resp; +using RESPite.Redis; +#if !PREVIEW_LANGVER +using RESPite.Redis.Alt; // needed for AsStrings() etc +#endif +#endif +using StackExchange.Redis; + +namespace BasicTest; + +[Config(typeof(CustomConfig))] +public class RedisBenchmarks : IDisposable +{ + private SocketManager mgr; + private ConnectionMultiplexer connection; + private IDatabase db; +#if !TEST_BASELINE + private Resp.RespConnectionPool pool, customPool; +#endif + + [GlobalSetup] + public void Setup() + { + // Pipelines.Sockets.Unofficial.SocketConnection.AssertDependencies(); +#if !TEST_BASELINE + pool = new(); +#pragma warning disable CS0618 // Type or member is obsolete + customPool = new() { UseCustomNetworkStream = true }; +#pragma warning restore CS0618 // Type or member is obsolete +#endif + // var options = ConfigurationOptions.Parse("127.0.0.1:6379"); + // connection = ConnectionMultiplexer.Connect(options); + // db = connection.GetDatabase(3); + // + // db.KeyDelete(GeoKey); + // db.KeyDelete(StringKey_K); + // db.StringSet(StringKey_K, StringValue_S); + // db.GeoAdd(GeoKey, 13.361389, 38.115556, "Palermo "); + // db.GeoAdd(GeoKey, 15.087269, 37.502669, "Catania"); + // + // db.KeyDelete(HashKey); + // for (int i = 0; i < 1000; i++) + // { + // db.HashSet(HashKey, i, i); + // } + } + + public const string StringKey_S = "string", StringValue_S = "some suitably non-trivial value"; + + public static readonly RedisKey GeoKey = "GeoTest", + IncrByKey = "counter", + StringKey_K = StringKey_S, + HashKey = "hash"; + + public static readonly RedisValue StringValue_V = StringValue_S; + + void IDisposable.Dispose() + { +#if !TEST_BASELINE + pool?.Dispose(); + customPool?.Dispose(); +#endif + mgr?.Dispose(); + connection?.Dispose(); + mgr = null; + db = null; + connection = null; + GC.SuppressFinalize(this); + } + + public const int OperationsPerInvoke = 128; + + /// + /// Run INCRBY lots of times. + /// + // [Benchmark(Description = "INCRBY/s", OperationsPerInvoke = COUNT)] + public int ExecuteIncrBy() + { + var rand = new Random(12345); + + db.KeyDelete(IncrByKey, CommandFlags.FireAndForget); + int expected = 0; + for (int i = 0; i < OperationsPerInvoke; i++) + { + int x = rand.Next(50); + expected += x; + db.StringIncrement(IncrByKey, x, CommandFlags.FireAndForget); + } + + int actual = (int)db.StringGet(IncrByKey); + if (actual != expected) throw new InvalidOperationException($"expected: {expected}, actual: {actual}"); + return actual; + } + + /// + /// Run INCRBY lots of times. + /// + // [Benchmark(Description = "INCRBY/a", OperationsPerInvoke = COUNT)] + public async Task ExecuteIncrByAsync() + { + var rand = new Random(12345); + + db.KeyDelete(IncrByKey, CommandFlags.FireAndForget); + int expected = 0; + for (int i = 0; i < OperationsPerInvoke; i++) + { + int x = rand.Next(50); + expected += x; + await db.StringIncrementAsync(IncrByKey, x, CommandFlags.FireAndForget).ConfigureAwait(false); + } + + int actual = (int)await db.StringGetAsync(IncrByKey).ConfigureAwait(false); + if (actual != expected) throw new InvalidOperationException($"expected: {expected}, actual: {actual}"); + return actual; + } + + /// + /// Run GEORADIUS lots of times. + /// + // [Benchmark(Description = "GEORADIUS/s", OperationsPerInvoke = COUNT)] + public int ExecuteGeoRadius() + { + int total = 0; + const GeoRadiusOptions options = GeoRadiusOptions.WithCoordinates | GeoRadiusOptions.WithDistance | + GeoRadiusOptions.WithGeoHash; + for (int i = 0; i < OperationsPerInvoke; i++) + { + var results = db.GeoRadius( + GeoKey, + 15, + 37, + 200, + GeoUnit.Kilometers, + options: options); + total += results.Length; + } + + return total; + } + + /// + /// Run GEORADIUS lots of times. + /// + // [Benchmark(Description = "GEORADIUS/a", OperationsPerInvoke = COUNT)] + public async Task ExecuteGeoRadiusAsync() + { + var options = GeoRadiusOptions.WithCoordinates | GeoRadiusOptions.WithDistance | + GeoRadiusOptions.WithGeoHash; + int total = 0; + for (int i = 0; i < OperationsPerInvoke; i++) + { + var results = await db.GeoRadiusAsync( + GeoKey, + 15, + 37, + 200, + GeoUnit.Kilometers, + options: options) + .ConfigureAwait(false); + total += results.Length; + } + + return total; + } + + /// + /// Run StringSet lots of times. + /// + // [Benchmark(Description = "StringSet/s", OperationsPerInvoke = COUNT)] + public void StringSet() + { + for (int i = 0; i < OperationsPerInvoke; i++) + { + db.StringSet(StringKey_K, StringValue_V); + } + } + + /// + /// Run StringGet lots of times. + /// + // [Benchmark(Description = "StringGet/s", OperationsPerInvoke = COUNT)] + public void StringGet() + { + for (int i = 0; i < OperationsPerInvoke; i++) + { + db.StringGet(StringKey_K); + } + } +#if !TEST_BASELINE + /// + /// Run StringSet lots of times. + /// + // [Benchmark(Description = "C StringSet/s", OperationsPerInvoke = COUNT)] + public void StringSet_Core() + { + using var conn = pool.GetConnection(); +#if PREVIEW_LANGVER + ref readonly RedisStrings s = ref conn.Context.Strings; +#else + var s = conn.Context.AsStrings(); +#endif + for (int i = 0; i < OperationsPerInvoke; i++) + { + s.Set(StringKey_S, StringValue_S); + } + } + + /// + /// Run StringGet lots of times. + /// + // [Benchmark(Description = "C StringGet/s", OperationsPerInvoke = COUNT)] + public void StringGet_Core() + { + using var conn = pool.GetConnection(); +#if PREVIEW_LANGVER + ref readonly RedisStrings s = ref conn.Context.Strings; +#else + var s = conn.Context.AsStrings(); +#endif + for (int i = 0; i < OperationsPerInvoke; i++) + { + s.Get(StringKey_S); + } + } + + /// + /// Run StringSet lots of times. + /// + // [Benchmark(Description = "PC StringSet/s", OperationsPerInvoke = COUNT)] + public void StringSet_Pipelined_Core() + { + using var conn = pool.GetConnection().ForPipeline(); +#if PREVIEW_LANGVER + ref readonly RedisStrings s = ref conn.Context.Strings; +#else + var s = conn.Context.AsStrings(); +#endif + for (int i = 0; i < OperationsPerInvoke; i++) + { + s.Set(StringKey_S, StringValue_S); + } + } + + /// + /// Run StringSet lots of times. + /// + // [Benchmark(Description = "PCA StringSet/s", OperationsPerInvoke = COUNT)] + public async Task StringSet_Pipelined_Core_Async() + { + using var conn = pool.GetConnection().ForPipeline(); + var ctx = conn.Context; + for (int i = 0; i < OperationsPerInvoke; i++) + { +#if PREVIEW_LANGVER + await ctx.Strings.SetAsync(StringKey_S, StringValue_S); +#else + await ctx.AsStrings().SetAsync(StringKey_S, StringValue_S); +#endif + } + } + + /// + /// Run StringGet lots of times. + /// + // [Benchmark(Description = "PC StringGet/s", OperationsPerInvoke = COUNT)] + public void StringGet_Pipelined_Core() + { + using var conn = pool.GetConnection().ForPipeline(); +#if PREVIEW_LANGVER + ref readonly RedisStrings s = ref conn.Context.Strings; +#else + var s = conn.Context.AsStrings(); +#endif + for (int i = 0; i < OperationsPerInvoke; i++) + { + s.Get(StringKey_S); + } + } + + /// + /// Run StringGet lots of times. + /// + // [Benchmark(Description = "PCA StringGet/s", OperationsPerInvoke = COUNT)] + public async Task StringGet_Pipelined_Core_Async() + { + using var conn = pool.GetConnection().ForPipeline(); + var ctx = conn.Context; + for (int i = 0; i < OperationsPerInvoke; i++) + { +#if PREVIEW_LANGVER + await ctx.Strings.GetAsync(StringKey_S); +#else + await ctx.AsStrings().GetAsync(StringKey_S); +#endif + } + } +#endif + + /// + /// Run HashGetAll lots of times. + /// + // [Benchmark(Description = "HashGetAll F+F/s", OperationsPerInvoke = COUNT)] + public void HashGetAll_FAF() + { + for (int i = 0; i < OperationsPerInvoke; i++) + { + db.HashGetAll(HashKey, CommandFlags.FireAndForget); + db.Ping(); // to wait for response + } + } + + /// + /// Run HashGetAll lots of times. + /// + // [Benchmark(Description = "HashGetAll F+F/a", OperationsPerInvoke = COUNT)] + public async Task HashGetAllAsync_FAF() + { + for (int i = 0; i < OperationsPerInvoke; i++) + { + await db.HashGetAllAsync(HashKey, CommandFlags.FireAndForget); + await db.PingAsync(); // to wait for response + } + } + + /// + /// Run incr lots of times. + /// + // [Benchmark(Description = "old incr", OperationsPerInvoke = OperationsPerInvoke)] + public int IncrBy_Old() + { + RedisValue value = 0; + db.StringSet(StringKey_K, value); + for (int i = 0; i < OperationsPerInvoke; i++) + { + value = db.StringIncrement(StringKey_K); + } + + return (int)value; + } + +#if !TEST_BASELINE + /// + /// Run incr lots of times. + /// + [Benchmark(Description = "new incr /p", OperationsPerInvoke = OperationsPerInvoke)] + public int IncrBy_New_Pipelined() + { + using var conn = pool.GetConnection().ForPipeline(); +#if PREVIEW_LANGVER + ref readonly RedisStrings s = ref conn.Context.Strings; +#else + var s = conn.Context.AsStrings(); +#endif + int value = 0; + s.Set(StringKey_S, value); + for (int i = 0; i < OperationsPerInvoke; i++) + { + value = s.Incr(StringKey_K); + } + + return value; + } + + /// + /// Run incr lots of times. + /// + [Benchmark(Description = "new incr /p/a", OperationsPerInvoke = OperationsPerInvoke)] + public async Task IncrBy_New_Pipelined_Async() + { + using var conn = pool.GetConnection().ForPipeline(); + var ctx = conn.Context; + int value = 0; +#if PREVIEW_LANGVER + await ctx.Strings.SetAsync(StringKey_S, value); +#else + await ctx.AsStrings().SetAsync(StringKey_S, value); +#endif + for (int i = 0; i < OperationsPerInvoke; i++) + { +#if PREVIEW_LANGVER + value = await ctx.Strings.IncrAsync(StringKey_K); +#else + value = await ctx.AsStrings().IncrAsync(StringKey_K); +#endif + } + + return value; + } + + /// + /// Run incr lots of times. + /// + [Benchmark(Description = "new incr", OperationsPerInvoke = OperationsPerInvoke)] + public int IncrBy_New() + { + using var conn = pool.GetConnection(); +#if PREVIEW_LANGVER + ref readonly RedisStrings s = ref conn.Context.Strings; +#else + var s = conn.Context.AsStrings(); +#endif + int value = 0; + s.Set(StringKey_S, value); + for (int i = 0; i < OperationsPerInvoke; i++) + { + value = s.Incr(StringKey_K); + } + + return value; + } + + /// + /// Run incr lots of times. + /// + // [Benchmark(Description = "new incr /pc", OperationsPerInvoke = OperationsPerInvoke)] + public int IncrBy_New_Pipelined_Custom() + { + using var conn = customPool.GetConnection().ForPipeline(); +#if PREVIEW_LANGVER + ref readonly RedisStrings s = ref conn.Context.Strings; +#else + var s = conn.Context.AsStrings(); +#endif + int value = 0; + s.Set(StringKey_S, value); + for (int i = 0; i < OperationsPerInvoke; i++) + { + value = s.Incr(StringKey_K); + } + + return value; + } + + /// + /// Run incr lots of times. + /// + // [Benchmark(Description = "new incr /c", OperationsPerInvoke = OperationsPerInvoke)] + public int IncrBy_New_Custom() + { + using var conn = customPool.GetConnection(); +#if PREVIEW_LANGVER + ref readonly RedisStrings s = ref conn.Context.Strings; +#else + var s = conn.Context.AsStrings(); +#endif + int value = 0; + s.Set(StringKey_S, value); + for (int i = 0; i < OperationsPerInvoke; i++) + { + value = s.Incr(StringKey_K); + } + + return value; + } +#endif +} diff --git a/tests/BasicTest/RespBenchmark.md b/tests/BasicTest/RespBenchmark.md new file mode 100644 index 000000000..b579d84de --- /dev/null +++ b/tests/BasicTest/RespBenchmark.md @@ -0,0 +1,352 @@ +# Influenced by redis-benchmark, which has typical output (with the default config) as below. + +Keys used (by default): + +- `key:__rand_int__` +- `counter:__rand_int__` +- `mylist` + +====== PING_INLINE ====== +100000 requests completed in 2.45 seconds +50 parallel clients +3 bytes payload +keep alive: 1 + +98.22% <= 1 milliseconds +99.88% <= 2 milliseconds +99.93% <= 3 milliseconds +99.99% <= 4 milliseconds +100.00% <= 5 milliseconds +100.00% <= 5 milliseconds +40849.68 requests per second + +====== PING_BULK ====== +100000 requests completed in 2.45 seconds +50 parallel clients +3 bytes payload +keep alive: 1 + +97.27% <= 1 milliseconds +99.86% <= 2 milliseconds +99.92% <= 3 milliseconds +99.94% <= 4 milliseconds +99.95% <= 23 milliseconds +99.96% <= 24 milliseconds +99.98% <= 25 milliseconds +100.00% <= 25 milliseconds +40866.37 requests per second + +====== SET ====== +100000 requests completed in 2.46 seconds +50 parallel clients +3 bytes payload +keep alive: 1 + +96.99% <= 1 milliseconds +99.47% <= 2 milliseconds +99.71% <= 3 milliseconds +99.86% <= 4 milliseconds +99.87% <= 9 milliseconds +99.88% <= 10 milliseconds +99.92% <= 11 milliseconds +99.93% <= 12 milliseconds +99.94% <= 13 milliseconds +99.96% <= 14 milliseconds +99.97% <= 15 milliseconds +99.97% <= 16 milliseconds +100.00% <= 27 milliseconds +40650.41 requests per second + +====== GET ====== +100000 requests completed in 3.00 seconds +50 parallel clients +3 bytes payload +keep alive: 1 + +90.56% <= 1 milliseconds +98.90% <= 2 milliseconds +99.46% <= 3 milliseconds +99.61% <= 4 milliseconds +99.70% <= 5 milliseconds +99.73% <= 6 milliseconds +99.75% <= 7 milliseconds +99.75% <= 9 milliseconds +99.77% <= 10 milliseconds +99.79% <= 12 milliseconds +99.80% <= 14 milliseconds +99.80% <= 15 milliseconds +99.83% <= 16 milliseconds +99.90% <= 17 milliseconds +99.93% <= 18 milliseconds +99.96% <= 19 milliseconds +99.98% <= 20 milliseconds +99.98% <= 22 milliseconds +99.98% <= 30 milliseconds +99.99% <= 31 milliseconds +100.00% <= 31 milliseconds +33377.84 requests per second + +====== INCR ====== +100000 requests completed in 2.94 seconds +50 parallel clients +3 bytes payload +keep alive: 1 + +93.21% <= 1 milliseconds +99.21% <= 2 milliseconds +99.70% <= 3 milliseconds +99.81% <= 4 milliseconds +99.86% <= 5 milliseconds +99.89% <= 6 milliseconds +99.93% <= 7 milliseconds +99.94% <= 8 milliseconds +99.96% <= 11 milliseconds +99.96% <= 12 milliseconds +99.96% <= 13 milliseconds +99.97% <= 14 milliseconds +99.97% <= 24 milliseconds +100.00% <= 24 milliseconds +34048.35 requests per second + +====== LPUSH ====== +100000 requests completed in 2.98 seconds +50 parallel clients +3 bytes payload +keep alive: 1 + +92.58% <= 1 milliseconds +99.21% <= 2 milliseconds +99.57% <= 3 milliseconds +99.71% <= 4 milliseconds +99.82% <= 5 milliseconds +99.85% <= 6 milliseconds +99.85% <= 7 milliseconds +99.88% <= 9 milliseconds +99.93% <= 10 milliseconds +99.93% <= 13 milliseconds +99.93% <= 14 milliseconds +99.95% <= 16 milliseconds +99.95% <= 31 milliseconds +99.99% <= 32 milliseconds +100.00% <= 32 milliseconds +33512.07 requests per second + +====== LPOP ====== +100000 requests completed in 2.91 seconds +50 parallel clients +3 bytes payload +keep alive: 1 + +92.81% <= 1 milliseconds +99.33% <= 2 milliseconds +99.89% <= 3 milliseconds +99.94% <= 4 milliseconds +99.96% <= 5 milliseconds +99.97% <= 15 milliseconds +99.98% <= 16 milliseconds +100.00% <= 17 milliseconds +34317.09 requests per second + +====== SADD ====== +100000 requests completed in 2.87 seconds +50 parallel clients +3 bytes payload +keep alive: 1 + +94.26% <= 1 milliseconds +99.58% <= 2 milliseconds +99.87% <= 3 milliseconds +99.93% <= 4 milliseconds +99.98% <= 17 milliseconds +99.98% <= 18 milliseconds +100.00% <= 19 milliseconds +34855.35 requests per second + +====== SPOP ====== +100000 requests completed in 2.99 seconds +50 parallel clients +3 bytes payload +keep alive: 1 + +91.00% <= 1 milliseconds +99.30% <= 2 milliseconds +99.69% <= 3 milliseconds +99.80% <= 4 milliseconds +99.85% <= 5 milliseconds +99.85% <= 8 milliseconds +99.86% <= 9 milliseconds +99.89% <= 10 milliseconds +99.92% <= 13 milliseconds +99.94% <= 14 milliseconds +99.95% <= 16 milliseconds +100.00% <= 16 milliseconds +33456.00 requests per second + +====== LPUSH (needed to benchmark LRANGE) ====== +100000 requests completed in 2.92 seconds +50 parallel clients +3 bytes payload +keep alive: 1 + +93.25% <= 1 milliseconds +99.45% <= 2 milliseconds +99.75% <= 3 milliseconds +99.86% <= 4 milliseconds +99.89% <= 5 milliseconds +99.91% <= 6 milliseconds +99.93% <= 9 milliseconds +99.95% <= 10 milliseconds +99.96% <= 11 milliseconds +99.97% <= 14 milliseconds +99.98% <= 15 milliseconds +99.99% <= 17 milliseconds +100.00% <= 18 milliseconds +100.00% <= 20 milliseconds +100.00% <= 20 milliseconds +34258.31 requests per second + +====== LRANGE_100 (first 100 elements) ====== +100000 requests completed in 4.33 seconds +50 parallel clients +3 bytes payload +keep alive: 1 + +35.50% <= 1 milliseconds +98.90% <= 2 milliseconds +99.61% <= 3 milliseconds +99.76% <= 4 milliseconds +99.83% <= 5 milliseconds +99.83% <= 7 milliseconds +99.84% <= 8 milliseconds +99.88% <= 9 milliseconds +99.88% <= 10 milliseconds +99.91% <= 11 milliseconds +99.91% <= 12 milliseconds +99.91% <= 13 milliseconds +99.96% <= 15 milliseconds +99.96% <= 34 milliseconds +99.97% <= 35 milliseconds +100.00% <= 39 milliseconds +100.00% <= 39 milliseconds +23089.36 requests per second + +====== LRANGE_300 (first 300 elements) ====== +100000 requests completed in 7.12 seconds +50 parallel clients +3 bytes payload +keep alive: 1 + +0.01% <= 1 milliseconds +84.00% <= 2 milliseconds +98.64% <= 3 milliseconds +99.44% <= 4 milliseconds +99.65% <= 5 milliseconds +99.70% <= 6 milliseconds +99.72% <= 7 milliseconds +99.75% <= 8 milliseconds +99.77% <= 9 milliseconds +99.81% <= 10 milliseconds +99.85% <= 11 milliseconds +99.87% <= 12 milliseconds +99.89% <= 13 milliseconds +99.90% <= 14 milliseconds +99.92% <= 15 milliseconds +99.96% <= 16 milliseconds +99.97% <= 17 milliseconds +99.99% <= 18 milliseconds +99.99% <= 26 milliseconds +99.99% <= 32 milliseconds +100.00% <= 37 milliseconds +100.00% <= 38 milliseconds +100.00% <= 39 milliseconds +14039.03 requests per second + +====== LRANGE_500 (first 450 elements) ====== +100000 requests completed in 8.32 seconds +50 parallel clients +3 bytes payload +keep alive: 1 + +0.71% <= 1 milliseconds +49.73% <= 2 milliseconds +96.81% <= 3 milliseconds +99.35% <= 4 milliseconds +99.79% <= 5 milliseconds +99.83% <= 6 milliseconds +99.84% <= 7 milliseconds +99.85% <= 8 milliseconds +99.91% <= 9 milliseconds +99.91% <= 10 milliseconds +99.91% <= 12 milliseconds +99.91% <= 27 milliseconds +99.91% <= 28 milliseconds +99.92% <= 29 milliseconds +99.93% <= 30 milliseconds +99.96% <= 31 milliseconds +99.96% <= 49 milliseconds +99.96% <= 50 milliseconds +99.98% <= 99 milliseconds +99.98% <= 100 milliseconds +100.00% <= 100 milliseconds +12022.12 requests per second + +====== LRANGE_600 (first 600 elements) ====== +100000 requests completed in 10.27 seconds +50 parallel clients +3 bytes payload +keep alive: 1 + +0.15% <= 1 milliseconds +28.15% <= 2 milliseconds +72.35% <= 3 milliseconds +96.20% <= 4 milliseconds +98.96% <= 5 milliseconds +99.68% <= 6 milliseconds +99.80% <= 7 milliseconds +99.85% <= 8 milliseconds +99.87% <= 9 milliseconds +99.88% <= 10 milliseconds +99.88% <= 11 milliseconds +99.88% <= 12 milliseconds +99.89% <= 13 milliseconds +99.89% <= 14 milliseconds +99.89% <= 15 milliseconds +99.90% <= 16 milliseconds +99.91% <= 17 milliseconds +99.91% <= 18 milliseconds +99.91% <= 19 milliseconds +99.92% <= 20 milliseconds +99.93% <= 21 milliseconds +99.95% <= 22 milliseconds +99.95% <= 23 milliseconds +99.96% <= 24 milliseconds +99.97% <= 25 milliseconds +99.97% <= 26 milliseconds +99.98% <= 27 milliseconds +100.00% <= 28 milliseconds +100.00% <= 29 milliseconds +100.00% <= 29 milliseconds +9736.15 requests per second + +====== MSET (10 keys) ====== +100000 requests completed in 2.94 seconds +50 parallel clients +3 bytes payload +keep alive: 1 + +92.48% <= 1 milliseconds +99.33% <= 2 milliseconds +99.91% <= 3 milliseconds +99.93% <= 4 milliseconds +99.94% <= 6 milliseconds +99.94% <= 11 milliseconds +99.96% <= 12 milliseconds +99.97% <= 13 milliseconds +99.98% <= 14 milliseconds +99.98% <= 17 milliseconds +99.99% <= 18 milliseconds +99.99% <= 19 milliseconds +99.99% <= 25 milliseconds +100.00% <= 30 milliseconds +100.00% <= 30 milliseconds +34059.95 requests per second \ No newline at end of file diff --git a/tests/BasicTest/SlowConfig.cs b/tests/BasicTest/SlowConfig.cs new file mode 100644 index 000000000..1d7e7cb1e --- /dev/null +++ b/tests/BasicTest/SlowConfig.cs @@ -0,0 +1,11 @@ +using BenchmarkDotNet.Jobs; + +namespace BasicTest; + +internal class SlowConfig : CustomConfig +{ + protected override Job Configure(Job j) + => j.WithLaunchCount(1) + .WithWarmupCount(1) + .WithIterationCount(5); +} diff --git a/tests/BasicTestBaseline/BasicTestBaseline.csproj b/tests/BasicTestBaseline/BasicTestBaseline.csproj index a9f75e441..7bc6d3697 100644 --- a/tests/BasicTestBaseline/BasicTestBaseline.csproj +++ b/tests/BasicTestBaseline/BasicTestBaseline.csproj @@ -6,12 +6,11 @@ BasicTestBaseline Exe BasicTestBaseline - $(DefineConstants);TEST_BASELINE - + diff --git a/tests/RESP.Core.Tests/BasicIntegrationTests.cs b/tests/RESP.Core.Tests/BasicIntegrationTests.cs new file mode 100644 index 000000000..128bd9b1f --- /dev/null +++ b/tests/RESP.Core.Tests/BasicIntegrationTests.cs @@ -0,0 +1,97 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Resp; +using RESPite.Redis; +using RESPite.Redis.Alt; // needed for AsStrings() etc +using Xunit; + +namespace RESP.Core.Tests; + +public class BasicIntegrationTests(ConnectionFixture fixture, ITestOutputHelper log) : IntegrationTestBase(fixture, log) +{ + [Fact] + public void Format() + { + Span buffer = stackalloc byte[128]; + var writer = new RespWriter(buffer); + RespFormatters.Value.String.Format("get"u8, ref writer, "abc"); + writer.Flush(); + Assert.Equal("*2\r\n$3\r\nget\r\n$3\r\nabc\r\n", writer.DebugBuffer()); + } + + [Fact] + public void Parse() + { + ReadOnlySpan buffer = "$3\r\nabc\r\n"u8; + var reader = new RespReader(buffer); + reader.MoveNext(); + var value = RespParsers.String.Parse(in Resp.Void.Instance, ref reader); + reader.DemandEnd(); + Assert.Equal("abc", value); + } + + [Theory] + [InlineData(1)] + [InlineData(5)] + [InlineData(100)] + public void Ping(int count) + { + using var conn = GetConnection(); + var ctx = conn.Context; + for (int i = 0; i < count; i++) + { + var key = $"{Me()}{i}"; + ctx.AsStrings().Set(key, $"def{i}"); + var val = ctx.AsStrings().Get(key); + Assert.Equal($"def{i}", val); + } + } + + [Theory] + [InlineData(1)] + [InlineData(5)] + [InlineData(100)] + public async Task PingAsync(int count) + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + await using var conn = GetConnection(); + for (int i = 0; i < count; i++) + { + var ctx = conn.Context.WithCancellationToken(cts.Token); + var key = $"{Me()}{i}"; + await ctx.AsStrings().SetAsync(key, $"def{i}"); + var val = await ctx.AsStrings().GetAsync(key); + Assert.Equal($"def{i}", val); + } + } + + [Theory] + [InlineData(1, false)] + [InlineData(5, false)] + [InlineData(100, false)] + [InlineData(1, true)] + [InlineData(5, true)] + [InlineData(100, true)] + public async Task PingPipelinedAsync(int count, bool forPipeline) + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + await using var conn = forPipeline ? GetConnection().ForPipeline() : GetConnection(); + + ValueTask[] tasks = new ValueTask[count]; + for (int i = 0; i < count; i++) + { + RespContext ctx = conn.Context.WithCancellationToken(cts.Token); + var key = $"{Me()}{i}"; + _ = ctx.AsStrings().SetAsync(key, $"def{i}"); + tasks[i] = ctx.AsStrings().GetAsync(key); + } + + for (int i = 0; i < count; i++) + { + var val = await tasks[i]; + Assert.Equal($"def{i}", val); + } + } +} diff --git a/tests/RESP.Core.Tests/BufferTests.cs b/tests/RESP.Core.Tests/BufferTests.cs new file mode 100644 index 000000000..68368451c --- /dev/null +++ b/tests/RESP.Core.Tests/BufferTests.cs @@ -0,0 +1,182 @@ +using System; +using System.Buffers; +using System.Diagnostics; +using System.Threading; +using System.Xml.XPath; +using Resp; +using Xunit; + +namespace RESP.Core.Tests; + +public class BufferTests +{ + [Fact] + public void SimpleUsage() + { + CycleBuffer buffer = CycleBuffer.Create(); + Assert.True(buffer.CommittedIsEmpty); + Assert.Equal(0, buffer.GetCommittedLength()); + Assert.False(buffer.TryGetFirstCommittedSpan(0, out _)); + + buffer.Write("hello world"u8); + Assert.False(buffer.CommittedIsEmpty, "should be empty"); + Assert.Equal(11, buffer.GetCommittedLength()); + + Assert.False(buffer.TryGetFirstCommittedSpan(-1, out _), "should have rejected full"); + Assert.True(buffer.TryGetFirstCommittedSpan(0, out var committed), "should have accepted partial"); + Assert.True(committed.SequenceEqual("hello world"u8)); + buffer.DiscardCommitted(11); + Assert.True(buffer.CommittedIsEmpty); + Assert.Equal(0, buffer.GetCommittedLength()); + Assert.False(buffer.TryGetFirstCommittedSpan(0, out _)); + + // now partial consume + buffer.Write("partial consume"u8); + Assert.False(buffer.CommittedIsEmpty); + Assert.Equal(15, buffer.GetCommittedLength()); + + Assert.False(buffer.TryGetFirstCommittedSpan(-1, out _)); + Assert.True(buffer.TryGetFirstCommittedSpan(0, out committed)); + Assert.True(committed.SequenceEqual("partial consume"u8)); + buffer.DiscardCommitted(8); + Assert.False(buffer.CommittedIsEmpty); + Assert.Equal(7, buffer.GetCommittedLength()); + Assert.True(buffer.TryGetFirstCommittedSpan(0, out committed)); + Assert.True(committed.SequenceEqual("consume"u8)); + buffer.DiscardCommitted(7); + Assert.True(buffer.CommittedIsEmpty); + Assert.Equal(0, buffer.GetCommittedLength()); + Assert.False(buffer.TryGetFirstCommittedSpan(0, out _)); + buffer.Release(); + } + + private sealed class CountingMemoryPool(MemoryPool? tail = null) : MemoryPool + { + private readonly MemoryPool _tail = tail ?? MemoryPool.Shared; + private int count; + + public int Count => Volatile.Read(ref count); + public override IMemoryOwner Rent(int minBufferSize = -1) => new Wrapper(this, _tail.Rent(minBufferSize)); + + protected override void Dispose(bool disposing) => throw new NotImplementedException(); + + private void Decrement() => Interlocked.Decrement(ref count); + + private CountingMemoryPool Increment() + { + Interlocked.Increment(ref count); + return this; + } + + public override int MaxBufferSize => _tail.MaxBufferSize; + + private sealed class Wrapper(CountingMemoryPool parent, IMemoryOwner tail) : IMemoryOwner + { + private int _disposed; + private readonly CountingMemoryPool _parent = parent.Increment(); + + public void Dispose() + { + if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0) + { + _parent.Decrement(); + tail.Dispose(); + } + else + { + ThrowDisposed(); + } + } + + private void ThrowDisposed() => throw new ObjectDisposedException(nameof(MemoryPool)); + + public Memory Memory + { + get + { + if (Volatile.Read(ref _disposed) != 0) ThrowDisposed(); + return tail.Memory; + } + } + } + } + + [Fact] + public void SkipAggregate() + { + var reader = new RespReader("*1\r\n$3\r\nabc\r\n"u8); // ["abc"] + reader.MoveNext(); + reader.SkipChildren(); + Assert.False(reader.TryMoveNext()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MultiSegmentUsage(bool multiSegmentRead) + { + byte[] garbage = new byte[1024 * 1024]; + var rand = new Random(Seed: 134521); + rand.NextBytes(garbage); + + int offset = 0; + var mgr = new CountingMemoryPool(); + CycleBuffer buffer = CycleBuffer.Create(mgr); + Assert.Equal(0, mgr.Count); + while (offset < garbage.Length) + { + var size = rand.Next(1, garbage.Length - offset + 1); + Debug.Assert(size > 0); + buffer.Write(new ReadOnlySpan(garbage, offset, size)); + offset += size; + Assert.Equal(offset, buffer.GetCommittedLength()); + } + + Assert.True(mgr.Count >= 50); // some non-trivial count + int total = 0; + if (multiSegmentRead) + { + while (!buffer.CommittedIsEmpty) + { + var seq = buffer.GetAllCommitted(); + var take = rand.Next((int)Math.Min(seq.Length, 4 * buffer.PageSize)) + 1; + var slice = seq.Slice(0, take); + Assert.True(SequenceEqual(slice, new(garbage, total, take)), "data integrity check"); + buffer.DiscardCommitted(take); + total += take; + } + } + else + { + while (buffer.TryGetFirstCommittedSpan(0, out var span)) + { + var take = rand.Next(span.Length) + 1; + var slice = span.Slice(0, take); + Assert.True(slice.SequenceEqual(new(garbage, total, take)), "data integrity check"); + buffer.DiscardCommitted(take); + total += take; + } + } + + Assert.Equal(garbage.Length, total); + Assert.Equal(3, mgr.Count); + buffer.Release(); + + Assert.Equal(0, mgr.Count); + + static bool SequenceEqual(ReadOnlySequence seq1, ReadOnlySpan seq2) + { + if (seq1.IsSingleSegment) + { + return seq1.First.Span.SequenceEqual(seq2); + } + + if (seq1.Length != seq2.Length) return false; + var arr = ArrayPool.Shared.Rent(seq2.Length); + seq1.CopyTo(arr); + var result = arr.AsSpan(0, seq2.Length).SequenceEqual(seq2); + ArrayPool.Shared.Return(arr); + return result; + } + } +} diff --git a/tests/RESP.Core.Tests/ConnectionFixture.cs b/tests/RESP.Core.Tests/ConnectionFixture.cs new file mode 100644 index 000000000..5fe57abbe --- /dev/null +++ b/tests/RESP.Core.Tests/ConnectionFixture.cs @@ -0,0 +1,20 @@ +using System; +using System.Net; +using System.Threading; +using Resp; +using RESP.Core.Tests; +using Xunit; + +[assembly: AssemblyFixture(typeof(ConnectionFixture))] + +namespace RESP.Core.Tests; + +public class ConnectionFixture : IDisposable +{ + private readonly RespConnectionPool _pool = new(new IPEndPoint(IPAddress.Loopback, 6379)); + + public void Dispose() => _pool.Dispose(); + + public IRespConnection GetConnection() + => _pool.GetConnection(cancellationToken: TestContext.Current.CancellationToken); +} diff --git a/tests/RESP.Core.Tests/IntegrationTestBase.cs b/tests/RESP.Core.Tests/IntegrationTestBase.cs new file mode 100644 index 000000000..61beba25c --- /dev/null +++ b/tests/RESP.Core.Tests/IntegrationTestBase.cs @@ -0,0 +1,22 @@ +using System.Runtime.CompilerServices; +using Resp; +using RESPite.Redis; +using RESPite.Redis.Alt; +using Xunit; + +namespace RESP.Core.Tests; + +public abstract class IntegrationTestBase(ConnectionFixture fixture, ITestOutputHelper log) +{ + public IRespConnection GetConnection([CallerMemberName] string caller = "") + { + var conn = fixture.GetConnection(); // includes cancellation from the test + // most of the time, they'll be using a key from Me(), so: pre-emptively nuke it + conn.Context.AsKeys().Del(caller); + return conn; + } + + public void Log(string message) => log?.WriteLine(message); + + protected string Me([CallerMemberName] string caller = "") => caller; +} diff --git a/tests/RESP.Core.Tests/RESP.Core.Tests.csproj b/tests/RESP.Core.Tests/RESP.Core.Tests.csproj new file mode 100644 index 000000000..75b13c455 --- /dev/null +++ b/tests/RESP.Core.Tests/RESP.Core.Tests.csproj @@ -0,0 +1,24 @@ + + + + net481;net8.0 + enable + false + true + Exe + + + + + + + + + + + + + + + + diff --git a/tests/RESP.Core.Tests/RedisStringsIntegrationTests.cs b/tests/RESP.Core.Tests/RedisStringsIntegrationTests.cs new file mode 100644 index 000000000..b4cdb496e --- /dev/null +++ b/tests/RESP.Core.Tests/RedisStringsIntegrationTests.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using RESPite.Redis; +using RESPite.Redis.Alt; // needed for AsStrings() etc +using Xunit; +using FactAttribute = StackExchange.Redis.Tests.FactAttribute; + +namespace RESP.Core.Tests; + +public class RedisStringsIntegrationTests(ConnectionFixture fixture, ITestOutputHelper log) + : IntegrationTestBase(fixture, log) +{ + [Fact] + public void Incr() + { + var key = Me(); + + using var conn = GetConnection(); + var ctx = conn.Context; + for (int i = 0; i < 5; i++) + { + ctx.AsStrings().Incr(key); + } + var result = ctx.AsStrings().GetInt32(key); + Assert.Equal(5, result); + } + + [Fact] + public async Task IncrAsync() + { + var key = Me(); + + await using var conn = GetConnection(); + var ctx = conn.Context; + for (int i = 0; i < 5; i++) + { + await ctx.AsStrings().IncrAsync(key); + } + var result = await ctx.AsStrings().GetInt32Async(key); + Assert.Equal(5, result); + } +} diff --git a/tests/RedisConfigs/.docker/Redis/Dockerfile b/tests/RedisConfigs/.docker/Redis/Dockerfile index 424abd1cd..4de03f221 100644 --- a/tests/RedisConfigs/.docker/Redis/Dockerfile +++ b/tests/RedisConfigs/.docker/Redis/Dockerfile @@ -1,4 +1,4 @@ -FROM redis:8.2.0 +FROM redis:7.4.2 COPY --from=configs ./Basic /data/Basic/ COPY --from=configs ./Failover /data/Failover/ diff --git a/tests/StackExchange.Redis.Tests/FailoverTests.cs b/tests/StackExchange.Redis.Tests/FailoverTests.cs index 68f8f2266..1f33275b5 100644 --- a/tests/StackExchange.Redis.Tests/FailoverTests.cs +++ b/tests/StackExchange.Redis.Tests/FailoverTests.cs @@ -217,7 +217,7 @@ public async Task SubscriptionsSurviveConnectionFailureAsync() await sub.PingAsync(); await Task.Delay(200).ConfigureAwait(false); - var counter1 = Thread.VolatileRead(ref counter); + var counter1 = Volatile.Read(ref counter); Log($"Expecting 1 message, got {counter1}"); Assert.Equal(1, counter1); @@ -274,9 +274,9 @@ public async Task SubscriptionsSurviveConnectionFailureAsync() // Give it a few seconds to get our messages Log("Waiting for 2 messages"); - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => Thread.VolatileRead(ref counter) == 2); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => Volatile.Read(ref counter) == 2); - var counter2 = Thread.VolatileRead(ref counter); + var counter2 = Volatile.Read(ref counter); Log($"Expecting 2 messages, got {counter2}"); Assert.Equal(2, counter2); diff --git a/tests/StackExchange.Redis.Tests/GetServerTests.cs b/tests/StackExchange.Redis.Tests/GetServerTests.cs deleted file mode 100644 index 50cb9e7ef..000000000 --- a/tests/StackExchange.Redis.Tests/GetServerTests.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Xunit; - -namespace StackExchange.Redis.Tests; - -public abstract class GetServerTestsBase(ITestOutputHelper output, SharedConnectionFixture fixture) - : TestBase(output, fixture) -{ - protected abstract bool IsCluster { get; } - - [Fact] - public async Task GetServersMemoization() - { - await using var conn = Create(); - - var servers0 = conn.GetServers(); - var servers1 = conn.GetServers(); - - // different array, exact same contents - Assert.NotSame(servers0, servers1); - Assert.NotEmpty(servers0); - Assert.NotNull(servers0); - Assert.NotNull(servers1); - Assert.Equal(servers0.Length, servers1.Length); - for (int i = 0; i < servers0.Length; i++) - { - Assert.Same(servers0[i], servers1[i]); - } - } - - [Fact] - public async Task GetServerByEndpointMemoization() - { - await using var conn = Create(); - var ep = conn.GetEndPoints().First(); - - IServer x = conn.GetServer(ep), y = conn.GetServer(ep); - Assert.Same(x, y); - - object asyncState = "whatever"; - x = conn.GetServer(ep, asyncState); - y = conn.GetServer(ep, asyncState); - Assert.NotSame(x, y); - } - - [Fact] - public async Task GetServerByKeyMemoization() - { - await using var conn = Create(); - RedisKey key = Me(); - string value = $"{key}:value"; - await conn.GetDatabase().StringSetAsync(key, value); - - IServer x = conn.GetServer(key), y = conn.GetServer(key); - Assert.False(y.IsReplica, "IsReplica"); - Assert.Same(x, y); - - y = conn.GetServer(key, flags: CommandFlags.DemandMaster); - Assert.Same(x, y); - - // async state demands separate instance - y = conn.GetServer(key, "async state", flags: CommandFlags.DemandMaster); - Assert.NotSame(x, y); - - // primary and replica should be different - y = conn.GetServer(key, flags: CommandFlags.DemandReplica); - Assert.NotSame(x, y); - Assert.True(y.IsReplica, "IsReplica"); - - // replica again: same - var z = conn.GetServer(key, flags: CommandFlags.DemandReplica); - Assert.Same(y, z); - - // check routed correctly - var actual = (string?)await x.ExecuteAsync(null, "get", [key], CommandFlags.NoRedirect); - Assert.Equal(value, actual); // check value against primary - - // for replica, don't check the value, because of replication delay - just: no error - _ = y.ExecuteAsync(null, "get", [key], CommandFlags.NoRedirect); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task GetServerWithDefaultKey(bool explicitNull) - { - await using var conn = Create(); - bool isCluster = conn.ServerSelectionStrategy.ServerType == ServerType.Cluster; - Assert.Equal(IsCluster, isCluster); // check our assumptions! - - // we expect explicit null and default to act the same, but: check - RedisKey key = explicitNull ? RedisKey.Null : default(RedisKey); - - IServer primary = conn.GetServer(key); - Assert.False(primary.IsReplica); - - IServer replica = conn.GetServer(key, flags: CommandFlags.DemandReplica); - Assert.True(replica.IsReplica); - - // check multiple calls - HashSet uniques = []; - for (int i = 0; i < 100; i++) - { - uniques.Add(conn.GetServer(key)); - } - - if (isCluster) - { - Assert.True(uniques.Count > 1); // should be able to get arbitrary servers - } - else - { - Assert.Single(uniques); - } - - uniques.Clear(); - for (int i = 0; i < 100; i++) - { - uniques.Add(conn.GetServer(key, flags: CommandFlags.DemandReplica)); - } - - if (isCluster) - { - Assert.True(uniques.Count > 1); // should be able to get arbitrary servers - } - else - { - Assert.Single(uniques); - } - } -} - -[RunPerProtocol] -public class GetServerTestsCluster(ITestOutputHelper output, SharedConnectionFixture fixture) : GetServerTestsBase(output, fixture) -{ - protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts; - - protected override bool IsCluster => true; -} - -[RunPerProtocol] -public class GetServerTestsStandalone(ITestOutputHelper output, SharedConnectionFixture fixture) : GetServerTestsBase(output, fixture) -{ - protected override string GetConfiguration() => // we want to test flags usage including replicas - TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; - - protected override bool IsCluster => false; -} diff --git a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs index 9656ee45b..cf6c7d326 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs @@ -197,7 +197,7 @@ public event EventHandler ServerMaintenanceEvent public IServer GetServer(IPAddress host, int port) => _inner.GetServer(host, port); public IServer GetServer(EndPoint endpoint, object? asyncState = null) => _inner.GetServer(endpoint, asyncState); - public IServer GetServer(RedisKey key, object? asyncState = null, CommandFlags flags = CommandFlags.None) => _inner.GetServer(key, asyncState, flags); + public IServer[] GetServers() => _inner.GetServers(); public string GetStatus() => _inner.GetStatus(); diff --git a/tests/StackExchange.Redis.Tests/KeyTests.cs b/tests/StackExchange.Redis.Tests/KeyTests.cs index e956af4ff..31cd87d79 100644 --- a/tests/StackExchange.Redis.Tests/KeyTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyTests.cs @@ -182,8 +182,8 @@ public async Task KeyEncoding() db.KeyDelete(key, CommandFlags.FireAndForget); db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - Assert.True(db.KeyEncoding(key) is "embstr" or "raw"); // server-version dependent - Assert.True(await db.KeyEncodingAsync(key) is "embstr" or "raw"); + Assert.Equal("embstr", db.KeyEncoding(key)); + Assert.Equal("embstr", await db.KeyEncodingAsync(key)); db.KeyDelete(key, CommandFlags.FireAndForget); db.ListLeftPush(key, "new value", flags: CommandFlags.FireAndForget); diff --git a/tests/StackExchange.Redis.Tests/LoggerTests.cs b/tests/StackExchange.Redis.Tests/LoggerTests.cs index e001250b0..bc097d24c 100644 --- a/tests/StackExchange.Redis.Tests/LoggerTests.cs +++ b/tests/StackExchange.Redis.Tests/LoggerTests.cs @@ -52,7 +52,7 @@ public class TestWrapperLoggerFactory(ILogger logger) : ILoggerFactory { public TestWrapperLogger Logger { get; } = new TestWrapperLogger(logger); - public void AddProvider(ILoggerProvider provider) { } + public void AddProvider(ILoggerProvider provider) => throw new NotImplementedException(); public ILogger CreateLogger(string categoryName) => Logger; public void Dispose() { } } @@ -81,9 +81,9 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except private class TestMultiLogger(params ILogger[] loggers) : ILogger { #if NET8_0_OR_GREATER - public IDisposable? BeginScope(TState state) where TState : notnull => null; + public IDisposable? BeginScope(TState state) where TState : notnull => throw new NotImplementedException(); #else - public IDisposable BeginScope(TState state) => null!; + public IDisposable BeginScope(TState state) => throw new NotImplementedException(); #endif public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) @@ -105,9 +105,9 @@ public TestLogger(LogLevel logLevel, TextWriter output) => (_logLevel, _output) = (logLevel, output); #if NET8_0_OR_GREATER - public IDisposable? BeginScope(TState state) where TState : notnull => null; + public IDisposable? BeginScope(TState state) where TState : notnull => throw new NotImplementedException(); #else - public IDisposable BeginScope(TState state) => null!; + public IDisposable BeginScope(TState state) => throw new NotImplementedException(); #endif public bool IsEnabled(LogLevel logLevel) => logLevel >= _logLevel; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) diff --git a/tests/StackExchange.Redis.Tests/PubSubTests.cs b/tests/StackExchange.Redis.Tests/PubSubTests.cs index 9418fe80f..a3dadb07e 100644 --- a/tests/StackExchange.Redis.Tests/PubSubTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubTests.cs @@ -30,19 +30,19 @@ public async Task ExplicitPublishMode() #pragma warning restore CS0618 await UntilConditionAsync( TimeSpan.FromSeconds(10), - () => Thread.VolatileRead(ref b) == 1 - && Thread.VolatileRead(ref c) == 1 - && Thread.VolatileRead(ref d) == 1); - Assert.Equal(0, Thread.VolatileRead(ref a)); - Assert.Equal(1, Thread.VolatileRead(ref b)); - Assert.Equal(1, Thread.VolatileRead(ref c)); - Assert.Equal(1, Thread.VolatileRead(ref d)); + () => Volatile.Read(ref b) == 1 + && Volatile.Read(ref c) == 1 + && Volatile.Read(ref d) == 1); + Assert.Equal(0, Volatile.Read(ref a)); + Assert.Equal(1, Volatile.Read(ref b)); + Assert.Equal(1, Volatile.Read(ref c)); + Assert.Equal(1, Volatile.Read(ref d)); #pragma warning disable CS0618 pub.Publish("*bcd", "efg"); #pragma warning restore CS0618 - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Thread.VolatileRead(ref a) == 1); - Assert.Equal(1, Thread.VolatileRead(ref a)); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Volatile.Read(ref a) == 1); + Assert.Equal(1, Volatile.Read(ref a)); } [Theory] @@ -86,7 +86,7 @@ public async Task TestBasicPubSub(string? channelPrefix, bool wildCard, string b { Assert.Empty(received); } - Assert.Equal(0, Thread.VolatileRead(ref secondHandler)); + Assert.Equal(0, Volatile.Read(ref secondHandler)); #pragma warning disable CS0618 var count = sub.Publish(pubChannel, "def"); #pragma warning restore CS0618 @@ -99,8 +99,8 @@ public async Task TestBasicPubSub(string? channelPrefix, bool wildCard, string b Assert.Single(received); } // Give handler firing a moment - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 1); - Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref secondHandler) == 1); + Assert.Equal(1, Volatile.Read(ref secondHandler)); // unsubscribe from first; should still see second #pragma warning disable CS0618 @@ -113,9 +113,9 @@ public async Task TestBasicPubSub(string? channelPrefix, bool wildCard, string b Assert.Single(received); } - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 2); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref secondHandler) == 2); - var secondHandlerCount = Thread.VolatileRead(ref secondHandler); + var secondHandlerCount = Volatile.Read(ref secondHandler); Log("Expecting 2 from second handler, got: " + secondHandlerCount); Assert.Equal(2, secondHandlerCount); Assert.Equal(1, count); @@ -130,7 +130,7 @@ public async Task TestBasicPubSub(string? channelPrefix, bool wildCard, string b { Assert.Single(received); } - secondHandlerCount = Thread.VolatileRead(ref secondHandler); + secondHandlerCount = Volatile.Read(ref secondHandler); Log("Expecting 2 from second handler, got: " + secondHandlerCount); Assert.Equal(2, secondHandlerCount); Assert.Equal(0, count); @@ -170,7 +170,7 @@ public async Task TestBasicPubSubFireAndForget() { Assert.Empty(received); } - Assert.Equal(0, Thread.VolatileRead(ref secondHandler)); + Assert.Equal(0, Volatile.Read(ref secondHandler)); await PingAsync(pub, sub).ForAwait(); var count = sub.Publish(key, "def", CommandFlags.FireAndForget); await PingAsync(pub, sub).ForAwait(); @@ -182,7 +182,7 @@ public async Task TestBasicPubSubFireAndForget() { Assert.Single(received); } - Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); + Assert.Equal(1, Volatile.Read(ref secondHandler)); sub.Unsubscribe(key); count = sub.Publish(key, "ghi", CommandFlags.FireAndForget); @@ -241,7 +241,7 @@ public async Task TestPatternPubSub() { Assert.Empty(received); } - Assert.Equal(0, Thread.VolatileRead(ref secondHandler)); + Assert.Equal(0, Volatile.Read(ref secondHandler)); await PingAsync(pub, sub).ForAwait(); var count = sub.Publish(RedisChannel.Literal("abc"), "def"); @@ -254,8 +254,8 @@ public async Task TestPatternPubSub() } // Give reception a bit, the handler could be delayed under load - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 1); - Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref secondHandler) == 1); + Assert.Equal(1, Volatile.Read(ref secondHandler)); #pragma warning disable CS0618 sub.Unsubscribe("a*c"); diff --git a/tests/StackExchange.Redis.Tests/SSLTests.cs b/tests/StackExchange.Redis.Tests/SSLTests.cs index 0dafe3f9b..5c01bd817 100644 --- a/tests/StackExchange.Redis.Tests/SSLTests.cs +++ b/tests/StackExchange.Redis.Tests/SSLTests.cs @@ -240,7 +240,9 @@ public async Task RedisLabsSSL() Skip.IfNoConfig(nameof(TestConfig.Config.RedisLabsSslServer), TestConfig.Current.RedisLabsSslServer); Skip.IfNoConfig(nameof(TestConfig.Config.RedisLabsPfxPath), TestConfig.Current.RedisLabsPfxPath); +#pragma warning disable SYSLIB0057 // because of TFM support var cert = new X509Certificate2(TestConfig.Current.RedisLabsPfxPath, ""); +#pragma warning restore SYSLIB0057 Assert.NotNull(cert); Log("Thumbprint: " + cert.Thumbprint); diff --git a/tests/StackExchange.Redis.Tests/StreamTests.cs b/tests/StackExchange.Redis.Tests/StreamTests.cs index 58d2bb1fb..196913f40 100644 --- a/tests/StackExchange.Redis.Tests/StreamTests.cs +++ b/tests/StackExchange.Redis.Tests/StreamTests.cs @@ -2155,28 +2155,6 @@ public async Task AddWithApproxCount(StreamTrimMode mode) db.StreamAdd(key, "field", "value", maxLength: 10, useApproximateMaxLength: true, trimMode: mode, flags: CommandFlags.None); } - [Theory] - [InlineData(StreamTrimMode.KeepReferences, 1)] - [InlineData(StreamTrimMode.DeleteReferences, 1)] - [InlineData(StreamTrimMode.Acknowledged, 1)] - [InlineData(StreamTrimMode.KeepReferences, 2)] - [InlineData(StreamTrimMode.DeleteReferences, 2)] - [InlineData(StreamTrimMode.Acknowledged, 2)] - public async Task AddWithMultipleApproxCount(StreamTrimMode mode, int count) - { - await using var conn = Create(require: ForMode(mode)); - - var db = conn.GetDatabase(); - var key = Me() + ":" + mode; - - var pairs = new NameValueEntry[count]; - for (var i = 0; i < count; i++) - { - pairs[i] = new NameValueEntry($"field{i}", $"value{i}"); - } - db.StreamAdd(key, maxLength: 10, useApproximateMaxLength: true, trimMode: mode, flags: CommandFlags.None, streamPairs: pairs); - } - [Fact] public async Task StreamReadGroupWithNoAckShowsNoPendingMessages() { diff --git a/tests/StackExchange.Redis.Tests/SyncContextTests.cs b/tests/StackExchange.Redis.Tests/SyncContextTests.cs index 8eddc9f1d..5feb37e3d 100644 --- a/tests/StackExchange.Redis.Tests/SyncContextTests.cs +++ b/tests/StackExchange.Redis.Tests/SyncContextTests.cs @@ -118,11 +118,11 @@ public MySyncContext(TextWriter log) _log = log; SetSynchronizationContext(this); } - public int OpCount => Thread.VolatileRead(ref _opCount); + public int OpCount => Volatile.Read(ref _opCount); private int _opCount; private void Incr() => Interlocked.Increment(ref _opCount); - public void Reset() => Thread.VolatileWrite(ref _opCount, 0); + public void Reset() => Volatile.Write(ref _opCount, 0); public override string ToString() => $"Sync context ({(IsCurrent ? "active" : "inactive")}): {OpCount}"; From 818f5e3d622c6ed5340da9776002536c069b59d8 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 29 Aug 2025 13:45:24 +0100 Subject: [PATCH 002/108] testing setup --- src/RESPite/Internal/IRespMessage.cs | 1 + src/RESPite/Internal/RespMessageBase.cs | 9 ++++ src/RESPite/RespOperation.cs | 45 +++++++++++++++++++ src/RESPite/RespOperationT.cs | 27 +---------- tests/RESP.Core.Tests/OperationUnitTests.cs | 50 +++++++++++++++++++++ 5 files changed, 106 insertions(+), 26 deletions(-) create mode 100644 tests/RESP.Core.Tests/OperationUnitTests.cs diff --git a/src/RESPite/Internal/IRespMessage.cs b/src/RESPite/Internal/IRespMessage.cs index 97c0bcf11..4632c45c4 100644 --- a/src/RESPite/Internal/IRespMessage.cs +++ b/src/RESPite/Internal/IRespMessage.cs @@ -15,4 +15,5 @@ internal interface IRespMessage : IValueTaskSource void ReleaseRequest(); bool AllowInlineParsing { get; } short Token { get; } + void OnSent(short token); } diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs index c903e64c0..801166b19 100644 --- a/src/RESPite/Internal/RespMessageBase.cs +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -128,6 +128,15 @@ private bool SetFlag(int flag) // in the "any" sense private bool HasFlag(int flag) => (Volatile.Read(ref _flags) & flag) != 0; + public void OnSent(short token) + { + if (GetStatus(token) != ValueTaskSourceStatus.Pending || _requestRefCount != 0 || !SetFlag(Flag_Sent)) + { + throw new InvalidOperationException( + "Operation must be in a pending, unsent state with no request payload. "); + } + } + public RespMessageBase Init(byte[] oversized, int offset, int length, ArrayPool? pool, CancellationToken cancellation) { DebugAssertPending(); diff --git a/src/RESPite/RespOperation.cs b/src/RESPite/RespOperation.cs index ba60f2fb9..ff8b1ba1c 100644 --- a/src/RESPite/RespOperation.cs +++ b/src/RESPite/RespOperation.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks.Sources; using RESPite.Internal; +using RESPite.Messages; namespace RESPite; @@ -116,6 +117,11 @@ internal Remote(IRespMessage message) _token = message.Token; } + /// + /// Record the operation as sent. + /// + public void OnSent() => _message.OnSent(_token); + /// public bool TrySetCanceled(CancellationToken cancellationToken = default) => _message.TrySetCanceled(_token); @@ -134,4 +140,43 @@ public bool TrySetResult(scoped ReadOnlySpan response) public bool TrySetResult(in ReadOnlySequence response) => _message.TrySetResult(_token, response); } + + /// + /// Create a disconnected without a RESP parser; this is only intended for testing purposes. + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static RespOperation Create(out Remote remote) + { + var msg = RespMessage.Get(null); + remote = new(msg); + return new RespOperation(msg); + } + + /// + /// Create a disconnected with a stateless RESP parser; this is only intended for testing purposes. + /// + /// The result of the operation. + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static RespOperation Create(IRespParser parser, out Remote remote) + { + var msg = RespMessage.Get(parser); + remote = new(msg); + return new RespOperation(msg); + } + + /// + /// Create a disconnected with a stateful RESP parser; this is only intended for testing purposes. + /// + /// The state used by the parser. + /// The result of the operation. + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static RespOperation Create( + in TState state, + IRespParser parser, + out Remote remote) + { + var msg = RespMessage.Get(in state, parser); + remote = new(msg); + return new RespOperation(msg); + } } diff --git a/src/RESPite/RespOperationT.cs b/src/RESPite/RespOperationT.cs index 69ad38666..4114911a4 100644 --- a/src/RESPite/RespOperationT.cs +++ b/src/RESPite/RespOperationT.cs @@ -1,8 +1,6 @@ -using System.ComponentModel; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using System.Threading.Tasks.Sources; using RESPite.Internal; -using RESPite.Messages; namespace RESPite; @@ -110,27 +108,4 @@ public RespOperation ConfigureAwait(bool continueOnCapturedContext) Unsafe.AsRef(in clone._disableCaptureContext) = !continueOnCapturedContext; return clone; } - - /// - /// Create a disconnected with a RESP parser; this is only intended for testing purposes. - /// - [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - public static RespOperation Create(IRespParser? parser, out RespOperation.Remote remote) - { - var msg = RespMessage.Get(parser); - remote = new(msg); - return new RespOperation(msg); - } - - /// - /// Create a disconnected with a stateful RESP parser; this is only intended for testing purposes. - /// - /// The state used by the parser. - [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - public static RespOperation Create(in TState state, IRespParser? parser, out RespOperation.Remote remote) - { - var msg = RespMessage.Get(in state, parser); - remote = new(msg); - return new RespOperation(msg); - } } diff --git a/tests/RESP.Core.Tests/OperationUnitTests.cs b/tests/RESP.Core.Tests/OperationUnitTests.cs new file mode 100644 index 000000000..3a721c45e --- /dev/null +++ b/tests/RESP.Core.Tests/OperationUnitTests.cs @@ -0,0 +1,50 @@ +using System; +using RESPite; +using Xunit; + +namespace RESP.Core.Tests; + +public class OperationUnitTests +{ + [Fact] + public void CanCreateAndCompleteOperation() + { + var op = RespOperation.Create(null, out var remote); + // initial state + Assert.False(op.IsCanceled); + Assert.False(op.IsCompleted); + Assert.False(op.IsCompletedSuccessfully); + Assert.False(op.IsFaulted); + + // complete first time + Assert.True(remote.TrySetResult(default)); + Assert.False(op.IsCanceled); + Assert.True(op.IsCompleted); + Assert.True(op.IsCompletedSuccessfully); + Assert.False(op.IsFaulted); + + // additional completions fail + Assert.False(remote.TrySetResult(default)); +#pragma warning disable xUnit1051 + Assert.False(remote.TrySetCanceled()); +#pragma warning restore xUnit1051 + Assert.False(remote.TrySetException(null!)); + + // can get result + _ = op.GetResult(); + + // but only once, after that: bad things + Assert.Throws(() => op.GetResult()); + Assert.Throws(() => op.IsCanceled); + Assert.Throws(() => op.IsCompleted); + Assert.Throws(() => op.IsCompletedSuccessfully); + Assert.Throws(() => op.IsFaulted); + + // additional completions continue to fail + Assert.False(remote.TrySetResult(default)); +#pragma warning disable xUnit1051 + Assert.False(remote.TrySetCanceled()); +#pragma warning restore xUnit1051 + Assert.False(remote.TrySetException(null!)); + } +} From 7fc7910ed0ca7957160568141b75e38db1943156 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 29 Aug 2025 17:07:09 +0100 Subject: [PATCH 003/108] intermediate, builds (only) --- Directory.Build.props | 2 +- StackExchange.Redis.sln | 7 + .../RespCommandGenerator.cs | 70 +- .../StackExchange.Redis.Build.csproj | 4 +- .../RESPite.Benchmark}/BenchmarkBase.cs | 41 +- .../RESPite.Benchmark}/NewCoreBenchmark.cs | 74 +- .../RESPite.Benchmark}/OldCoreBenchmark.cs | 2 +- src/RESPite.Benchmark/Program.cs | 77 ++ .../RESPite.Benchmark.csproj | 25 + .../RESPite.Benchmark}/RespBenchmark.md | 0 .../{ => Internal}/BatchConnection.cs | 2 +- .../Internal/ConfiguredConnection.cs | 35 + .../Connections/Internal/NullConnection.cs | 37 + .../Internal/SynchronizedConnection.cs | 219 +++++ .../Connections/RespConnectionExtensions.cs | 17 + src/RESPite/Connections/RespConnectionPool.cs | 194 ++++ src/RESPite/Internal/IRespMessage.cs | 1 + src/RESPite/Internal/RespMessageBase.cs | 1 + src/RESPite/Messages/IRespFormatterT.cs | 19 + src/RESPite/Messages/RespWriter.cs | 914 ++++++++++++++++++ src/RESPite/RespCommandAttribute.cs | 34 + src/RESPite/RespContext.cs | 31 +- src/RESPite/RespContextExtensions.cs | 37 + src/RESPite/RespFormatters.cs | 70 ++ src/RESPite/RespOperation.cs | 1 + src/RESPite/RespOperationBuilder.cs | 50 + src/RESPite/RespOperationT.cs | 2 + src/RESPite/RespParsers.cs | 191 ++++ tests/BasicTest/Program.cs | 89 +- tests/RESP.Core.Tests/OperationUnitTests.cs | 9 +- 30 files changed, 2057 insertions(+), 198 deletions(-) rename {tests/BasicTest => src/RESPite.Benchmark}/BenchmarkBase.cs (95%) rename {tests/BasicTest => src/RESPite.Benchmark}/NewCoreBenchmark.cs (72%) rename {tests/BasicTest => src/RESPite.Benchmark}/OldCoreBenchmark.cs (99%) create mode 100644 src/RESPite.Benchmark/Program.cs create mode 100644 src/RESPite.Benchmark/RESPite.Benchmark.csproj rename {tests/BasicTest => src/RESPite.Benchmark}/RespBenchmark.md (100%) rename src/RESPite/Connections/{ => Internal}/BatchConnection.cs (99%) create mode 100644 src/RESPite/Connections/Internal/ConfiguredConnection.cs create mode 100644 src/RESPite/Connections/Internal/NullConnection.cs create mode 100644 src/RESPite/Connections/Internal/SynchronizedConnection.cs create mode 100644 src/RESPite/Connections/RespConnectionExtensions.cs create mode 100644 src/RESPite/Connections/RespConnectionPool.cs create mode 100644 src/RESPite/Messages/IRespFormatterT.cs create mode 100644 src/RESPite/Messages/RespWriter.cs create mode 100644 src/RESPite/RespCommandAttribute.cs create mode 100644 src/RESPite/RespContextExtensions.cs create mode 100644 src/RESPite/RespFormatters.cs create mode 100644 src/RESPite/RespOperationBuilder.cs create mode 100644 src/RESPite/RespParsers.cs diff --git a/Directory.Build.props b/Directory.Build.props index 35291c5d8..bc079a5ff 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - true + false 2.0.0 2014 - $([System.DateTime]::Now.Year) Stack Exchange, Inc. diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index 2ade781b0..736c56699 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -136,6 +136,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RESPite.Redis", "src\RESPit EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RESPite.StackExchange.Redis", "src\RESPite.StackExchange.Redis\RESPite.StackExchange.Redis.csproj", "{A5580114-C236-494E-851C-A21E3DB86FC8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RESPite.Benchmark", "src\RESPite.Benchmark\RESPite.Benchmark.csproj", "{3725A78B-B6B5-4379-9DE0-37A180ADE95A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -218,6 +220,10 @@ Global {A5580114-C236-494E-851C-A21E3DB86FC8}.Debug|Any CPU.Build.0 = Debug|Any CPU {A5580114-C236-494E-851C-A21E3DB86FC8}.Release|Any CPU.ActiveCfg = Release|Any CPU {A5580114-C236-494E-851C-A21E3DB86FC8}.Release|Any CPU.Build.0 = Release|Any CPU + {3725A78B-B6B5-4379-9DE0-37A180ADE95A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3725A78B-B6B5-4379-9DE0-37A180ADE95A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3725A78B-B6B5-4379-9DE0-37A180ADE95A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3725A78B-B6B5-4379-9DE0-37A180ADE95A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -246,6 +252,7 @@ Global {F8762EE5-3461-4F6B-8C24-C876B6D9E637} = {00CA0876-DA9F-44E8-B0DC-A88716BF347A} {3A92C2E7-3033-4FDF-8DDC-5DF43D290537} = {00CA0876-DA9F-44E8-B0DC-A88716BF347A} {A5580114-C236-494E-851C-A21E3DB86FC8} = {00CA0876-DA9F-44E8-B0DC-A88716BF347A} + {3725A78B-B6B5-4379-9DE0-37A180ADE95A} = {00CA0876-DA9F-44E8-B0DC-A88716BF347A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {193AA352-6748-47C1-A5FC-C9AA6B5F000B} diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index d32e96153..1e4cfd85b 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -104,7 +104,11 @@ private static string GetName(ITypeSymbol type) string? formatter = null, parser = null; foreach (var attrib in method.GetAttributes()) { - if (attrib.AttributeClass?.Name == "RespCommandAttribute") + if (attrib.AttributeClass is + { + Name: "RespCommandAttribute", + ContainingNamespace: { Name: "RESPite", ContainingNamespace.IsGlobalNamespace: true } + }) { if (attrib.ConstructorArguments.Length == 1) { @@ -221,7 +225,7 @@ static bool IsRespContext(ITypeSymbol type) => type is INamedTypeSymbol { Name: "RespContext", - ContainingNamespace: { Name: "Resp", ContainingNamespace.IsGlobalNamespace: true } + ContainingNamespace: { Name: "RESPite", ContainingNamespace.IsGlobalNamespace: true } }; var syntax = (MethodDeclarationSyntax)ctx.Node; @@ -258,9 +262,15 @@ private bool IsKey(IParameterSymbol param) foreach (var attrib in param.GetAttributes()) { - if (attrib.AttributeClass?.Name == "KeyAttribute") return true; + if (attrib.AttributeClass is + { + Name: "KeyAttribute", + ContainingNamespace: { Name: "RESPite", ContainingNamespace.IsGlobalNamespace: true } + }) + { + return true; + } } - return false; } @@ -414,10 +424,11 @@ void WriteMethod(bool asAsync) sb.Append(")"); indent++; - var parser = method.Parser ?? InbuiltParser(method.ReturnType); + var parser = method.Parser ?? InbuiltParser(method.ReturnType, explicitSuccess: true); bool useDirectCall = method.Context is { Length: > 0 } & formatter is { Length: > 0 } & parser is { Length: > 0 }; + useDirectCall = false; // disable for now if (string.IsNullOrWhiteSpace(method.Context)) { NewLine().Append("=> throw new NotSupportedException(\"No RespContext available\");"); @@ -439,7 +450,7 @@ void WriteMethod(bool asAsync) } } - sb.Append(asAsync ? ").AsValueTask" : ").Wait"); + sb.Append(asAsync ? ").Send" : ").Wait"); if (!string.IsNullOrWhiteSpace(method.ReturnType)) { sb.Append('<').Append(method.ReturnType).Append('>'); @@ -450,7 +461,9 @@ void WriteMethod(bool asAsync) if (useDirectCall) // avoid the intermediate step when possible { - sb = NewLine().Append("=> global::Resp.Message.Send").Append(asAsync ? "Async" : "") + /* + sb = NewLine().Append("=> ").Append(context).A "global::RESPite.Messages.something.Send") + .Append(asAsync ? "Async" : "") .Append('<'); WriteTuple( method.Parameters, @@ -460,6 +473,7 @@ void WriteMethod(bool asAsync) .Append(csValue).Append("u8").Append(", "); WriteTuple(method.Parameters, sb, TupleMode.Values); sb.Append(", ").Append(formatter).Append(", ").Append(parser).Append(");"); + */ } indent--; @@ -484,7 +498,7 @@ void WriteMethod(bool asAsync) var names = tuple.Value.Shared ? TupleMode.SyntheticNames : TupleMode.NamedTuple; NewLine(); - sb = NewLine().Append("sealed file class ").Append(name).Append(" : Resp.IRespFormatter<"); + sb = NewLine().Append("sealed file class ").Append(name).Append(" : global::RESPite.Messages.IRespFormatter<"); WriteTuple(parameters, sb, names); sb.Append('>'); NewLine().Append("{"); @@ -493,7 +507,7 @@ void WriteMethod(bool asAsync) NewLine(); sb = NewLine() - .Append("public void Format(scoped ReadOnlySpan command, ref Resp.RespWriter writer, in "); + .Append("public void Format(scoped ReadOnlySpan command, ref global::RESPite.Messages.RespWriter writer, in "); WriteTuple(parameters, sb, names); sb.Append(" request)"); NewLine().Append("{"); @@ -639,30 +653,34 @@ private static int DataParameterCount( return count; } + private const string RespFormattersPrefix = "global::RESPite.RespFormatters."; + private static string? InbuiltFormatter(string type, bool isKey) => type switch { - "string" => isKey ? "Resp.RespFormatters.Key.String" : "Resp.RespFormatters.Value.String", - "byte[]" => isKey ? "Resp.RespFormatters.Key.ByteArray" : "Resp.RespFormatters.Value.ByteArray", - "int" => "Resp.RespFormatters.Int32", - "long" => "Resp.RespFormatters.Int64", - "float" => "Resp.RespFormatters.Single", - "double" => "Resp.RespFormatters.Double", + "string" => isKey ? (RespFormattersPrefix + "Key.String") : (RespFormattersPrefix + "Value.String"), + "byte[]" => isKey ? (RespFormattersPrefix + "Key.ByteArray") : (RespFormattersPrefix + "Value.ByteArray"), + "int" => RespFormattersPrefix + "Int32", + "long" => RespFormattersPrefix + "Int64", + "float" => RespFormattersPrefix + "Single", + "double" => RespFormattersPrefix + "Double", _ => null, }; + private const string RespParsersPrefix = "global::RESPite.RespParsers."; + private static string? InbuiltParser(string type, bool explicitSuccess = false) => type switch { - "" when explicitSuccess => "Resp.RespParsers.Success", - "string" => "Resp.RespParsers.String", - "int" => "Resp.RespParsers.Int32", - "long" => "Resp.RespParsers.Int64", - "float" => "Resp.RespParsers.Single", - "double" => "Resp.RespParsers.Double", - "int?" => "Resp.RespParsers.NullableInt32", - "long?" => "Resp.RespParsers.NullableInt64", - "float?" => "Resp.RespParsers.NullableSingle", - "double?" => "Resp.RespParsers.NullableDouble", - "global::Resp.ResponseSummary" => "Resp.ResponseSummary.Parser", + "" when explicitSuccess => RespParsersPrefix + "Success", + "string" => RespParsersPrefix + "String", + "int" => RespParsersPrefix + "Int32", + "long" => RespParsersPrefix + "Int64", + "float" => RespParsersPrefix + "Single", + "double" => RespParsersPrefix + "Double", + "int?" => RespParsersPrefix + "NullableInt32", + "long?" => RespParsersPrefix + "NullableInt64", + "float?" => RespParsersPrefix + "NullableSingle", + "double?" => RespParsersPrefix + "NullableDouble", + "global::Resp.ResponseSummary" => RespParsersPrefix + "ResponseSummary.Parser", _ => null, }; diff --git a/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj index c7b4f6506..5363bef38 100644 --- a/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj +++ b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj @@ -1,7 +1,9 @@  - netstandard2.0 + + netstandard2.0;net8.0;net9.0 enable enable true diff --git a/tests/BasicTest/BenchmarkBase.cs b/src/RESPite.Benchmark/BenchmarkBase.cs similarity index 95% rename from tests/BasicTest/BenchmarkBase.cs rename to src/RESPite.Benchmark/BenchmarkBase.cs index f8adcfad5..624bc8bd9 100644 --- a/tests/BasicTest/BenchmarkBase.cs +++ b/src/RESPite.Benchmark/BenchmarkBase.cs @@ -6,24 +6,11 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; -#if TEST_BASELINE -using Void = BasicTest.Void; - -#else -using Resp; -using Void = Resp.Void; -#endif +using RESPite; // influenced by redis-benchmark, see .md file -namespace BasicTest; +namespace RESPite.Benchmark; -#if TEST_BASELINE -public readonly struct Void -{ - private static readonly Void _instance; - public static ref readonly Void Instance => ref _instance; -} -#endif public abstract class BenchmarkBase : IDisposable { protected const string @@ -137,13 +124,13 @@ public BenchmarkBase(string[] args) protected static readonly Func NoFlush = () => throw new NotSupportedException("Not a batch; cannot flush"); - protected Task Pipeline(Func operation, Func flush) => + protected Task Pipeline(Func operation, Func flush) => Pipeline(() => new ValueTask(operation()), flush); - protected Task Pipeline(Func> operation, Func flush) => + protected Task Pipeline(Func> operation, Func flush) => Pipeline(() => new ValueTask(operation()), flush); - protected async Task Pipeline(Func operation, Func flush) + protected async Task Pipeline(Func operation, Func flush) { var opsPerClient = OperationsPerClient; int i = 0; @@ -212,15 +199,13 @@ protected async Task Pipeline(Func operation, Func f Console.Error.WriteLine($"{operation.Method.Name} failed after {i} operations"); Program.WriteException(ex); } - - return Void.Instance; } - protected async Task Pipeline(Func> operation, Func flush) + protected async Task Pipeline(Func> operation, Func flush) { var opsPerClient = OperationsPerClient; int i = 0; - T result = default; + T? result = default; try { if (PipelineDepth == 1) @@ -334,9 +319,9 @@ public async Task InitAsync() protected abstract TClient CreateBatch(TClient client); protected async Task RunAsync( - string key, + string? key, Func, Task> action, - Func init = null, + Func? init = null, string format = "") { string name = action.Method.Name; @@ -402,8 +387,8 @@ protected async Task RunAsync( var pending = new Task[ClientCount]; int index = 0; -#if DEBUG && !TEST_BASELINE - DebugCounters.Flush(); +#if DEBUG + Internal.DebugCounters.Flush(); #endif // optionally support cancellation, applied per-test CancellationToken cancellationToken = CancellationToken.None; @@ -439,7 +424,7 @@ protected async Task RunAsync( $"{TotalOperations:###,###,##0} requests completed in {seconds:0.00} seconds, {rate:###,###,##0} ops/sec"); } - if (typeof(T) != typeof(Void) && !Quiet) + if (!Quiet) { if (string.IsNullOrWhiteSpace(format)) { @@ -458,7 +443,7 @@ protected async Task RunAsync( finally { #if DEBUG && !TEST_BASELINE - var counters = DebugCounters.Flush(); // flush even if not showing + var counters = Internal.DebugCounters.Flush(); // flush even if not showing if (!Quiet) { if (counters.WriteBytes != 0) diff --git a/tests/BasicTest/NewCoreBenchmark.cs b/src/RESPite.Benchmark/NewCoreBenchmark.cs similarity index 72% rename from tests/BasicTest/NewCoreBenchmark.cs rename to src/RESPite.Benchmark/NewCoreBenchmark.cs index 4faf3bde0..a345f7ae2 100644 --- a/tests/BasicTest/NewCoreBenchmark.cs +++ b/src/RESPite.Benchmark/NewCoreBenchmark.cs @@ -1,12 +1,11 @@ -#if !TEST_BASELINE -using System; +using System; using System.ComponentModel; using System.Threading; using System.Threading.Tasks; -using Resp; -using Void = Resp.Void; +using RESPite.Connections; +using RESPite.Messages; -namespace BasicTest; +namespace RESPite.Benchmark; public sealed class NewCoreBenchmark : BenchmarkBase { @@ -40,7 +39,7 @@ public NewCoreBenchmark(string[] args) : base(args) if (Multiplexed) { - var conn = _connectionPool.GetConnection().ForPipeline(); + var conn = _connectionPool.GetConnection().Synchronized(); var ctx = conn.Context; for (int i = 0; i < ClientCount; i++) // init all { @@ -54,7 +53,7 @@ public NewCoreBenchmark(string[] args) : base(args) var conn = _connectionPool.GetConnection(); if (PipelineDepth > 1) { - conn = conn.ForPipeline(); + conn = conn.Synchronized(); } _clients[i] = conn.Context; @@ -125,21 +124,21 @@ protected override Func GetFlush(RespContext client) } [DisplayName("PING_INLINE")] - private Task PingInline(RespContext ctx, Func flush) => Pipeline(() => ctx.PingInlineAsync(_payload), flush); + private Task PingInline(RespContext ctx, Func flush) => Pipeline(() => ctx.PingInlineAsync(_payload), flush); [DisplayName("PING_BULK")] - private Task PingBulk(RespContext ctx, Func flush) => Pipeline(() => ctx.PingAsync(_payload), flush); + private Task PingBulk(RespContext ctx, Func flush) => Pipeline(() => ctx.PingAsync(_payload), flush); [DisplayName("INCR")] private Task Incr(RespContext ctx, Func flush) => Pipeline(() => ctx.IncrAsync(_counterKey), flush); [DisplayName("GET")] - private Task Get(RespContext ctx, Func flush) => Pipeline(() => ctx.GetAsync(_getSetKey), flush); + private Task Get(RespContext ctx, Func flush) => Pipeline(() => ctx.GetAsync(_getSetKey), flush); private Task GetInit(RespContext ctx) => ctx.SetAsync(_getSetKey, _payload).AsTask(); [DisplayName("SET")] - private Task Set(RespContext ctx, Func flush) => Pipeline(() => ctx.SetAsync(_getSetKey, _payload), flush); + private Task Set(RespContext ctx, Func flush) => Pipeline(() => ctx.SetAsync(_getSetKey, _payload), flush); [DisplayName("LPUSH")] private Task LPush(RespContext ctx, Func flush) => Pipeline(() => ctx.LPushAsync(_listKey, _payload), flush); @@ -148,22 +147,22 @@ protected override Func GetFlush(RespContext client) private Task RPush(RespContext ctx, Func flush) => Pipeline(() => ctx.RPushAsync(_listKey, _payload), flush); [DisplayName("LRANGE_100")] - private Task LRange100(RespContext ctx, Func flush) => Pipeline(() => ctx.LRangeAsync(_listKey, 0, 99), flush); + private Task LRange100(RespContext ctx, Func flush) => Pipeline(() => ctx.LRangeAsync(_listKey, 0, 99), flush); [DisplayName("LRANGE_300")] - private Task LRange300(RespContext ctx, Func flush) => Pipeline(() => ctx.LRangeAsync(_listKey, 0, 299), flush); + private Task LRange300(RespContext ctx, Func flush) => Pipeline(() => ctx.LRangeAsync(_listKey, 0, 299), flush); [DisplayName("LRANGE_500")] - private Task LRange500(RespContext ctx, Func flush) => Pipeline(() => ctx.LRangeAsync(_listKey, 0, 499), flush); + private Task LRange500(RespContext ctx, Func flush) => Pipeline(() => ctx.LRangeAsync(_listKey, 0, 499), flush); [DisplayName("LRANGE_600")] - private Task LRange600(RespContext ctx, Func flush) => Pipeline(() => ctx.LRangeAsync(_listKey, 0, 599), flush); + private Task LRange600(RespContext ctx, Func flush) => Pipeline(() => ctx.LRangeAsync(_listKey, 0, 599), flush); [DisplayName("LPOP")] - private Task LPop(RespContext ctx, Func flush) => Pipeline(() => ctx.LPopAsync(_listKey), flush); + private Task LPop(RespContext ctx, Func flush) => Pipeline(() => ctx.LPopAsync(_listKey), flush); [DisplayName("RPOP")] - private Task RPop(RespContext ctx, Func flush) => Pipeline(() => ctx.RPopAsync(_listKey), flush); + private Task RPop(RespContext ctx, Func flush) => Pipeline(() => ctx.RPopAsync(_listKey), flush); private Task LPopInit(RespContext ctx) => ctx.LPushAsync(_listKey, _payload, TotalOperations).AsTask(); @@ -178,7 +177,7 @@ private Task HSet(RespContext ctx, Func flush) => private Task ZAdd(RespContext ctx, Func flush) => Pipeline(() => ctx.ZAddAsync(_sortedSetKey, 0, "element:__rand_int__"), flush); [DisplayName("ZPOPMIN")] - private Task ZPopMin(RespContext ctx, Func flush) => Pipeline(() => ctx.ZPopMinAsync(_sortedSetKey), flush); + private Task ZPopMin(RespContext ctx, Func flush) => Pipeline(() => ctx.ZPopMinAsync(_sortedSetKey), flush); private async Task ZPopMinInit(RespContext ctx) { @@ -192,7 +191,7 @@ await ctx.ZAddAsync(_sortedSetKey, (rand.NextDouble() * 2000) - 1000, "element:_ } [DisplayName("SPOP")] - private Task SPop(RespContext ctx, Func flush) => Pipeline(() => ctx.SPopAsync(_setKey), flush); + private Task SPop(RespContext ctx, Func flush) => Pipeline(() => ctx.SPopAsync(_setKey), flush); private async Task SPopInit(RespContext ctx) { @@ -204,28 +203,28 @@ private async Task SPopInit(RespContext ctx) } [DisplayName("MSET"), Description("10 keys")] - private Task MSet(RespContext ctx, Func flush) => Pipeline(() => ctx.MSetAsync(_pairs), flush); + private Task MSet(RespContext ctx, Func flush) => Pipeline(() => ctx.MSetAsync(_pairs), flush); private Task LRangeInit(RespContext ctx) => ctx.LPushAsync(_listKey, _payload, TotalOperations).AsTask(); [DisplayName("XADD")] - private Task XAdd(RespContext ctx, Func flush) => + private Task XAdd(RespContext ctx, Func flush) => Pipeline(() => ctx.XAddAsync(_streamKey, "*", "myfield", _payload), flush); } internal static partial class RedisCommands { [RespCommand] - internal static partial ResponseSummary Ping(this in RespContext ctx); + internal static partial RespParsers.ResponseSummary Ping(this in RespContext ctx); [RespCommand] - internal static partial ResponseSummary SPop(this in RespContext ctx, string key); + internal static partial RespParsers.ResponseSummary SPop(this in RespContext ctx, string key); [RespCommand] internal static partial int SAdd(this in RespContext ctx, string key, string payload); [RespCommand] - internal static partial ResponseSummary Set(this in RespContext ctx, string key, byte[] payload); + internal static partial RespParsers.ResponseSummary Set(this in RespContext ctx, string key, byte[] payload); [RespCommand] internal static partial int LPush(this in RespContext ctx, string key, byte[] payload); @@ -256,34 +255,34 @@ public void Format( internal static partial int RPush(this in RespContext ctx, string key, byte[] payload); [RespCommand] - internal static partial ResponseSummary LPop(this in RespContext ctx, string key); + internal static partial RespParsers.ResponseSummary LPop(this in RespContext ctx, string key); [RespCommand] - internal static partial ResponseSummary RPop(this in RespContext ctx, string key); + internal static partial RespParsers.ResponseSummary RPop(this in RespContext ctx, string key); [RespCommand] - internal static partial ResponseSummary LRange(this in RespContext ctx, string key, int start, int stop); + internal static partial RespParsers.ResponseSummary LRange(this in RespContext ctx, string key, int start, int stop); [RespCommand] internal static partial int HSet(this in RespContext ctx, string key, string field, byte[] payload); [RespCommand] - internal static partial ResponseSummary Ping(this in RespContext ctx, byte[] payload); + internal static partial RespParsers.ResponseSummary Ping(this in RespContext ctx, byte[] payload); [RespCommand] internal static partial int Incr(this in RespContext ctx, string key); [RespCommand] - internal static partial ResponseSummary Del(this in RespContext ctx, string key); + internal static partial RespParsers.ResponseSummary Del(this in RespContext ctx, string key); [RespCommand] - internal static partial ResponseSummary ZPopMin(this in RespContext ctx, string key); + internal static partial RespParsers.ResponseSummary ZPopMin(this in RespContext ctx, string key); [RespCommand] internal static partial int ZAdd(this in RespContext ctx, string key, double score, string payload); [RespCommand] - internal static partial ResponseSummary XAdd( + internal static partial RespParsers.ResponseSummary XAdd( this in RespContext ctx, string key, string id, @@ -291,17 +290,17 @@ internal static partial ResponseSummary XAdd( byte[] value); [RespCommand] - internal static partial ResponseSummary Get(this in RespContext ctx, string key); + internal static partial RespParsers.ResponseSummary Get(this in RespContext ctx, string key); [RespCommand(Formatter = "PairsFormatter.Instance")] // custom command formatter - internal static partial void MSet(this in RespContext ctx, (string, byte[])[] pairs); + internal static partial bool MSet(this in RespContext ctx, (string, byte[])[] pairs); - internal static ResponseSummary PingInline(this in RespContext ctx, byte[] payload) - => ctx.Command("ping"u8, payload, InlinePingFormatter.Instance).Wait(ResponseSummary.Parser); + internal static RespParsers.ResponseSummary PingInline(this in RespContext ctx, byte[] payload) + => ctx.Command("ping"u8, payload, InlinePingFormatter.Instance).Wait(RespParsers.ResponseSummary.Parser); - internal static ValueTask PingInlineAsync(this in global::Resp.RespContext ctx, byte[] payload) + internal static ValueTask PingInlineAsync(this in RespContext ctx, byte[] payload) => ctx.Command("ping"u8, payload, InlinePingFormatter.Instance) - .AsValueTask(ResponseSummary.Parser); + .Send(RespParsers.ResponseSummary.Parser); private sealed class InlinePingFormatter : IRespFormatter { @@ -335,4 +334,3 @@ public void Format( } } } -#endif diff --git a/tests/BasicTest/OldCoreBenchmark.cs b/src/RESPite.Benchmark/OldCoreBenchmark.cs similarity index 99% rename from tests/BasicTest/OldCoreBenchmark.cs rename to src/RESPite.Benchmark/OldCoreBenchmark.cs index d6e15c2cc..6cea82fad 100644 --- a/tests/BasicTest/OldCoreBenchmark.cs +++ b/src/RESPite.Benchmark/OldCoreBenchmark.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using StackExchange.Redis; -namespace BasicTest; +namespace RESPite.Benchmark; public class OldCoreBenchmark : BenchmarkBase { diff --git a/src/RESPite.Benchmark/Program.cs b/src/RESPite.Benchmark/Program.cs new file mode 100644 index 000000000..694c09a11 --- /dev/null +++ b/src/RESPite.Benchmark/Program.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace RESPite.Benchmark; + +internal static class Program +{ + private static async Task Main(string[] args) + { + try + { + List benchmarks = []; + foreach (var arg in args) + { + switch (arg) + { + case "--old": + benchmarks.Add(new OldCoreBenchmark(args)); + break; + + case "--new": + benchmarks.Add(new NewCoreBenchmark(args)); + break; + } + } + + if (benchmarks.Count == 0) + { + benchmarks.Add(new NewCoreBenchmark(args)); + } + + do + { + foreach (var bench in benchmarks) + { + if (benchmarks.Count > 1) + { + Console.WriteLine($"### {bench} ###"); + } + + await bench.RunAll().ConfigureAwait(false); + } + } + // ReSharper disable once LoopVariableIsNeverChangedInsideLoop + while (benchmarks[0].Loop); + + return 0; + } + catch (Exception ex) + { + WriteException(ex); + return -1; + } + } + + internal static void WriteException(Exception? ex) + { + while (ex is not null) + { + Console.Error.WriteLine(); + Console.Error.WriteLine($"{ex.GetType().Name}: {ex.Message}"); + Console.Error.WriteLine($"\t{ex.StackTrace}"); + var data = ex.Data; + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (data is not null) + { + foreach (var key in data.Keys) + { + Console.Error.WriteLine($"\t{key}: {data[key]}"); + } + } + + ex = ex.InnerException; + } + } +} diff --git a/src/RESPite.Benchmark/RESPite.Benchmark.csproj b/src/RESPite.Benchmark/RESPite.Benchmark.csproj new file mode 100644 index 000000000..a12a9ec55 --- /dev/null +++ b/src/RESPite.Benchmark/RESPite.Benchmark.csproj @@ -0,0 +1,25 @@ + + + + enable + + Exe + net8.0;net9.0 + resp-benchmark + true + command-line "RESP" benchmark client, comparable to redis-benchmark + True + readme.md + False + True + false + false + LatestMajor + + + + + + + + diff --git a/tests/BasicTest/RespBenchmark.md b/src/RESPite.Benchmark/RespBenchmark.md similarity index 100% rename from tests/BasicTest/RespBenchmark.md rename to src/RESPite.Benchmark/RespBenchmark.md diff --git a/src/RESPite/Connections/BatchConnection.cs b/src/RESPite/Connections/Internal/BatchConnection.cs similarity index 99% rename from src/RESPite/Connections/BatchConnection.cs rename to src/RESPite/Connections/Internal/BatchConnection.cs index 3d8c11f42..999c21d79 100644 --- a/src/RESPite/Connections/BatchConnection.cs +++ b/src/RESPite/Connections/Internal/BatchConnection.cs @@ -1,7 +1,7 @@ using System.Buffers; using System.Runtime.InteropServices; -namespace RESPite.Connections; +namespace RESPite.Connections.Internal; internal sealed class BatchConnection : IBatchConnection { diff --git a/src/RESPite/Connections/Internal/ConfiguredConnection.cs b/src/RESPite/Connections/Internal/ConfiguredConnection.cs new file mode 100644 index 000000000..5a5491c01 --- /dev/null +++ b/src/RESPite/Connections/Internal/ConfiguredConnection.cs @@ -0,0 +1,35 @@ +namespace RESPite.Connections.Internal; + +internal sealed class ConfiguredConnection : IRespConnection +{ + private readonly IRespConnection _tail; + private readonly RespConfiguration _configuration; + private readonly RespContext _context; + + public ref readonly RespContext Context => ref _context; + + public ConfiguredConnection(in RespContext tail, RespConfiguration configuration) + { + _tail = tail.Connection; + _configuration = configuration; + _context = tail.WithConnection(this); + } + + public void Dispose() => _tail.Dispose(); + + public ValueTask DisposeAsync() => _tail.DisposeAsync(); + + public RespConfiguration Configuration => _configuration; + + public bool CanWrite => _tail.CanWrite; + + public int Outstanding => _tail.Outstanding; + + public void Send(in RespOperation message) => _tail.Send(message); + public void Send(ReadOnlySpan messages) => _tail.Send(messages); + + public Task SendAsync(in RespOperation message) => + _tail.SendAsync(message); + + public Task SendAsync(ReadOnlyMemory messages) => _tail.SendAsync(messages); +} diff --git a/src/RESPite/Connections/Internal/NullConnection.cs b/src/RESPite/Connections/Internal/NullConnection.cs new file mode 100644 index 000000000..0ed166f16 --- /dev/null +++ b/src/RESPite/Connections/Internal/NullConnection.cs @@ -0,0 +1,37 @@ +namespace RESPite.Connections.Internal; + +internal sealed class NullConnection : IRespConnection +{ + private readonly RespContext _context; + + public static NullConnection WithConfiguration(RespConfiguration configuration) + => ReferenceEquals(configuration, RespConfiguration.Default) + ? Instance + : new(configuration); + + private NullConnection(RespConfiguration configuration) + { + _context = RespContext.For(this); + Configuration = configuration; + } + + public static readonly NullConnection Instance = new(RespConfiguration.Default); + public void Dispose() { } + + public ValueTask DisposeAsync() => default; + + public RespConfiguration Configuration { get; } + public bool CanWrite => false; + public int Outstanding => 0; + + public ref readonly RespContext Context => ref _context; + + private const string SendErrorMessage = "Null connections do not support sending messages."; + public void Send(in RespOperation message) => throw new NotSupportedException(SendErrorMessage); + + public void Send(ReadOnlySpan message) => throw new NotSupportedException(SendErrorMessage); + + public Task SendAsync(in RespOperation message) => throw new NotSupportedException(SendErrorMessage); + + public Task SendAsync(ReadOnlyMemory message) => throw new NotSupportedException(SendErrorMessage); +} diff --git a/src/RESPite/Connections/Internal/SynchronizedConnection.cs b/src/RESPite/Connections/Internal/SynchronizedConnection.cs new file mode 100644 index 000000000..5aa65d5ef --- /dev/null +++ b/src/RESPite/Connections/Internal/SynchronizedConnection.cs @@ -0,0 +1,219 @@ +using RESPite.Internal; + +namespace RESPite.Connections.Internal; + +internal sealed class SynchronizedConnection : IRespConnection +{ + private readonly IRespConnection _tail; + private readonly RespContext _context; + private readonly SemaphoreSlim _semaphore = new(1); + + public ref readonly RespContext Context => ref _context; + + public SynchronizedConnection(in RespContext tail) + { + _tail = tail.Connection; + _context = tail.WithConnection(this); + } + + public void Dispose() + { + _semaphore.Dispose(); + _tail.Dispose(); + } + + public ValueTask DisposeAsync() + { + _semaphore.Dispose(); + return _tail.DisposeAsync(); + } + + public RespConfiguration Configuration => _tail.Configuration; + public bool CanWrite => _semaphore.CurrentCount > 0 && _tail.CanWrite; + public int Outstanding => _tail.Outstanding; + + public void Send(in RespOperation message) + { + _semaphore.Wait(message.CancellationToken); + try + { + _tail.Send(message); + } + catch (Exception ex) + { + message.Message.TrySetException(message.Token, ex); + throw; + } + finally + { + _semaphore.Release(); + } + } + + public void Send(ReadOnlySpan messages) + { + switch (messages.Length) + { + case 0: return; + case 1: + Send(messages[0]); + return; + } + + _semaphore.Wait(messages[0].CancellationToken); + try + { + _tail.Send(messages); + } + catch (Exception ex) + { + TrySetException(messages, ex); + throw; + } + finally + { + _semaphore.Release(); + } + } + + public Task SendAsync(in RespOperation message) + { + bool haveLock = false; + try + { + haveLock = _semaphore.Wait(0); + if (!haveLock) + { + DebugCounters.OnPipelineFullAsync(); + return FullAsync(this, message); + } + + var pending = _tail.SendAsync(message); + if (!pending.IsCompleted) + { + DebugCounters.OnPipelineSendAsync(); + haveLock = false; // transferring + return AwaitAndReleaseLock(pending); + } + + DebugCounters.OnPipelineFullSync(); + pending.GetAwaiter().GetResult(); + return Task.CompletedTask; + } + catch (Exception ex) + { + message.Message.TrySetException(message.Token, ex); + throw; + } + finally + { + if (haveLock) _semaphore.Release(); + } + + static async Task FullAsync(SynchronizedConnection @this, RespOperation message) + { + try + { + await @this._semaphore.WaitAsync(message.CancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + message.Message.TrySetException(message.Token, ex); + throw; + } + + try + { + await @this._tail.SendAsync(message).ConfigureAwait(false); + } + finally + { + @this._semaphore.Release(); + } + } + } + + private async Task AwaitAndReleaseLock(Task pending) + { + try + { + await pending.ConfigureAwait(false); + } + finally + { + _semaphore.Release(); + } + } + + private static void TrySetException(ReadOnlySpan messages, Exception ex) + { + foreach (var message in messages) + { + message.Message.TrySetException(message.Token, ex); + } + } + + public Task SendAsync(ReadOnlyMemory messages) + { + switch (messages.Length) + { + case 0: return Task.CompletedTask; + case 1: return SendAsync(messages.Span[0]); + } + + bool haveLock = false; + try + { + haveLock = _semaphore.Wait(0); + if (!haveLock) + { + DebugCounters.OnPipelineFullAsync(); + return FullAsync(this, messages); + } + + var pending = _tail.SendAsync(messages); + if (!pending.IsCompleted) + { + DebugCounters.OnPipelineSendAsync(); + haveLock = false; // transferring + return AwaitAndReleaseLock(pending); + } + + DebugCounters.OnPipelineFullSync(); + pending.GetAwaiter().GetResult(); + return Task.CompletedTask; + } + catch (Exception ex) + { + TrySetException(messages.Span, ex); + throw; + } + finally + { + if (haveLock) _semaphore.Release(); + } + + static async Task FullAsync(SynchronizedConnection @this, ReadOnlyMemory messages) + { + bool haveLock = false; // we don't have the lock initially + try + { + await @this._semaphore.WaitAsync(messages.Span[0].CancellationToken).ConfigureAwait(false); + haveLock = true; + await @this._tail.SendAsync(messages).ConfigureAwait(false); + } + catch (Exception ex) + { + TrySetException(messages.Span, ex); + throw; + } + finally + { + if (haveLock) + { + @this._semaphore.Release(); + } + } + } + } +} diff --git a/src/RESPite/Connections/RespConnectionExtensions.cs b/src/RESPite/Connections/RespConnectionExtensions.cs new file mode 100644 index 000000000..8ea38a6e4 --- /dev/null +++ b/src/RESPite/Connections/RespConnectionExtensions.cs @@ -0,0 +1,17 @@ +using RESPite.Connections.Internal; + +namespace RESPite.Connections; + +public static class RespConnectionExtensions +{ + /// + /// Enforces stricter ordering guarantees, so that unawaited async operations cannot cause overlapping writes. + /// + public static IRespConnection Synchronized(this IRespConnection connection) + => connection is SynchronizedConnection ? connection : new SynchronizedConnection(in connection.Context); + + public static IRespConnection WithConfiguration(this IRespConnection connection, RespConfiguration configuration) + => ReferenceEquals(configuration, connection.Configuration) + ? connection + : new ConfiguredConnection(in connection.Context, configuration); +} diff --git a/src/RESPite/Connections/RespConnectionPool.cs b/src/RESPite/Connections/RespConnectionPool.cs new file mode 100644 index 000000000..e78f4b4ce --- /dev/null +++ b/src/RESPite/Connections/RespConnectionPool.cs @@ -0,0 +1,194 @@ +using System.Collections.Concurrent; +using System.ComponentModel; +using System.Net; +using System.Net.Sockets; +using RESPite.Connections.Internal; +using RESPite.Internal; + +namespace RESPite.Connections; + +public sealed class RespConnectionPool : IDisposable +{ + private const int DefaultCount = 10; + private bool _isDisposed; + + [Obsolete("This is for testing only")] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public bool UseCustomNetworkStream { get; set; } + + private readonly ConcurrentQueue _pool = []; + private readonly Func _createConnection; + private readonly int _count; + private readonly RespContext _defaultTemplate; + + public RespConnectionPool( + in RespContext template, + Func createConnection, + int count = DefaultCount) + { + _createConnection = createConnection; + _count = count; + template.CancellationToken.ThrowIfCancellationRequested(); + // swap out the connection for a dummy (retaining the configuration) + var configuredConnection = NullConnection.WithConfiguration(template.Connection.Configuration); + _defaultTemplate = template.WithConnection(configuredConnection); + } + + public RespConnectionPool( + Func createConnection, + int count = DefaultCount) : this(RespContext.Null, createConnection, count) + { + } + + public RespConnectionPool( + in RespContext template, + IPAddress? address = null, + int port = 6379, + int count = DefaultCount) + : this(in template, new IPEndPoint(address ?? IPAddress.Loopback, port), count) + { + } + + public RespConnectionPool( + IPAddress? address = null, + int port = 6379, + int count = DefaultCount) : this(RespContext.Null, address, port, count) + { + } + + public RespConnectionPool(EndPoint endPoint, int count = DefaultCount) + : this(RespContext.Null, endPoint, count) + { + } + + public RespConnectionPool(in RespContext template, EndPoint endPoint, int count = DefaultCount) + : this(template, config => CreateConnection(config, endPoint), count) + { + } + + /// + /// Borrow a connection from the pool, using the default template. + /// + public IRespConnection GetConnection() => GetConnection(in _defaultTemplate); + + /// + /// Borrow a connection from the pool. + /// + /// The template context to use for the leased connection; everything except the connection + /// will be inherited by the new context. + public IRespConnection GetConnection(in RespContext template) + { + ThrowIfDisposed(); + template.CancellationToken.ThrowIfCancellationRequested(); + + if (!_pool.TryDequeue(out var connection)) + { + connection = _createConnection(template.Connection.Configuration); + } + + return new PoolWrapper(this, connection, in template); + } + + private void ThrowIfDisposed() + { + if (_isDisposed) Throw(); + static void Throw() => throw new ObjectDisposedException(nameof(RespConnectionPool)); + } + + public void Dispose() + { + _isDisposed = true; + while (_pool.TryDequeue(out var connection)) + { + connection.Dispose(); + } + } + + private void Return(IRespConnection tail) + { + if (_isDisposed || !tail.CanWrite || _pool.Count >= _count) + { + tail.Dispose(); + } + else + { + _pool.Enqueue(tail); + } + } + + private static IRespConnection CreateConnection(RespConfiguration config, EndPoint endpoint) + { + Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.NoDelay = true; + socket.Connect(endpoint); + return new StreamConnection(config, new NetworkStream(socket)); + } + + private sealed class PoolWrapper : IRespConnection + { + private bool _isDisposed; + private readonly RespConnectionPool _pool; + private readonly IRespConnection _tail; + private readonly RespContext _context; + + public ref readonly RespContext Context => ref _context; + + public PoolWrapper( + RespConnectionPool pool, + IRespConnection tail, + in RespContext template) + { + _pool = pool; + _tail = tail; + _context = template.WithConnection(this); + } + + public void Dispose() + { + _isDisposed = true; + _pool.Return(_tail); + } + + public bool CanWrite => !_isDisposed && _tail.CanWrite; + + public int Outstanding => _tail.Outstanding; + + public RespConfiguration Configuration => _tail.Configuration; + + private void ThrowIfDisposed() + { + if (_isDisposed) Throw(); + static void Throw() => throw new ObjectDisposedException(nameof(PoolWrapper)); + } + + public ValueTask DisposeAsync() + { + Dispose(); + return default; + } + + public void Send(in RespOperation message) + { + ThrowIfDisposed(); + _tail.Send(message); + } + + public void Send(ReadOnlySpan messages) + { + ThrowIfDisposed(); + _tail.Send(messages); + } + + public Task SendAsync(in RespOperation message) + { + ThrowIfDisposed(); + return _tail.SendAsync(message); + } + + public Task SendAsync(ReadOnlyMemory messages) + { + ThrowIfDisposed(); + return _tail.SendAsync(messages); + } + } +} diff --git a/src/RESPite/Internal/IRespMessage.cs b/src/RESPite/Internal/IRespMessage.cs index 4632c45c4..43bc4ff2b 100644 --- a/src/RESPite/Internal/IRespMessage.cs +++ b/src/RESPite/Internal/IRespMessage.cs @@ -15,5 +15,6 @@ internal interface IRespMessage : IValueTaskSource void ReleaseRequest(); bool AllowInlineParsing { get; } short Token { get; } + ref readonly CancellationToken CancellationToken { get; } void OnSent(short token); } diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs index 801166b19..e90a75450 100644 --- a/src/RESPite/Internal/RespMessageBase.cs +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -17,6 +17,7 @@ internal abstract class RespMessageBase : IRespMessage, IValueTaskSou private int _requestRefCount; private int _flags; private ManualResetValueTaskSourceCore _asyncCore; + public ref readonly CancellationToken CancellationToken => ref _cancellationToken; private const int Flag_Sent = 1 << 0, // the request has been sent diff --git a/src/RESPite/Messages/IRespFormatterT.cs b/src/RESPite/Messages/IRespFormatterT.cs new file mode 100644 index 000000000..9857f3add --- /dev/null +++ b/src/RESPite/Messages/IRespFormatterT.cs @@ -0,0 +1,19 @@ +namespace RESPite.Messages; + +public interface IRespFormatter +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif +{ + void Format(scoped ReadOnlySpan command, ref RespWriter writer, in TRequest request); +} + +/* +public interface IRespSizeEstimator : IRespFormatter +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif +{ + int EstimateSize(scoped ReadOnlySpan command, in TRequest request); +} +*/ diff --git a/src/RESPite/Messages/RespWriter.cs b/src/RESPite/Messages/RespWriter.cs new file mode 100644 index 000000000..cc66aa4f9 --- /dev/null +++ b/src/RESPite/Messages/RespWriter.cs @@ -0,0 +1,914 @@ +using System; +using System.Buffers; +using System.Buffers.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using RESPite.Internal; + +namespace RESPite.Messages; + +/// +/// Provides low-level RESP formatting operations. +/// +public ref struct RespWriter +{ + private readonly IBufferWriter? _target; + + [SuppressMessage("Style", "IDE0032:Use auto property", Justification = "Clarity")] + private int _index; + + internal readonly int IndexInCurrentBuffer => _index; + +#if NET7_0_OR_GREATER + private ref byte StartOfBuffer; + private int BufferLength; + + private ref byte WriteHead + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.Add(ref StartOfBuffer, _index); + } + + private Span Tail + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => MemoryMarshal.CreateSpan(ref Unsafe.Add(ref StartOfBuffer, _index), BufferLength - _index); + } + + private void WriteRawUnsafe(byte value) => Unsafe.Add(ref StartOfBuffer, _index++) = value; + + private readonly ReadOnlySpan WrittenLocalBuffer => + MemoryMarshal.CreateReadOnlySpan(ref StartOfBuffer, _index); +#else + private Span _buffer; + private readonly int BufferLength => _buffer.Length; + + private readonly ref byte StartOfBuffer + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref MemoryMarshal.GetReference(_buffer); + } + + private readonly ref byte WriteHead + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.Add(ref MemoryMarshal.GetReference(_buffer), _index); + } + + private readonly Span Tail => _buffer.Slice(_index); + private void WriteRawUnsafe(byte value) => _buffer[_index++] = value; + + private readonly ReadOnlySpan WrittenLocalBuffer => _buffer.Slice(0, _index); +#endif + + internal readonly string DebugBuffer() => RespConstants.UTF8.GetString(WrittenLocalBuffer); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteCrLfUnsafe() + { + Unsafe.WriteUnaligned(ref WriteHead, RespConstants.CrLfUInt16); + _index += 2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteCrLf() + { + if (Available >= 2) + { + Unsafe.WriteUnaligned(ref WriteHead, RespConstants.CrLfUInt16); + _index += 2; + } + else + { + WriteRaw(RespConstants.CrlfBytes); + } + } + + private readonly int Available + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => BufferLength - _index; + } + + /// + /// Create a new RESP writer over the provided target. + /// + public RespWriter(IBufferWriter target) + { + _target = target; + _index = 0; +#if NET7_0_OR_GREATER + StartOfBuffer = ref Unsafe.NullRef(); + BufferLength = 0; +#else + _buffer = default; +#endif + GetBuffer(); + } + + /// + /// Create a new RESP writer over the provided target. + /// + public RespWriter(Span target) + { + _index = 0; +#if NET7_0_OR_GREATER + BufferLength = target.Length; + StartOfBuffer = ref MemoryMarshal.GetReference(target); +#else + _buffer = target; +#endif + } + + /// + /// Commits any unwritten bytes to the output. + /// + public void Flush() + { + if (_index != 0 && _target is not null) + { + _target.Advance(_index); +#if NET7_0_OR_GREATER + _index = BufferLength = 0; + StartOfBuffer = ref Unsafe.NullRef(); +#else + _index = 0; + _buffer = default; +#endif + } + } + + private void FlushAndGetBuffer(int sizeHint) + { + Flush(); + GetBuffer(sizeHint); + } + + private void GetBuffer(int sizeHint = 128) + { + if (Available == 0) + { + if (_target is null) + { + ThrowFixedBufferExceeded(); + } + else + { + const int MIN_BUFFER = 1024; + _index = 0; +#if NET7_0_OR_GREATER + var span = _target.GetSpan(Math.Max(sizeHint, MIN_BUFFER)); + BufferLength = span.Length; + StartOfBuffer = ref MemoryMarshal.GetReference(span); +#else + _buffer = _target.GetSpan(Math.Max(sizeHint, MIN_BUFFER)); +#endif + ActivationHelper.DebugBreakIf(Available == 0); + } + } + } + + [DoesNotReturn, MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowFixedBufferExceeded() => + throw new InvalidOperationException("Fixed buffer cannot be expanded"); + + /// + /// Write raw RESP data to the output; no validation will occur. + /// + public void WriteRaw(scoped ReadOnlySpan buffer) + { + const int MAX_TO_DOUBLE_BUFFER = 128; + if (buffer.Length <= MAX_TO_DOUBLE_BUFFER && buffer.Length <= Available) + { + buffer.CopyTo(Tail); + _index += buffer.Length; + } + else + { + // write directly to the output + Flush(); + if (_target is null) + { + ThrowFixedBufferExceeded(); + } + else + { + _target.Write(buffer); + } + } + } + + public RespCommandMap? CommandMap { get; set; } + + /// + /// Write a command header. + /// + /// The command name to write. + /// The number of arguments for the command (excluding the command itself). + public void WriteCommand(scoped ReadOnlySpan command, int args) + { + if (args < 0) Throw(); + WritePrefixedInteger(RespPrefix.Array, args + 1); + if (command.IsEmpty) ThrowEmptyCommand(); + if (CommandMap is { } map) + { + var mapped = map.Map(command); + if (mapped.IsEmpty) ThrowCommandUnavailable(command); + command = mapped; + } + + WriteBulkString(command); + + static void Throw() => throw new ArgumentOutOfRangeException(nameof(args)); + + static void ThrowEmptyCommand() => + throw new ArgumentException(paramName: nameof(command), message: "Empty command specified."); + + static void ThrowCommandUnavailable(ReadOnlySpan command) + => throw new ArgumentException( + paramName: nameof(command), + message: $"The command {Encoding.UTF8.GetString(command)} is not available."); + } + + /// + /// Write a key as a bulk string. + /// + /// The key to write. + public void WriteKey(scoped ReadOnlySpan value) => WriteBulkString(value); + + /// + /// Write a key as a bulk string. + /// + /// The key to write. + public void WriteKey(ReadOnlyMemory value) => WriteBulkString(value.Span); + + /// + /// Write a key as a bulk string. + /// + /// The key to write. + public void WriteKey(scoped ReadOnlySpan value) => WriteBulkString(value); + + /// + /// Write a key as a bulk string. + /// + /// The key to write. + public void WriteKey(ReadOnlyMemory value) => WriteBulkString(value.Span); + + /// + /// Write a key as a bulk string. + /// + /// The key to write. + public void WriteKey(string value) => WriteBulkString(value); + + /// + /// Write a key as a bulk string. + /// + /// The key to write. + public void WriteKey(byte[] value) => WriteBulkString(value.AsSpan()); + + /// + /// Write a payload as a bulk string. + /// + /// The payload to write. + public void WriteBulkString(byte[] value) => WriteBulkString(value.AsSpan()); + + /// + /// Write a payload as a bulk string. + /// + /// The payload to write. + public void WriteBulkString(ReadOnlyMemory value) + => WriteBulkString(value.Span); + + /// + /// Write a payload as a bulk string. + /// + /// The payload to write. + public void WriteBulkString(scoped ReadOnlySpan value) + { + if (value.IsEmpty) + { + if (Available >= 6) + { + WriteRawPrechecked(Raw.BulkStringEmpty_6, 6); + } + else + { + WriteRaw("$0\r\n\r\n"u8); + } + } + else + { + WriteBulkStringHeader(value.Length); + if (Available >= value.Length + 2) + { + value.CopyTo(Tail); + _index += value.Length; + WriteCrLfUnsafe(); + } + else + { + // slow path + WriteRaw(value); + WriteCrLf(); + } + } + } + + /* + /// + /// Write a payload as a bulk string. + /// + /// The payload to write. + public void WriteBulkString(in SimpleString value) + { + if (value.IsEmpty) + { + WriteRaw("$0\r\n\r\n"u8); + } + else if (value.TryGetBytes(span: out var bytes)) + { + WriteBulkString(bytes); + } + else if (value.TryGetChars(span: out var chars)) + { + WriteBulkString(chars); + } + else if (value.TryGetBytes(sequence: out var bytesSeq)) + { + WriteBulkString(bytesSeq); + } + else if (value.TryGetChars(sequence: out var charsSeq)) + { + WriteBulkString(charsSeq); + } + else + { + Throw(); + } + + static void Throw() => throw new InvalidOperationException($"It was not possible to read the {nameof(SimpleString)} contents"); + } + */ + + /// + /// Write an integer as a bulk string. + /// + public void WriteBulkString(bool value) => WriteBulkString(value ? 1 : 0); + + /// + /// Write a floating point as a bulk string. + /// + public void WriteBulkString(double value) + { + if (value == 0.0 | double.IsNaN(value) | double.IsInfinity(value)) + { + WriteKnownDouble(ref this, value); + + static void WriteKnownDouble(ref RespWriter writer, double value) + { + if (value == 0.0) + { + writer.WriteRaw("$1\r\n0\r\n"u8); + } + else if (double.IsNaN(value)) + { + writer.WriteRaw("$3\r\nnan\r\n"u8); + } + else if (double.IsPositiveInfinity(value)) + { + writer.WriteRaw("$3\r\ninf\r\n"u8); + } + else if (double.IsNegativeInfinity(value)) + { + writer.WriteRaw("$4\r\n-inf\r\n"u8); + } + else + { + Throw(); + static void Throw() => throw new ArgumentOutOfRangeException(nameof(value)); + } + } + } + else + { + Debug.Assert(RespConstants.MaxProtocolBytesBytesNumber <= 32); + Span scratch = stackalloc byte[24]; + if (!Utf8Formatter.TryFormat(value, scratch, out int bytes, G17)) + ThrowFormatException(); + WritePrefixedInteger(RespPrefix.BulkString, bytes); + WriteRaw(scratch.Slice(0, bytes)); + WriteCrLf(); + } + } + + private static readonly StandardFormat G17 = new('G', 17); + + /// + /// Write an integer as a bulk string. + /// + public void WriteBulkString(long value) + { + if (value >= -1 & value <= 20) + { + WriteRaw(value switch + { + -1 => "$2\r\n-1\r\n"u8, + 0 => "$1\r\n0\r\n"u8, + 1 => "$1\r\n1\r\n"u8, + 2 => "$1\r\n2\r\n"u8, + 3 => "$1\r\n3\r\n"u8, + 4 => "$1\r\n4\r\n"u8, + 5 => "$1\r\n5\r\n"u8, + 6 => "$1\r\n6\r\n"u8, + 7 => "$1\r\n7\r\n"u8, + 8 => "$1\r\n8\r\n"u8, + 9 => "$1\r\n9\r\n"u8, + 10 => "$2\r\n10\r\n"u8, + 11 => "$2\r\n11\r\n"u8, + 12 => "$2\r\n12\r\n"u8, + 13 => "$2\r\n13\r\n"u8, + 14 => "$2\r\n14\r\n"u8, + 15 => "$2\r\n15\r\n"u8, + 16 => "$2\r\n16\r\n"u8, + 17 => "$2\r\n17\r\n"u8, + 18 => "$2\r\n18\r\n"u8, + 19 => "$2\r\n19\r\n"u8, + 20 => "$2\r\n20\r\n"u8, + _ => Throw(), + }); + + static ReadOnlySpan Throw() => throw new ArgumentOutOfRangeException(nameof(value)); + } + else if (Available >= RespConstants.MaxProtocolBytesBulkStringIntegerInt64) + { + var singleDigit = value >= -99_999_999 && value <= 999_999_999; + WriteRawUnsafe((byte)RespPrefix.BulkString); + + var target = Tail.Slice(singleDigit ? 3 : 4); // N\r\n or NN\r\n + if (!Utf8Formatter.TryFormat(value, target, out var valueBytes)) + ThrowFormatException(); + + Debug.Assert(valueBytes > 0 && singleDigit ? valueBytes < 10 : valueBytes is 10 or 11); + if (!Utf8Formatter.TryFormat(valueBytes, Tail, out var prefixBytes)) + ThrowFormatException(); + Debug.Assert(prefixBytes == (singleDigit ? 1 : 2)); + _index += prefixBytes; + WriteCrLfUnsafe(); + _index += valueBytes; + WriteCrLfUnsafe(); + } + else + { + Debug.Assert(RespConstants.MaxRawBytesInt64 <= 24); + Span scratch = stackalloc byte[24]; + if (!Utf8Formatter.TryFormat(value, scratch, out int bytes)) + ThrowFormatException(); + WritePrefixedInteger(RespPrefix.BulkString, bytes); + WriteRaw(scratch.Slice(0, bytes)); + WriteCrLf(); + } + } + + private static void ThrowFormatException() => throw new FormatException(); + + private void WritePrefixedInteger(RespPrefix prefix, int length) + { + if (Available >= RespConstants.MaxProtocolBytesIntegerInt32) + { + WriteRawUnsafe((byte)prefix); + if (length >= 0 & length <= 9) + { + WriteRawUnsafe((byte)(length + '0')); + } + else + { + if (!Utf8Formatter.TryFormat(length, Tail, out var bytesWritten)) + { + ThrowFormatException(); + } + + _index += bytesWritten; + } + + WriteCrLfUnsafe(); + } + else + { + WriteViaStack(ref this, prefix, length); + } + + static void WriteViaStack(ref RespWriter respWriter, RespPrefix prefix, int length) + { + Debug.Assert(RespConstants.MaxProtocolBytesIntegerInt32 <= 16); + Span buffer = stackalloc byte[16]; + buffer[0] = (byte)prefix; + int payloadLength; + if (length >= 0 & length <= 9) + { + buffer[1] = (byte)(length + '0'); + payloadLength = 1; + } + else if (!Utf8Formatter.TryFormat(length, buffer.Slice(1), out payloadLength)) + { + ThrowFormatException(); + } + + Unsafe.WriteUnaligned(ref buffer[payloadLength + 1], RespConstants.CrLfUInt16); + respWriter.WriteRaw(buffer.Slice(0, payloadLength + 3)); + } + + bool writeToStack = Available < RespConstants.MaxProtocolBytesIntegerInt32; + + Span target = writeToStack ? stackalloc byte[16] : Tail; + target[0] = (byte)prefix; + } + + /// + /// Write a payload as a bulk string. + /// + /// The payload to write. + public void WriteBulkString(string value) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (value is null) ThrowNull(); + WriteBulkString(value.AsSpan()); + } + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + // ReSharper disable once NotResolvedInText + private static void ThrowNull() => + throw new ArgumentNullException("value", "Null values cannot be sent from client to server"); + + internal void WriteBulkStringUnoptimized(string? value) + { + if (value is null) ThrowNull(); + if (value.Length == 0) + { + WriteRaw("$0\r\n\r\n"u8); + } + else + { + var byteCount = RespConstants.UTF8.GetByteCount(value); + WritePrefixedInteger(RespPrefix.BulkString, byteCount); + if (Available >= byteCount) + { + var actual = RespConstants.UTF8.GetBytes(value.AsSpan(), Tail); + Debug.Assert(actual == byteCount); + _index += actual; + } + else + { + WriteUtf8Slow(value.AsSpan(), byteCount); + } + + WriteCrLf(); + } + } + + /// + /// Write a payload as a bulk string. + /// + /// The payload to write. + public void WriteBulkString(ReadOnlyMemory value) => WriteBulkString(value.Span); + + /// + /// Write a payload as a bulk string. + /// + /// The payload to write. + public void WriteBulkString(scoped ReadOnlySpan value) + { + if (value.Length == 0) + { + if (Available >= 6) + { + WriteRawPrechecked(Raw.BulkStringEmpty_6, 6); + } + else + { + WriteRaw("$0\r\n\r\n"u8); + } + } + else + { + var byteCount = RespConstants.UTF8.GetByteCount(value); + WriteBulkStringHeader(byteCount); + if (Available >= 2 + byteCount) + { + var actual = RespConstants.UTF8.GetBytes(value, Tail); + Debug.Assert(actual == byteCount); + _index += actual; + WriteCrLfUnsafe(); + } + else + { + FlushAndGetBuffer(Math.Min(byteCount, MAX_BUFFER_HINT)); + if (Available >= byteCount + 2) + { + // that'll work + var actual = RespConstants.UTF8.GetBytes(value, Tail); + Debug.Assert(actual == byteCount); + _index += actual; + WriteCrLfUnsafe(); + } + else + { + WriteUtf8Slow(value, byteCount); + WriteCrLf(); + } + } + } + } + + private const int MAX_BUFFER_HINT = 64 * 1024; + + private void WriteUtf8Slow(scoped ReadOnlySpan value, int remaining) + { + var enc = _perThreadEncoder; + if (enc is null) + { + enc = _perThreadEncoder = RespConstants.UTF8.GetEncoder(); + } + else + { + enc.Reset(); + } + + bool completed; + int charsUsed, bytesUsed; + do + { + enc.Convert(value, Tail, false, out charsUsed, out bytesUsed, out completed); + value = value.Slice(charsUsed); + _index += bytesUsed; + remaining -= bytesUsed; + FlushAndGetBuffer(Math.Min(remaining, MAX_BUFFER_HINT)); + } + // until done... + while (!completed); + + if (remaining != 0) + { + // any trailing data? + FlushAndGetBuffer(Math.Min(remaining, MAX_BUFFER_HINT)); + enc.Convert(value, Tail, true, out charsUsed, out bytesUsed, out completed); + Debug.Assert(charsUsed == 0 && completed); + _index += bytesUsed; + remaining -= bytesUsed; + } + + enc.Reset(); + Debug.Assert(remaining == 0); + } + + internal void WriteBulkString(in ReadOnlySequence value) + { + if (value.IsSingleSegment) + { +#if NETCOREAPP3_0_OR_GREATER + WriteBulkString(value.FirstSpan); +#else + WriteBulkString(value.First.Span); +#endif + } + else + { + // lazy for now + int len = checked((int)value.Length); + byte[] buffer = ArrayPool.Shared.Rent(len); + value.CopyTo(buffer); + WriteBulkString(new ReadOnlySpan(buffer, 0, len)); + ArrayPool.Shared.Return(buffer); + } + } + + internal void WriteBulkString(in ReadOnlySequence value) + { + if (value.IsSingleSegment) + { +#if NETCOREAPP3_0_OR_GREATER + WriteBulkString(value.FirstSpan); +#else + WriteBulkString(value.First.Span); +#endif + } + else + { + // lazy for now + int len = checked((int)value.Length); + char[] buffer = ArrayPool.Shared.Rent(len); + value.CopyTo(buffer); + WriteBulkString(new ReadOnlySpan(buffer, 0, len)); + ArrayPool.Shared.Return(buffer); + } + } + + /// + /// Experimental. + /// + public void WriteBulkString(int value) + { + if (Available >= sizeof(ulong)) + { + switch (value) + { + case -1: + WriteRawPrechecked(Raw.BulkStringInt32_M1_8, 8); + return; + case 0: + WriteRawPrechecked(Raw.BulkStringInt32_0_7, 7); + return; + case 1: + WriteRawPrechecked(Raw.BulkStringInt32_1_7, 7); + return; + case 2: + WriteRawPrechecked(Raw.BulkStringInt32_2_7, 7); + return; + case 3: + WriteRawPrechecked(Raw.BulkStringInt32_3_7, 7); + return; + case 4: + WriteRawPrechecked(Raw.BulkStringInt32_4_7, 7); + return; + case 5: + WriteRawPrechecked(Raw.BulkStringInt32_5_7, 7); + return; + case 6: + WriteRawPrechecked(Raw.BulkStringInt32_6_7, 7); + return; + case 7: + WriteRawPrechecked(Raw.BulkStringInt32_7_7, 7); + return; + case 8: + WriteRawPrechecked(Raw.BulkStringInt32_8_7, 7); + return; + case 9: + WriteRawPrechecked(Raw.BulkStringInt32_9_7, 7); + return; + case 10: + WriteRawPrechecked(Raw.BulkStringInt32_10_8, 8); + return; + } + } + + WriteBulkStringUnoptimized(value); + } + + internal void WriteBulkStringUnoptimized(int value) + { + if (Available >= RespConstants.MaxProtocolBytesBulkStringIntegerInt32) + { + var singleDigit = value >= -99_999_999 && value <= 999_999_999; + WriteRawUnsafe((byte)RespPrefix.BulkString); + + var target = Tail.Slice(singleDigit ? 3 : 4); // N\r\n or NN\r\n + if (!Utf8Formatter.TryFormat(value, target, out var valueBytes)) + ThrowFormatException(); + + Debug.Assert(valueBytes > 0 && singleDigit ? valueBytes < 10 : valueBytes is 10 or 11); + if (!Utf8Formatter.TryFormat(valueBytes, Tail, out var prefixBytes)) + ThrowFormatException(); + Debug.Assert(prefixBytes == (singleDigit ? 1 : 2)); + _index += prefixBytes; + WriteCrLfUnsafe(); + _index += valueBytes; + WriteCrLfUnsafe(); + } + else + { + Debug.Assert(RespConstants.MaxRawBytesInt32 <= 16); + Span scratch = stackalloc byte[16]; + if (!Utf8Formatter.TryFormat(value, scratch, out int bytes)) + ThrowFormatException(); + WritePrefixedInteger(RespPrefix.BulkString, bytes); + WriteRaw(scratch.Slice(0, bytes)); + WriteCrLf(); + } + } + + /// + /// Write an array header. + /// + /// The number of elements in the array. + public void WriteArray(int count) + { + if (Available >= sizeof(uint)) + { + switch (count) + { + case 0: + WriteRawPrechecked(Raw.ArrayPrefix_0_4, 4); + return; + case 1: + WriteRawPrechecked(Raw.ArrayPrefix_1_4, 4); + return; + case 2: + WriteRawPrechecked(Raw.ArrayPrefix_2_4, 4); + return; + case 3: + WriteRawPrechecked(Raw.ArrayPrefix_3_4, 4); + return; + case 4: + WriteRawPrechecked(Raw.ArrayPrefix_4_4, 4); + return; + case 5: + WriteRawPrechecked(Raw.ArrayPrefix_5_4, 4); + return; + case 6: + WriteRawPrechecked(Raw.ArrayPrefix_6_4, 4); + return; + case 7: + WriteRawPrechecked(Raw.ArrayPrefix_7_4, 4); + return; + case 8: + WriteRawPrechecked(Raw.ArrayPrefix_8_4, 4); + return; + case 9: + WriteRawPrechecked(Raw.ArrayPrefix_9_4, 4); + return; + case 10 when Available >= sizeof(ulong): + WriteRawPrechecked(Raw.ArrayPrefix_10_5, 5); + return; + case -1: + WriteRawPrechecked(Raw.ArrayPrefix_M1_5, 5); + return; + } + } + + WritePrefixedInteger(RespPrefix.Array, count); + } + + private void WriteBulkStringHeader(int count) + { + if (Available >= sizeof(uint)) + { + switch (count) + { + case 0: + WriteRawPrechecked(Raw.BulkStringPrefix_0_4, 4); + return; + case 1: + WriteRawPrechecked(Raw.BulkStringPrefix_1_4, 4); + return; + case 2: + WriteRawPrechecked(Raw.BulkStringPrefix_2_4, 4); + return; + case 3: + WriteRawPrechecked(Raw.BulkStringPrefix_3_4, 4); + return; + case 4: + WriteRawPrechecked(Raw.BulkStringPrefix_4_4, 4); + return; + case 5: + WriteRawPrechecked(Raw.BulkStringPrefix_5_4, 4); + return; + case 6: + WriteRawPrechecked(Raw.BulkStringPrefix_6_4, 4); + return; + case 7: + WriteRawPrechecked(Raw.BulkStringPrefix_7_4, 4); + return; + case 8: + WriteRawPrechecked(Raw.BulkStringPrefix_8_4, 4); + return; + case 9: + WriteRawPrechecked(Raw.BulkStringPrefix_9_4, 4); + return; + case 10 when Available >= sizeof(ulong): + WriteRawPrechecked(Raw.BulkStringPrefix_10_5, 5); + return; + case -1 when Available >= sizeof(ulong): + WriteRawPrechecked(Raw.BulkStringPrefix_M1_5, 5); + return; + } + } + + WritePrefixedInteger(RespPrefix.BulkString, count); + } + + internal void WriteArrayUnpotimized(int count) => WritePrefixedInteger(RespPrefix.Array, count); + + private void WriteRawPrechecked(ulong value, int count) + { + Debug.Assert(Available >= sizeof(ulong)); + Debug.Assert(count >= 0 && count <= sizeof(long)); + Unsafe.WriteUnaligned(ref WriteHead, value); + _index += count; + } + + private void WriteRawPrechecked(uint value, int count) + { + Debug.Assert(Available >= sizeof(uint)); + Debug.Assert(count >= 0 && count <= sizeof(uint)); + Unsafe.WriteUnaligned(ref WriteHead, value); + _index += count; + } + + internal void DebugResetIndex() => _index = 0; + + [ThreadStatic] + // used for multi-chunk encoding + private static Encoder? _perThreadEncoder; +} diff --git a/src/RESPite/RespCommandAttribute.cs b/src/RESPite/RespCommandAttribute.cs new file mode 100644 index 000000000..e3f0c5239 --- /dev/null +++ b/src/RESPite/RespCommandAttribute.cs @@ -0,0 +1,34 @@ +using System.Diagnostics; + +namespace RESPite; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +[Conditional("DEBUG")] +public sealed class RespCommandAttribute(string? command = null) : Attribute +{ + public string? Command => command; + public string? Formatter { get; set; } + public string? Parser { get; set; } + + public static class Parsers + { + private const string Prefix = "global::RESPite.RespParsers."; + + public const string Summary = Prefix + nameof(RespParsers.ResponseSummary) + + "." + nameof(RespParsers.ResponseSummary.Parser); + public const string ByteArray = Prefix + nameof(RespParsers.ByteArray); + public const string String = Prefix + nameof(RespParsers.String); + public const string Int32 = Prefix + nameof(RespParsers.Int32); + public const string Int64 = Prefix + nameof(RespParsers.Int64); + public const string NullableInt64 = Prefix + nameof(RespParsers.NullableInt64); + public const string NullableInt32 = Prefix + nameof(RespParsers.NullableInt32); + public const string NullableSingle = Prefix + nameof(RespParsers.NullableSingle); + public const string BufferWriter = Prefix + nameof(RespParsers.BufferWriter); + public const string ByteArrayArray = Prefix + nameof(RespParsers.ByteArrayArray); + public const string OK = Prefix + nameof(RespParsers.OK); + public const string Single = Prefix + nameof(RespParsers.Single); + public const string Double = Prefix + nameof(RespParsers.Double); + public const string Success = Prefix + nameof(RespParsers.Success); + public const string NullableDouble = Prefix + nameof(RespParsers.NullableDouble); + } +} diff --git a/src/RESPite/RespContext.cs b/src/RESPite/RespContext.cs index 009ba366c..da10b4ecf 100644 --- a/src/RESPite/RespContext.cs +++ b/src/RESPite/RespContext.cs @@ -1,5 +1,7 @@ using System.Runtime.CompilerServices; using RESPite.Connections; +using RESPite.Connections.Internal; +using RESPite.Messages; namespace RESPite; @@ -8,9 +10,14 @@ namespace RESPite; /// public readonly struct RespContext { + public static ref readonly RespContext Null => ref NullConnection.Instance.Context; + private readonly IRespConnection _connection; - private readonly int _database; private readonly CancellationToken _cancellationToken; + private readonly int _database; + + private readonly int _flags; + private const int FlagsDisableCaptureContext = 1 << 0; private const string CtorUsageWarning = $"The context from {nameof(IRespConnection)}.{nameof(IRespConnection.Context)} should be preferred, using {nameof(WithCancellationToken)} etc as necessary."; @@ -45,19 +52,6 @@ public RespContext( public IRespConnection Connection => _connection; public int Database => _database; public CancellationToken CancellationToken => _cancellationToken; -/* - public RespMessageBuilder Command(ReadOnlySpan command, T value, IRespFormatter formatter) - => new(this, command, value, formatter); - - public RespMessageBuilder Command(ReadOnlySpan command) - => new(this, command, Void.Instance, RespFormatters.Void); - - public RespMessageBuilder Command(ReadOnlySpan command, string value, bool isKey) - => new(this, command, value, RespFormatters.String(isKey)); - - public RespMessageBuilder Command(ReadOnlySpan command, byte[] value, bool isKey) - => new(this, command, value, RespFormatters.ByteArray(isKey)); - */ public RespCommandMap RespCommandMap => _connection.Configuration.RespCommandMap; @@ -83,6 +77,15 @@ public RespContext WithConnection(IRespConnection connection) return clone; } + public RespContext ConfigureAwait(bool continueOnCapturedContext) + { + RespContext clone = this; + Unsafe.AsRef(in clone._flags) = continueOnCapturedContext + ? _flags & ~FlagsDisableCaptureContext + : _flags | FlagsDisableCaptureContext; + return clone; + } + public IBatchConnection CreateBatch(int sizeHint = 0) => new BatchConnection(in this, sizeHint); internal static RespContext For(IRespConnection connection) diff --git a/src/RESPite/RespContextExtensions.cs b/src/RESPite/RespContextExtensions.cs new file mode 100644 index 000000000..73e2fe451 --- /dev/null +++ b/src/RESPite/RespContextExtensions.cs @@ -0,0 +1,37 @@ +using RESPite.Messages; + +namespace RESPite; + +public static class RespContextExtensions +{ + public static RespOperationBuilder Command( + this in RespContext context, + ReadOnlySpan command, + T value, + IRespFormatter formatter) + => new(in context, command, value, formatter); + + /* + public static RespOperationBuilder Command( + this in RespContext context, + ReadOnlySpan command, + T value) + => new(in context, command, value, RespFormatters.Get()); +*/ + + public static RespOperationBuilder Command(this in RespContext context, ReadOnlySpan command) + => new(in context, command, false, RespFormatters.Empty); + + /* + public static RespOperationBuilder Command(this in RespContext context, ReadOnlySpan command, + string value, bool isKey) + => new(in context, command, value, RespFormatters.String(isKey)); + + public static RespOperationBuilder Command( + this in RespContext context, + ReadOnlySpan command, + byte[] value, + bool isKey) + => new(in context, command, value, RespFormatters.ByteArray(isKey)); + */ +} diff --git a/src/RESPite/RespFormatters.cs b/src/RESPite/RespFormatters.cs new file mode 100644 index 000000000..8680d484b --- /dev/null +++ b/src/RESPite/RespFormatters.cs @@ -0,0 +1,70 @@ +using RESPite.Messages; + +namespace RESPite; + +public static class RespFormatters +{ + public static IRespFormatter String(bool isKey) => isKey ? Key.String : Value.String; + public static IRespFormatter ByteArray(bool isKey) => isKey ? Key.ByteArray : Value.ByteArray; + public static IRespFormatter Empty => EmptyFormatter.Instance; + + public static class Key + { + // ReSharper disable once MemberHidesStaticFromOuterClass + public static IRespFormatter String => Formatter.Default; + // ReSharper disable once MemberHidesStaticFromOuterClass + public static IRespFormatter ByteArray => Formatter.Default; + + internal sealed class Formatter : IRespFormatter, IRespFormatter + { + private Formatter() { } + public static readonly Formatter Default = new(); + + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in string value) + { + writer.WriteCommand(command, 1); + writer.WriteKey(value); + } + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in byte[] value) + { + writer.WriteCommand(command, 1); + writer.WriteKey(value); + } + } + } + + public static class Value + { + // ReSharper disable once MemberHidesStaticFromOuterClass + public static IRespFormatter String => Formatter.Default; + // ReSharper disable once MemberHidesStaticFromOuterClass + public static IRespFormatter ByteArray => Formatter.Default; + + internal sealed class Formatter : IRespFormatter, IRespFormatter + { + private Formatter() { } + public static readonly Formatter Default = new(); + + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in string value) + { + writer.WriteCommand(command, 1); + writer.WriteBulkString(value); + } + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in byte[] value) + { + writer.WriteCommand(command, 1); + writer.WriteBulkString(value); + } + } + } + + private sealed class EmptyFormatter : IRespFormatter + { + private EmptyFormatter() { } + public static readonly EmptyFormatter Instance = new(); + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in bool value) + { + writer.WriteCommand(command, 0); + } + } +} diff --git a/src/RESPite/RespOperation.cs b/src/RESPite/RespOperation.cs index ff8b1ba1c..17106dd7a 100644 --- a/src/RESPite/RespOperation.cs +++ b/src/RESPite/RespOperation.cs @@ -63,6 +63,7 @@ public void Wait(TimeSpan timeout = default) public bool IsCanceled => Message.GetStatus(_token) == ValueTaskSourceStatus.Canceled; internal short Token => _token; + public ref readonly CancellationToken CancellationToken => ref Message.CancellationToken; internal static readonly Action InvokeState = static state => ((Action)state!).Invoke(); diff --git a/src/RESPite/RespOperationBuilder.cs b/src/RESPite/RespOperationBuilder.cs new file mode 100644 index 000000000..e59ac5a3d --- /dev/null +++ b/src/RESPite/RespOperationBuilder.cs @@ -0,0 +1,50 @@ +using RESPite.Messages; + +namespace RESPite; + +public readonly ref struct RespOperationBuilder( + in RespContext context, + ReadOnlySpan command, + TRequest value, + IRespFormatter formatter) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif +{ + private readonly RespContext _context = context; + private readonly ReadOnlySpan _command = command; + private readonly TRequest _value = value; // cannot inline to .ctor because of "allows ref struct" + + public TResponse Wait() + => Send(RespParsers.Get()).Wait(); + + public TResponse Wait(IRespParser parser) + => Send(parser).Wait(); + + public TResponse Wait(in TState state) + => Send(in state, RespParsers.Get()).Wait(); + + public TResponse Wait(in TState state, IRespParser parser) + => Send(in state, parser).Wait(); + + public void Wait() => Send(RespParsers.Success).Wait(); + + public RespOperation Send() + => Send(RespParsers.Get()); + + public RespOperation Send(IRespParser parser) + { + _ = _context; + _ = formatter; + throw new NotImplementedException(); + } + + public RespOperation Send() => Send(RespParsers.Success); + public RespOperation Send(in TState state) + => Send(in state, RespParsers.Get()); + + public RespOperation Send(in TState state, IRespParser parser) + { + throw new NotImplementedException(); + } +} diff --git a/src/RESPite/RespOperationT.cs b/src/RESPite/RespOperationT.cs index 4114911a4..d9cd9fad1 100644 --- a/src/RESPite/RespOperationT.cs +++ b/src/RESPite/RespOperationT.cs @@ -27,6 +27,8 @@ internal RespOperation(RespMessageBase message, bool disableCaptureContext = } internal IRespMessage Message => _message ?? RespOperation.ThrowNoMessage(); + public CancellationToken CancellationToken => Message.CancellationToken; + private RespMessageBase TypedMessage => _message ?? (RespMessageBase)RespOperation.ThrowNoMessage(); /// diff --git a/src/RESPite/RespParsers.cs b/src/RESPite/RespParsers.cs new file mode 100644 index 000000000..2ef47d537 --- /dev/null +++ b/src/RESPite/RespParsers.cs @@ -0,0 +1,191 @@ +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using RESPite.Internal; +using RESPite.Messages; + +namespace RESPite; + +public static class RespParsers +{ + public static IRespParser Success => InbuiltInlineParsers.Default; + public static IRespParser OK => OKParser.Default; + public static IRespParser String => InbuiltCopyOutParsers.Default; + public static IRespParser Int32 => InbuiltInlineParsers.Default; + public static IRespParser NullableInt32 => InbuiltInlineParsers.Default; + public static IRespParser Int64 => InbuiltInlineParsers.Default; + public static IRespParser NullableInt64 => InbuiltInlineParsers.Default; + public static IRespParser Single => InbuiltInlineParsers.Default; + public static IRespParser NullableSingle => InbuiltInlineParsers.Default; + public static IRespParser Double => InbuiltInlineParsers.Default; + public static IRespParser NullableDouble => InbuiltInlineParsers.Default; + public static IRespParser ByteArray => InbuiltCopyOutParsers.Default; + public static IRespParser ByteArrayArray => InbuiltCopyOutParsers.Default; + public static IRespParser, int> BufferWriter => InbuiltCopyOutParsers.Default; + + private static class StatelessCache + { + public static IRespParser? Instance = + (InbuiltCopyOutParsers.Default as IRespParser) ?? // regular (may allocate, etc) + (InbuiltInlineParsers.Default as IRespParser) ?? // inline + (ResponseSummary.Parser as IRespParser); // inline+metadata + } + + private static class StatefulCache + { + public static IRespParser? Instance = + InbuiltCopyOutParsers.Default as IRespParser; // ?? // regular (may allocate, etc) + // (InbuiltInlineParsers.Default as IRespParser) ?? // inline + // (ResponseSummary.Parser as IRespParser); // inline+metadata + } + + private static bool IsInbuilt(object? obj) => obj is InbuiltCopyOutParsers or InbuiltInlineParsers + or ResponseSummary.ResponseSummaryParser; + + public static IRespParser Get() + { + var obj = StatelessCache.Instance; + if (obj is null) ThrowNoParser(); + return obj; + } + + public static IRespParser Get() + { + var obj = StatefulCache.Instance; + if (obj is null) ThrowNoParser(); + return obj; + } + + public static void Set(IRespParser parser) + { + if (IsInbuilt(StatelessCache.Instance)) ThrowInbuiltParser(); + StatelessCache.Instance = parser; + } + + public static void Set(IRespParser parser) + { + if (IsInbuilt(StatefulCache.Instance)) ThrowInbuiltParser(); + StatefulCache.Instance = parser; + } + + [DoesNotReturn] + private static void ThrowNoParser() => throw new InvalidOperationException( + message: + $"No default parser registered for this type; a custom parser must be specified via {nameof(RespParsers)}.{nameof(Set)}(...)."); + + [DoesNotReturn] + private static void ThrowInbuiltParser() => throw new InvalidOperationException( + message: $"This type has inbuilt handling and cannot be changed."); + + private sealed class InbuiltInlineParsers : IRespInlineParser, + IRespParser, + IRespParser, IRespParser, + IRespParser, IRespParser, + IRespParser, IRespParser, + IRespParser, IRespParser + { + private InbuiltInlineParsers() { } + public static readonly InbuiltInlineParsers Default = new(); + + bool IRespParser.Parse(ref RespReader reader) => reader.ReadBoolean(); + int IRespParser.Parse(ref RespReader reader) => reader.ReadInt32(); + + int? IRespParser.Parse(ref RespReader reader) => reader.IsNull ? null : reader.ReadInt32(); + + long IRespParser.Parse(ref RespReader reader) => reader.ReadInt64(); + + long? IRespParser.Parse(ref RespReader reader) => reader.IsNull ? null : reader.ReadInt64(); + + float IRespParser.Parse(ref RespReader reader) => (float)reader.ReadDouble(); + + float? IRespParser.Parse(ref RespReader reader) => reader.IsNull ? null : (float)reader.ReadDouble(); + + double IRespParser.Parse(ref RespReader reader) => reader.ReadDouble(); + + double? IRespParser.Parse(ref RespReader reader) => reader.IsNull ? null : reader.ReadDouble(); + } + + private sealed class OKParser : IRespParser, IRespInlineParser + { + private OKParser() { } + public static readonly OKParser Default = new(); + + public bool Parse(ref RespReader reader) + { + if (!(reader.Prefix == RespPrefix.SimpleString && reader.IsOK())) + { + Throw(); + } + + return true; + static void Throw() => throw new InvalidOperationException("Expected +OK response or similar."); + } + } + + private sealed class InbuiltCopyOutParsers : IRespParser, + IRespParser, IRespParser, + IRespParser, int> + { + private InbuiltCopyOutParsers() { } + public static readonly InbuiltCopyOutParsers Default = new(); + + string? IRespParser.Parse(ref RespReader reader) => reader.ReadString(); + byte[]? IRespParser.Parse(ref RespReader reader) => reader.ReadByteArray(); + + byte[]?[]? IRespParser.Parse(ref RespReader reader) => + reader.ReadArray(static (ref RespReader reader) => reader.ReadByteArray()); + + int IRespParser, int>.Parse(in IBufferWriter state, ref RespReader reader) + { + reader.DemandScalar(); + if (reader.IsNull) return -1; + return reader.CopyTo(state); + } + } + + public readonly struct ResponseSummary(RespPrefix prefix, int length, long protocolBytes) + : IEquatable + { + public RespPrefix Prefix { get; } = prefix; + public int Length { get; } = length; + public long ProtocolBytes { get; } = protocolBytes; + + /// + public override string ToString() => $"{Prefix}, Length: {Length}, Protocol Bytes: {ProtocolBytes}"; + + /// + public bool Equals(ResponseSummary other) => EqualsCore(in other); + + private bool EqualsCore(in ResponseSummary other) => + Prefix == other.Prefix && Length == other.Length && ProtocolBytes == other.ProtocolBytes; + + bool IEquatable.Equals(ResponseSummary other) => EqualsCore(in other); + + /// + public override bool Equals(object? obj) => obj is ResponseSummary summary && EqualsCore(in summary); + + /// + public override int GetHashCode() => (int)Prefix ^ Length ^ ProtocolBytes.GetHashCode(); + + public static IRespParser Parser => ResponseSummaryParser.Default; + + internal sealed class ResponseSummaryParser : IRespParser, IRespInlineParser, + IRespMetadataParser + { + private ResponseSummaryParser() { } + public static readonly ResponseSummaryParser Default = new(); + + public ResponseSummary Parse(ref RespReader reader) + { + var protocolBytes = reader.ProtocolBytesRemaining; + int length = 0; + if (reader.TryMoveNext()) + { + if (reader.IsScalar) length = reader.ScalarLength(); + else if (reader.IsAggregate) length = reader.AggregateLength(); + } + + return new ResponseSummary(reader.Prefix, length, protocolBytes); + } + } + } +} diff --git a/tests/BasicTest/Program.cs b/tests/BasicTest/Program.cs index 06017f476..c32eef998 100644 --- a/tests/BasicTest/Program.cs +++ b/tests/BasicTest/Program.cs @@ -1,87 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -#if !TEST_BASELINE -using Resp; -using RESPite; -using RESPite.Redis; -#endif +using System.Reflection; +using BenchmarkDotNet.Running; -namespace BasicTest -{ - internal static class Program - { - private static async Task Main(string[] args) - { - try - { - List benchmarks = []; -#if TEST_BASELINE - benchmarks.Add(new OldCoreBenchmark(args)); -#else - foreach (var arg in args) - { - switch (arg) - { - case "--old": - benchmarks.Add(new OldCoreBenchmark(args)); - break; - - case "--new": - benchmarks.Add(new NewCoreBenchmark(args)); - break; - } - } - - if (benchmarks.Count == 0) - { - benchmarks.Add(new NewCoreBenchmark(args)); - } -#endif - - do - { - foreach (var bench in benchmarks) - { - if (benchmarks.Count > 1) - { - Console.WriteLine($"### {bench} ###"); - } - - await bench.RunAll().ConfigureAwait(false); - } - } - // ReSharper disable once LoopVariableIsNeverChangedInsideLoop - while (benchmarks[0].Loop); - - return 0; - } - catch (Exception ex) - { - WriteException(ex); - return -1; - } - } - - internal static void WriteException(Exception ex) - { - while (ex is not null) - { - Console.Error.WriteLine(); - Console.Error.WriteLine($"{ex.GetType().Name}: {ex.Message}"); - Console.Error.WriteLine($"\t{ex.StackTrace}"); - var data = ex.Data; - if (data is not null) - { - foreach (var key in data.Keys) - { - Console.Error.WriteLine($"\t{key}: {data[key]}"); - } - } - - ex = ex.InnerException; - } - } - // private static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); - } -} +BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); diff --git a/tests/RESP.Core.Tests/OperationUnitTests.cs b/tests/RESP.Core.Tests/OperationUnitTests.cs index 3a721c45e..73f93b9f3 100644 --- a/tests/RESP.Core.Tests/OperationUnitTests.cs +++ b/tests/RESP.Core.Tests/OperationUnitTests.cs @@ -1,15 +1,20 @@ using System; +using System.Diagnostics.CodeAnalysis; using RESPite; using Xunit; namespace RESP.Core.Tests; +[SuppressMessage( + "Usage", + "xUnit1031:Do not use blocking task operations in test method", + Justification = "This isn't actually async; we're testing an awaitable.")] public class OperationUnitTests { [Fact] public void CanCreateAndCompleteOperation() { - var op = RespOperation.Create(null, out var remote); + var op = RespOperation.Create(out var remote); // initial state Assert.False(op.IsCanceled); Assert.False(op.IsCompleted); @@ -31,7 +36,7 @@ public void CanCreateAndCompleteOperation() Assert.False(remote.TrySetException(null!)); // can get result - _ = op.GetResult(); + op.GetResult(); // but only once, after that: bad things Assert.Throws(() => op.GetResult()); From 752ec80f3d19de163d985e0a899f4a431d9f1429 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 29 Aug 2025 17:11:39 +0100 Subject: [PATCH 004/108] parsers: bool and ResponseSummary --- eng/StackExchange.Redis.Build/RespCommandGenerator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index 1e4cfd85b..aee2f7751 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -671,6 +671,7 @@ private static int DataParameterCount( private static string? InbuiltParser(string type, bool explicitSuccess = false) => type switch { "" when explicitSuccess => RespParsersPrefix + "Success", + "bool" => RespParsersPrefix + "Success", "string" => RespParsersPrefix + "String", "int" => RespParsersPrefix + "Int32", "long" => RespParsersPrefix + "Int64", @@ -680,7 +681,7 @@ private static int DataParameterCount( "long?" => RespParsersPrefix + "NullableInt64", "float?" => RespParsersPrefix + "NullableSingle", "double?" => RespParsersPrefix + "NullableDouble", - "global::Resp.ResponseSummary" => RespParsersPrefix + "ResponseSummary.Parser", + "global::RESPite.RespParsers.ResponseSummary" => RespParsersPrefix + "ResponseSummary.Parser", _ => null, }; From e307d6ed8b2184a966f1ff0b438d3f3606be0e66 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 30 Aug 2025 07:06:57 +0100 Subject: [PATCH 005/108] unifying tests --- src/RESPite.Redis/Alt/DownlevelExtensions.cs | 1 - src/RESPite.Redis/KeyStringArrayFormatter.cs | 3 +- src/RESPite.Redis/RESPite.Redis.csproj | 3 +- src/RESPite.Redis/RedisExtensions.cs | 1 - src/RESPite.Redis/RedisKeys.cs | 1 - src/RESPite.Redis/RedisStrings.cs | 2 +- src/RESPite/Connections/RespConnectionPool.cs | 2 + src/RESPite/Internal/CycleBuffer.cs | 2 +- src/RESPite/RESPite.csproj | 3 + src/RESPite/RespConfiguration.cs | 8 +- src/RESPite/RespContext.cs | 2 +- src/RESPite/RespContextExtensions.cs | 89 ++++++++++++++++++- tests/BasicTest/BasicTest.csproj | 1 - tests/BasicTest/RedisBenchmarks.cs | 19 ++-- .../RESP.Core.Tests/BasicIntegrationTests.cs | 8 +- tests/RESP.Core.Tests/BufferTests.cs | 4 +- tests/RESP.Core.Tests/ConnectionFixture.cs | 9 +- tests/RESP.Core.Tests/IntegrationTestBase.cs | 3 +- tests/RESP.Core.Tests/RESP.Core.Tests.csproj | 1 - 19 files changed, 124 insertions(+), 38 deletions(-) diff --git a/src/RESPite.Redis/Alt/DownlevelExtensions.cs b/src/RESPite.Redis/Alt/DownlevelExtensions.cs index e437850bc..03fac7d6b 100644 --- a/src/RESPite.Redis/Alt/DownlevelExtensions.cs +++ b/src/RESPite.Redis/Alt/DownlevelExtensions.cs @@ -1,5 +1,4 @@ using System.Runtime.CompilerServices; -using Resp; namespace RESPite.Redis.Alt; // legacy fallback for down-level compilers diff --git a/src/RESPite.Redis/KeyStringArrayFormatter.cs b/src/RESPite.Redis/KeyStringArrayFormatter.cs index 98d37494e..eb65b2507 100644 --- a/src/RESPite.Redis/KeyStringArrayFormatter.cs +++ b/src/RESPite.Redis/KeyStringArrayFormatter.cs @@ -1,5 +1,6 @@ using System; -using Resp; +using RESPite; +using RESPite.Messages; namespace RESPite.Redis; diff --git a/src/RESPite.Redis/RESPite.Redis.csproj b/src/RESPite.Redis/RESPite.Redis.csproj index 1b9f7fef5..33fc28fd1 100644 --- a/src/RESPite.Redis/RESPite.Redis.csproj +++ b/src/RESPite.Redis/RESPite.Redis.csproj @@ -8,8 +8,7 @@ - - + diff --git a/src/RESPite.Redis/RedisExtensions.cs b/src/RESPite.Redis/RedisExtensions.cs index a10ca1064..f4468bebe 100644 --- a/src/RESPite.Redis/RedisExtensions.cs +++ b/src/RESPite.Redis/RedisExtensions.cs @@ -1,5 +1,4 @@ using System.Runtime.CompilerServices; -using Resp; namespace RESPite.Redis; diff --git a/src/RESPite.Redis/RedisKeys.cs b/src/RESPite.Redis/RedisKeys.cs index 1a45c34a0..db956425e 100644 --- a/src/RESPite.Redis/RedisKeys.cs +++ b/src/RESPite.Redis/RedisKeys.cs @@ -1,5 +1,4 @@ using System; -using Resp; namespace RESPite.Redis; diff --git a/src/RESPite.Redis/RedisStrings.cs b/src/RESPite.Redis/RedisStrings.cs index 19e96c390..02d224dc6 100644 --- a/src/RESPite.Redis/RedisStrings.cs +++ b/src/RESPite.Redis/RedisStrings.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using Resp; +using RESPite.Messages; #if !PREVIEW_LANGVER using RESPite.Redis.Alt; diff --git a/src/RESPite/Connections/RespConnectionPool.cs b/src/RESPite/Connections/RespConnectionPool.cs index e78f4b4ce..a77d7e52d 100644 --- a/src/RESPite/Connections/RespConnectionPool.cs +++ b/src/RESPite/Connections/RespConnectionPool.cs @@ -21,6 +21,8 @@ public sealed class RespConnectionPool : IDisposable private readonly int _count; private readonly RespContext _defaultTemplate; + public ref readonly RespContext Template => ref _defaultTemplate; + public RespConnectionPool( in RespContext template, Func createConnection, diff --git a/src/RESPite/Internal/CycleBuffer.cs b/src/RESPite/Internal/CycleBuffer.cs index f32f66b28..7bd4ca4b5 100644 --- a/src/RESPite/Internal/CycleBuffer.cs +++ b/src/RESPite/Internal/CycleBuffer.cs @@ -28,7 +28,7 @@ namespace RESPite.Internal; /// /// There is a *lot* of validation in debug mode; we want to be super sure that we don't corrupt buffer state. /// -partial struct CycleBuffer +internal partial struct CycleBuffer { // note: if someone uses an uninitialized CycleBuffer (via default): that's a skills issue; git gud public static CycleBuffer Create(MemoryPool? pool = null, int pageSize = DefaultPageSize) diff --git a/src/RESPite/RESPite.csproj b/src/RESPite/RESPite.csproj index 24de49d05..cfb5f5b87 100644 --- a/src/RESPite/RESPite.csproj +++ b/src/RESPite/RESPite.csproj @@ -15,6 +15,9 @@ + + + RespOperation.cs diff --git a/src/RESPite/RespConfiguration.cs b/src/RESPite/RespConfiguration.cs index 638d94857..fd0bdb519 100644 --- a/src/RESPite/RespConfiguration.cs +++ b/src/RESPite/RespConfiguration.cs @@ -25,7 +25,7 @@ public Builder(RespConfiguration? source) { if (source is not null) { - CommandMap = source.RespCommandMap; + CommandMap = source.CommandMap; SyncTimeout = source.SyncTimeout; KeyPrefix = source.KeyPrefix.ToArray(); ServiceProvider = source.ServiceProvider; @@ -58,12 +58,12 @@ public RespConfiguration Create() } private RespConfiguration( - RespCommandMap respCommandMap, + RespCommandMap commandMap, byte[] keyPrefix, TimeSpan syncTimeout, IServiceProvider serviceProvider) { - RespCommandMap = respCommandMap; + CommandMap = commandMap; SyncTimeout = syncTimeout; _keyPrefix = (byte[])keyPrefix.Clone(); // create isolated copy ServiceProvider = serviceProvider; @@ -71,7 +71,7 @@ private RespConfiguration( private readonly byte[] _keyPrefix; public IServiceProvider ServiceProvider { get; } - public RespCommandMap RespCommandMap { get; } + public RespCommandMap CommandMap { get; } public TimeSpan SyncTimeout { get; } public ReadOnlySpan KeyPrefix => _keyPrefix; diff --git a/src/RESPite/RespContext.cs b/src/RESPite/RespContext.cs index da10b4ecf..e5e7c65a2 100644 --- a/src/RESPite/RespContext.cs +++ b/src/RESPite/RespContext.cs @@ -53,7 +53,7 @@ public RespContext( public int Database => _database; public CancellationToken CancellationToken => _cancellationToken; - public RespCommandMap RespCommandMap => _connection.Configuration.RespCommandMap; + public RespCommandMap CommandMap => _connection.Configuration.CommandMap; public RespContext WithCancellationToken(CancellationToken cancellationToken) { diff --git a/src/RESPite/RespContextExtensions.cs b/src/RESPite/RespContextExtensions.cs index 73e2fe451..96fae7d2b 100644 --- a/src/RESPite/RespContextExtensions.cs +++ b/src/RESPite/RespContextExtensions.cs @@ -1,14 +1,16 @@ -using RESPite.Messages; +using System.Buffers; +using RESPite.Internal; +using RESPite.Messages; namespace RESPite; public static class RespContextExtensions { - public static RespOperationBuilder Command( + public static RespOperationBuilder Command( this in RespContext context, ReadOnlySpan command, - T value, - IRespFormatter formatter) + TRequest value, + IRespFormatter formatter) => new(in context, command, value, formatter); /* @@ -34,4 +36,83 @@ public static RespOperationBuilder Command( bool isKey) => new(in context, command, value, RespFormatters.ByteArray(isKey)); */ + public static RespOperation Send( + this in RespContext context, + ReadOnlySpan command, + in TRequest request, + IRespFormatter formatter, + IRespParser parser) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + var oversized = Serialize( + context.CommandMap, command, in request, formatter, out int length); + var msg = RespMessage.Get(parser) + .Init(oversized, 0, length, ArrayPool.Shared, context.CancellationToken); + RespOperation operation = new(msg); + context.Connection.Send(operation); + return operation; + } + + public static RespOperation Send( + this in RespContext context, + ReadOnlySpan command, + in TRequest request, + in TState state, + IRespFormatter formatter, + IRespParser parser) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + var oversized = Serialize( + context.CommandMap, command, in request, formatter, out int length); + var msg = RespMessage.Get(in state, parser) + .Init(oversized, 0, length, ArrayPool.Shared, context.CancellationToken); + RespOperation operation = new(msg); + context.Connection.Send(operation); + return operation; + } + + private static byte[] Serialize( + RespCommandMap commandMap, + ReadOnlySpan command, + in TRequest request, + IRespFormatter formatter, + out int length) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + throw new NotImplementedException(); + /* + int size = 0; + + if (formatter is IRespSizeEstimator estimator) + { + size = estimator.EstimateSize(command, request); + } + + + var buffer = AmbientBufferWriter.Get(size); + try + { + var writer = new RespWriter(buffer); + if (!ReferenceEquals(commandMap, RespCommandMap.Default)) + { + writer.CommandMap = commandMap; + } + + formatter.Format(command, ref writer, request); + writer.Flush(); + return buffer.Detach(out length); + } + catch + { + buffer.Reset(); + throw; + } + */ + } } diff --git a/tests/BasicTest/BasicTest.csproj b/tests/BasicTest/BasicTest.csproj index 3cb3dc5e4..97b916dd5 100644 --- a/tests/BasicTest/BasicTest.csproj +++ b/tests/BasicTest/BasicTest.csproj @@ -14,7 +14,6 @@ - diff --git a/tests/BasicTest/RedisBenchmarks.cs b/tests/BasicTest/RedisBenchmarks.cs index 9df2c5a56..0e6c02b93 100644 --- a/tests/BasicTest/RedisBenchmarks.cs +++ b/tests/BasicTest/RedisBenchmarks.cs @@ -2,7 +2,8 @@ using System.Threading.Tasks; using BenchmarkDotNet.Attributes; #if !TEST_BASELINE -using Resp; +using RESPite; +using RESPite.Connections; using RESPite.Redis; #if !PREVIEW_LANGVER using RESPite.Redis.Alt; // needed for AsStrings() etc @@ -19,7 +20,7 @@ public class RedisBenchmarks : IDisposable private ConnectionMultiplexer connection; private IDatabase db; #if !TEST_BASELINE - private Resp.RespConnectionPool pool, customPool; + private RespConnectionPool pool, customPool; #endif [GlobalSetup] @@ -233,7 +234,7 @@ public void StringGet_Core() // [Benchmark(Description = "PC StringSet/s", OperationsPerInvoke = COUNT)] public void StringSet_Pipelined_Core() { - using var conn = pool.GetConnection().ForPipeline(); + using var conn = pool.GetConnection().Synchronized(); #if PREVIEW_LANGVER ref readonly RedisStrings s = ref conn.Context.Strings; #else @@ -251,7 +252,7 @@ public void StringSet_Pipelined_Core() // [Benchmark(Description = "PCA StringSet/s", OperationsPerInvoke = COUNT)] public async Task StringSet_Pipelined_Core_Async() { - using var conn = pool.GetConnection().ForPipeline(); + using var conn = pool.GetConnection().Synchronized(); var ctx = conn.Context; for (int i = 0; i < OperationsPerInvoke; i++) { @@ -269,7 +270,7 @@ public async Task StringSet_Pipelined_Core_Async() // [Benchmark(Description = "PC StringGet/s", OperationsPerInvoke = COUNT)] public void StringGet_Pipelined_Core() { - using var conn = pool.GetConnection().ForPipeline(); + using var conn = pool.GetConnection().Synchronized(); #if PREVIEW_LANGVER ref readonly RedisStrings s = ref conn.Context.Strings; #else @@ -287,7 +288,7 @@ public void StringGet_Pipelined_Core() // [Benchmark(Description = "PCA StringGet/s", OperationsPerInvoke = COUNT)] public async Task StringGet_Pipelined_Core_Async() { - using var conn = pool.GetConnection().ForPipeline(); + using var conn = pool.GetConnection().Synchronized(); var ctx = conn.Context; for (int i = 0; i < OperationsPerInvoke; i++) { @@ -349,7 +350,7 @@ public int IncrBy_Old() [Benchmark(Description = "new incr /p", OperationsPerInvoke = OperationsPerInvoke)] public int IncrBy_New_Pipelined() { - using var conn = pool.GetConnection().ForPipeline(); + using var conn = pool.GetConnection().Synchronized(); #if PREVIEW_LANGVER ref readonly RedisStrings s = ref conn.Context.Strings; #else @@ -371,7 +372,7 @@ public int IncrBy_New_Pipelined() [Benchmark(Description = "new incr /p/a", OperationsPerInvoke = OperationsPerInvoke)] public async Task IncrBy_New_Pipelined_Async() { - using var conn = pool.GetConnection().ForPipeline(); + using var conn = pool.GetConnection().Synchronized(); var ctx = conn.Context; int value = 0; #if PREVIEW_LANGVER @@ -419,7 +420,7 @@ public int IncrBy_New() // [Benchmark(Description = "new incr /pc", OperationsPerInvoke = OperationsPerInvoke)] public int IncrBy_New_Pipelined_Custom() { - using var conn = customPool.GetConnection().ForPipeline(); + using var conn = customPool.GetConnection().Synchronized(); #if PREVIEW_LANGVER ref readonly RedisStrings s = ref conn.Context.Strings; #else diff --git a/tests/RESP.Core.Tests/BasicIntegrationTests.cs b/tests/RESP.Core.Tests/BasicIntegrationTests.cs index 128bd9b1f..9c624b30a 100644 --- a/tests/RESP.Core.Tests/BasicIntegrationTests.cs +++ b/tests/RESP.Core.Tests/BasicIntegrationTests.cs @@ -1,7 +1,9 @@ using System; using System.Threading; using System.Threading.Tasks; -using Resp; +using RESPite; +using RESPite.Connections; +using RESPite.Messages; using RESPite.Redis; using RESPite.Redis.Alt; // needed for AsStrings() etc using Xunit; @@ -26,7 +28,7 @@ public void Parse() ReadOnlySpan buffer = "$3\r\nabc\r\n"u8; var reader = new RespReader(buffer); reader.MoveNext(); - var value = RespParsers.String.Parse(in Resp.Void.Instance, ref reader); + var value = RespParsers.String.Parse(ref reader); reader.DemandEnd(); Assert.Equal("abc", value); } @@ -77,7 +79,7 @@ public async Task PingPipelinedAsync(int count, bool forPipeline) { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - await using var conn = forPipeline ? GetConnection().ForPipeline() : GetConnection(); + await using var conn = forPipeline ? GetConnection().Synchronized() : GetConnection(); ValueTask[] tasks = new ValueTask[count]; for (int i = 0; i < count; i++) diff --git a/tests/RESP.Core.Tests/BufferTests.cs b/tests/RESP.Core.Tests/BufferTests.cs index 68368451c..faa507293 100644 --- a/tests/RESP.Core.Tests/BufferTests.cs +++ b/tests/RESP.Core.Tests/BufferTests.cs @@ -2,8 +2,8 @@ using System.Buffers; using System.Diagnostics; using System.Threading; -using System.Xml.XPath; -using Resp; +using RESPite.Internal; +using RESPite.Messages; using Xunit; namespace RESP.Core.Tests; diff --git a/tests/RESP.Core.Tests/ConnectionFixture.cs b/tests/RESP.Core.Tests/ConnectionFixture.cs index 5fe57abbe..0b77ebb66 100644 --- a/tests/RESP.Core.Tests/ConnectionFixture.cs +++ b/tests/RESP.Core.Tests/ConnectionFixture.cs @@ -1,8 +1,8 @@ using System; using System.Net; -using System.Threading; -using Resp; using RESP.Core.Tests; +using RESPite; +using RESPite.Connections; using Xunit; [assembly: AssemblyFixture(typeof(ConnectionFixture))] @@ -16,5 +16,8 @@ public class ConnectionFixture : IDisposable public void Dispose() => _pool.Dispose(); public IRespConnection GetConnection() - => _pool.GetConnection(cancellationToken: TestContext.Current.CancellationToken); + { + var template = _pool.Template.WithCancellationToken(TestContext.Current.CancellationToken); + return _pool.GetConnection(template); + } } diff --git a/tests/RESP.Core.Tests/IntegrationTestBase.cs b/tests/RESP.Core.Tests/IntegrationTestBase.cs index 61beba25c..601de5618 100644 --- a/tests/RESP.Core.Tests/IntegrationTestBase.cs +++ b/tests/RESP.Core.Tests/IntegrationTestBase.cs @@ -1,6 +1,5 @@ using System.Runtime.CompilerServices; -using Resp; -using RESPite.Redis; +using RESPite; using RESPite.Redis.Alt; using Xunit; diff --git a/tests/RESP.Core.Tests/RESP.Core.Tests.csproj b/tests/RESP.Core.Tests/RESP.Core.Tests.csproj index 75b13c455..977a0f3a6 100644 --- a/tests/RESP.Core.Tests/RESP.Core.Tests.csproj +++ b/tests/RESP.Core.Tests/RESP.Core.Tests.csproj @@ -18,7 +18,6 @@ - From fd00c0e5e7a80b56e175995a15fb66f2a9d35262 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 30 Aug 2025 09:36:28 +0100 Subject: [PATCH 006/108] Move to RespConnection (base class); compiles only --- src/RESPite.Benchmark/NewCoreBenchmark.cs | 2 +- src/RESPite/Alt/DownlevelExtensions.cs | 10 -- ...hConnection.cs => BasicBatchConnection.cs} | 112 +++++------- .../Internal/ConfiguredConnection.cs | 35 +--- .../Internal/DecoratorConnection.cs | 35 ++++ .../Connections/Internal/NullConnection.cs | 38 ++-- .../Internal/SynchronizedConnection.cs | 68 +++----- .../Connections/RespConnectionExtensions.cs | 4 +- src/RESPite/Connections/RespConnectionPool.cs | 85 +++------ src/RESPite/IRespBatch.cs | 7 - src/RESPite/IRespConnection.cs | 19 -- src/RESPite/Internal/StreamConnection.cs | 46 ++--- src/RESPite/RespBatch.cs | 14 ++ src/RESPite/RespConnection.cs | 163 ++++++++++++++++++ src/RESPite/RespContext.cs | 44 +---- tests/RESP.Core.Tests/ConnectionFixture.cs | 2 +- tests/RESP.Core.Tests/IntegrationTestBase.cs | 2 +- 17 files changed, 361 insertions(+), 325 deletions(-) delete mode 100644 src/RESPite/Alt/DownlevelExtensions.cs rename src/RESPite/Connections/Internal/{BatchConnection.cs => BasicBatchConnection.cs} (56%) create mode 100644 src/RESPite/Connections/Internal/DecoratorConnection.cs delete mode 100644 src/RESPite/IRespBatch.cs delete mode 100644 src/RESPite/IRespConnection.cs create mode 100644 src/RESPite/RespBatch.cs create mode 100644 src/RESPite/RespConnection.cs diff --git a/src/RESPite.Benchmark/NewCoreBenchmark.cs b/src/RESPite.Benchmark/NewCoreBenchmark.cs index a345f7ae2..c0df677ec 100644 --- a/src/RESPite.Benchmark/NewCoreBenchmark.cs +++ b/src/RESPite.Benchmark/NewCoreBenchmark.cs @@ -112,7 +112,7 @@ public override async Task RunAll() protected override Func GetFlush(RespContext client) { - if (client.Connection is IBatchConnection batch) + if (client.Connection is RespBatch batch) { return () => { diff --git a/src/RESPite/Alt/DownlevelExtensions.cs b/src/RESPite/Alt/DownlevelExtensions.cs deleted file mode 100644 index 68e0c2850..000000000 --- a/src/RESPite/Alt/DownlevelExtensions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace RESPite.Alt; - -/// -/// For use with older compilers that don't support byref-return, extension-everything, etc. -/// -public static class DownlevelExtensions -{ - public static RespContext GetContext(this IRespConnection connection) - => connection.Context; -} diff --git a/src/RESPite/Connections/Internal/BatchConnection.cs b/src/RESPite/Connections/Internal/BasicBatchConnection.cs similarity index 56% rename from src/RESPite/Connections/Internal/BatchConnection.cs rename to src/RESPite/Connections/Internal/BasicBatchConnection.cs index 999c21d79..e6738e8da 100644 --- a/src/RESPite/Connections/Internal/BatchConnection.cs +++ b/src/RESPite/Connections/Internal/BasicBatchConnection.cs @@ -1,65 +1,60 @@ using System.Buffers; +using System.Diagnostics; using System.Runtime.InteropServices; namespace RESPite.Connections.Internal; -internal sealed class BatchConnection : IBatchConnection +/// +/// Holds basic RespOperation, queue and release - turns +/// multiple send/send-many calls into a single send-many call. +/// +internal sealed class BasicBatchConnection : RespBatch { - private bool _isDisposed; private readonly List _unsent; - private readonly IRespConnection _tail; - private readonly RespContext _context; - public BatchConnection(in RespContext context, int sizeHint) + public BasicBatchConnection(in RespContext context, int sizeHint) : base(context) { - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - an abundance of caution - var tail = context.Connection; - if (tail is not { CanWrite: true }) ThrowNonWritable(); - if (tail is BatchConnection) ThrowBatch(); + // ack: yes, I know we won't spot every recursive+decorated scenario + if (Tail is BasicBatchConnection) ThrowNestedBatch(); _unsent = sizeHint <= 0 ? [] : new List(sizeHint); - _tail = tail!; - _context = context.WithConnection(this); - static void ThrowBatch() => throw new ArgumentException("Nested batches are not supported", nameof(tail)); - static void ThrowNonWritable() => - throw new ArgumentException("A writable connection is required", nameof(tail)); + static void ThrowNestedBatch() => throw new ArgumentException("Nested batches are not supported", nameof(context)); } - public void Dispose() + protected override void OnDispose(bool disposing) { - lock (_unsent) + if (disposing) { - /* everyone else checks disposal inside the lock, so: - once we've set this, we can be sure that no more - items will be added */ - _isDisposed = true; - } + lock (_unsent) + { + /* everyone else checks disposal inside the lock; + the base type already marked as disposed, so: + once we're past this point, we can be sure that no more + items will be added */ + Debug.Assert(IsDisposed); + } #if NET5_0_OR_GREATER - var span = CollectionsMarshal.AsSpan(_unsent); - foreach (var message in span) - { - message.Message.TrySetException(message.Token, new ObjectDisposedException(ToString())); - } + var span = CollectionsMarshal.AsSpan(_unsent); + foreach (var message in span) + { + message.Message.TrySetException(message.Token, CreateObjectDisposedException()); + } #else - foreach (var message in _unsent) - { - message.Message.TrySetException(message.Token, new ObjectDisposedException(ToString())); - } + foreach (var message in _unsent) + { + message.Message.TrySetException(message.Token, CreateObjectDisposedException()); + } #endif - _unsent.Clear(); - } + _unsent.Clear(); + } - public ValueTask DisposeAsync() - { - Dispose(); - return default; + base.OnDispose(disposing); } - public RespConfiguration Configuration => _tail.Configuration; - public bool CanWrite => _tail.CanWrite; + internal override bool IsHealthy => base.IsHealthy & Tail.IsHealthy; - public int Outstanding + internal override int OutstandingOperations { get { @@ -70,31 +65,16 @@ public int Outstanding } } - public ref readonly RespContext Context => ref _context; - - private const string SyncMessage = "Batch connections do not support synchronous sends"; - public void Send(in RespOperation message) => throw new NotSupportedException(SyncMessage); - - public void Send(ReadOnlySpan messages) => throw new NotSupportedException(SyncMessage); - - private void ThrowIfDisposed() - { - if (_isDisposed) Throw(); - static void Throw() => throw new ObjectDisposedException(nameof(BatchConnection)); - } - - public Task SendAsync(in RespOperation message) + public override void Send(in RespOperation message) { lock (_unsent) { ThrowIfDisposed(); _unsent.Add(message); } - - return Task.CompletedTask; } - public Task SendAsync(ReadOnlyMemory messages) + internal override void Send(ReadOnlySpan messages) { if (messages.Length != 0) { @@ -102,7 +82,7 @@ public Task SendAsync(ReadOnlyMemory messages) { ThrowIfDisposed(); #if NET8_0_OR_GREATER - _unsent.AddRange(messages.Span); // internally optimized + _unsent.AddRange(messages); // internally optimized #else // two-step; first ensure capacity, then add in loop #if NET6_0_OR_GREATER @@ -118,15 +98,13 @@ public Task SendAsync(ReadOnlyMemory messages) _unsent.Capacity = newCapacity; } #endif - foreach (var message in messages.Span) + foreach (var message in messages) { _unsent.Add(message); } #endif } } - - return Task.CompletedTask; } private int Flush(out RespOperation[] oversized, out RespOperation single) @@ -156,17 +134,17 @@ private int Flush(out RespOperation[] oversized, out RespOperation single) } } - public Task FlushAsync() + public override Task FlushAsync() { var count = Flush(out var oversized, out var single); return count switch { 0 => Task.CompletedTask, - 1 => _tail.SendAsync(single!), - _ => SendAndRecycleAsync(_tail, oversized, count), + 1 => Tail.SendAsync(single!), + _ => SendAndRecycleAsync(Tail, oversized, count), }; - static async Task SendAndRecycleAsync(IRespConnection tail, RespOperation[] oversized, int count) + static async Task SendAndRecycleAsync(RespConnection tail, RespOperation[] oversized, int count) { try { @@ -185,7 +163,7 @@ static async Task SendAndRecycleAsync(IRespConnection tail, RespOperation[] over } } - public void Flush() + public override void Flush() { var count = Flush(out var oversized, out var single); switch (count) @@ -193,13 +171,13 @@ public void Flush() case 0: return; case 1: - _tail.Send(single!); + Tail.Send(single!); return; } try { - _tail.Send(oversized.AsSpan(0, count)); + Tail.Send(oversized.AsSpan(0, count)); } catch (Exception ex) { diff --git a/src/RESPite/Connections/Internal/ConfiguredConnection.cs b/src/RESPite/Connections/Internal/ConfiguredConnection.cs index 5a5491c01..ad5ae6ce7 100644 --- a/src/RESPite/Connections/Internal/ConfiguredConnection.cs +++ b/src/RESPite/Connections/Internal/ConfiguredConnection.cs @@ -1,35 +1,4 @@ namespace RESPite.Connections.Internal; -internal sealed class ConfiguredConnection : IRespConnection -{ - private readonly IRespConnection _tail; - private readonly RespConfiguration _configuration; - private readonly RespContext _context; - - public ref readonly RespContext Context => ref _context; - - public ConfiguredConnection(in RespContext tail, RespConfiguration configuration) - { - _tail = tail.Connection; - _configuration = configuration; - _context = tail.WithConnection(this); - } - - public void Dispose() => _tail.Dispose(); - - public ValueTask DisposeAsync() => _tail.DisposeAsync(); - - public RespConfiguration Configuration => _configuration; - - public bool CanWrite => _tail.CanWrite; - - public int Outstanding => _tail.Outstanding; - - public void Send(in RespOperation message) => _tail.Send(message); - public void Send(ReadOnlySpan messages) => _tail.Send(messages); - - public Task SendAsync(in RespOperation message) => - _tail.SendAsync(message); - - public Task SendAsync(ReadOnlyMemory messages) => _tail.SendAsync(messages); -} +internal sealed class ConfiguredConnection(in RespContext tail, RespConfiguration configuration) + : DecoratorConnection(tail, configuration); diff --git a/src/RESPite/Connections/Internal/DecoratorConnection.cs b/src/RESPite/Connections/Internal/DecoratorConnection.cs new file mode 100644 index 000000000..d5745e3f0 --- /dev/null +++ b/src/RESPite/Connections/Internal/DecoratorConnection.cs @@ -0,0 +1,35 @@ +namespace RESPite.Connections.Internal; + +internal abstract class DecoratorConnection : RespConnection +{ + protected readonly RespConnection Tail; + + public DecoratorConnection(in RespContext tail, RespConfiguration? configuration = null) + : base(tail, configuration) + { + Tail = tail.Connection; + } + + protected virtual bool OwnsConnection => true; + + internal override bool IsHealthy => base.IsHealthy & Tail.IsHealthy; + internal override int OutstandingOperations => Tail.OutstandingOperations; + + protected override void OnDispose(bool disposing) + { + if (disposing & OwnsConnection) Tail.Dispose(); + } + + protected override ValueTask OnDisposeAsync() => + OwnsConnection ? Tail.DisposeAsync() : default; + + // Note that default behaviour *does not* add a dispose check, as it + // assumes that the connection is "owned", and therefore the tail will throw. + public override void Send(in RespOperation message) => Tail.Send(message); + + internal override void Send(ReadOnlySpan messages) => Tail.Send(messages); + + public override Task SendAsync(in RespOperation message) => Tail.SendAsync(in message); + + internal override Task SendAsync(ReadOnlyMemory messages) => Tail.SendAsync(messages); +} diff --git a/src/RESPite/Connections/Internal/NullConnection.cs b/src/RESPite/Connections/Internal/NullConnection.cs index 0ed166f16..a7f807674 100644 --- a/src/RESPite/Connections/Internal/NullConnection.cs +++ b/src/RESPite/Connections/Internal/NullConnection.cs @@ -1,37 +1,27 @@ namespace RESPite.Connections.Internal; -internal sealed class NullConnection : IRespConnection +internal sealed class NullConnection : RespConnection { - private readonly RespContext _context; - public static NullConnection WithConfiguration(RespConfiguration configuration) => ReferenceEquals(configuration, RespConfiguration.Default) - ? Instance + ? Default : new(configuration); - private NullConnection(RespConfiguration configuration) + public static readonly NullConnection Default = new(RespConfiguration.Default); + + private NullConnection(RespConfiguration configuration) : base(configuration) { - _context = RespContext.For(this); - Configuration = configuration; } - public static readonly NullConnection Instance = new(RespConfiguration.Default); - public void Dispose() { } - - public ValueTask DisposeAsync() => default; - - public RespConfiguration Configuration { get; } - public bool CanWrite => false; - public int Outstanding => 0; - - public ref readonly RespContext Context => ref _context; - private const string SendErrorMessage = "Null connections do not support sending messages."; - public void Send(in RespOperation message) => throw new NotSupportedException(SendErrorMessage); - - public void Send(ReadOnlySpan message) => throw new NotSupportedException(SendErrorMessage); - - public Task SendAsync(in RespOperation message) => throw new NotSupportedException(SendErrorMessage); + public override void Send(in RespOperation message) + { + message.Message.TrySetException(message.Token, new NotSupportedException(SendErrorMessage)); + } - public Task SendAsync(ReadOnlyMemory message) => throw new NotSupportedException(SendErrorMessage); + public override Task SendAsync(in RespOperation message) + { + Send(message); + return Task.CompletedTask; + } } diff --git a/src/RESPite/Connections/Internal/SynchronizedConnection.cs b/src/RESPite/Connections/Internal/SynchronizedConnection.cs index 5aa65d5ef..efe5aecd1 100644 --- a/src/RESPite/Connections/Internal/SynchronizedConnection.cs +++ b/src/RESPite/Connections/Internal/SynchronizedConnection.cs @@ -2,42 +2,32 @@ namespace RESPite.Connections.Internal; -internal sealed class SynchronizedConnection : IRespConnection +internal sealed class SynchronizedConnection(in RespContext tail) : DecoratorConnection(tail) { - private readonly IRespConnection _tail; - private readonly RespContext _context; private readonly SemaphoreSlim _semaphore = new(1); - public ref readonly RespContext Context => ref _context; - - public SynchronizedConnection(in RespContext tail) - { - _tail = tail.Connection; - _context = tail.WithConnection(this); - } - - public void Dispose() + protected override void OnDispose(bool disposing) { - _semaphore.Dispose(); - _tail.Dispose(); + if (disposing) + { + _semaphore.Dispose(); + } + base.OnDispose(disposing); } - public ValueTask DisposeAsync() + protected override ValueTask OnDisposeAsync() { _semaphore.Dispose(); - return _tail.DisposeAsync(); + return base.OnDisposeAsync(); } - public RespConfiguration Configuration => _tail.Configuration; - public bool CanWrite => _semaphore.CurrentCount > 0 && _tail.CanWrite; - public int Outstanding => _tail.Outstanding; - - public void Send(in RespOperation message) + internal override bool IsHealthy => _semaphore.CurrentCount > 0 & base.IsHealthy; + public override void Send(in RespOperation message) { - _semaphore.Wait(message.CancellationToken); try { - _tail.Send(message); + _semaphore.Wait(message.CancellationToken); + Tail.Send(message); } catch (Exception ex) { @@ -50,7 +40,7 @@ public void Send(in RespOperation message) } } - public void Send(ReadOnlySpan messages) + internal override void Send(ReadOnlySpan messages) { switch (messages.Length) { @@ -60,14 +50,14 @@ public void Send(ReadOnlySpan messages) return; } - _semaphore.Wait(messages[0].CancellationToken); try { - _tail.Send(messages); + _semaphore.Wait(messages[0].CancellationToken); + Tail.Send(messages); } catch (Exception ex) { - TrySetException(messages, ex); + MarkFaulted(messages, ex); throw; } finally @@ -76,7 +66,7 @@ public void Send(ReadOnlySpan messages) } } - public Task SendAsync(in RespOperation message) + public override Task SendAsync(in RespOperation message) { bool haveLock = false; try @@ -88,7 +78,7 @@ public Task SendAsync(in RespOperation message) return FullAsync(this, message); } - var pending = _tail.SendAsync(message); + var pending = Tail.SendAsync(message); if (!pending.IsCompleted) { DebugCounters.OnPipelineSendAsync(); @@ -124,7 +114,7 @@ static async Task FullAsync(SynchronizedConnection @this, RespOperation message) try { - await @this._tail.SendAsync(message).ConfigureAwait(false); + await @this.Tail.SendAsync(message).ConfigureAwait(false); } finally { @@ -145,15 +135,7 @@ private async Task AwaitAndReleaseLock(Task pending) } } - private static void TrySetException(ReadOnlySpan messages, Exception ex) - { - foreach (var message in messages) - { - message.Message.TrySetException(message.Token, ex); - } - } - - public Task SendAsync(ReadOnlyMemory messages) + internal override Task SendAsync(ReadOnlyMemory messages) { switch (messages.Length) { @@ -171,7 +153,7 @@ public Task SendAsync(ReadOnlyMemory messages) return FullAsync(this, messages); } - var pending = _tail.SendAsync(messages); + var pending = Tail.SendAsync(messages); if (!pending.IsCompleted) { DebugCounters.OnPipelineSendAsync(); @@ -185,7 +167,7 @@ public Task SendAsync(ReadOnlyMemory messages) } catch (Exception ex) { - TrySetException(messages.Span, ex); + MarkFaulted(messages.Span, ex); throw; } finally @@ -200,11 +182,11 @@ static async Task FullAsync(SynchronizedConnection @this, ReadOnlyMemory /// Enforces stricter ordering guarantees, so that unawaited async operations cannot cause overlapping writes. /// - public static IRespConnection Synchronized(this IRespConnection connection) + public static RespConnection Synchronized(this RespConnection connection) => connection is SynchronizedConnection ? connection : new SynchronizedConnection(in connection.Context); - public static IRespConnection WithConfiguration(this IRespConnection connection, RespConfiguration configuration) + public static RespConnection WithConfiguration(this RespConnection connection, RespConfiguration configuration) => ReferenceEquals(configuration, connection.Configuration) ? connection : new ConfiguredConnection(in connection.Context, configuration); diff --git a/src/RESPite/Connections/RespConnectionPool.cs b/src/RESPite/Connections/RespConnectionPool.cs index a77d7e52d..f41a3cfa4 100644 --- a/src/RESPite/Connections/RespConnectionPool.cs +++ b/src/RESPite/Connections/RespConnectionPool.cs @@ -16,8 +16,8 @@ public sealed class RespConnectionPool : IDisposable [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public bool UseCustomNetworkStream { get; set; } - private readonly ConcurrentQueue _pool = []; - private readonly Func _createConnection; + private readonly ConcurrentQueue _pool = []; + private readonly Func _createConnection; private readonly int _count; private readonly RespContext _defaultTemplate; @@ -25,7 +25,7 @@ public sealed class RespConnectionPool : IDisposable public RespConnectionPool( in RespContext template, - Func createConnection, + Func createConnection, int count = DefaultCount) { _createConnection = createConnection; @@ -37,7 +37,7 @@ public RespConnectionPool( } public RespConnectionPool( - Func createConnection, + Func createConnection, int count = DefaultCount) : this(RespContext.Null, createConnection, count) { } @@ -71,14 +71,14 @@ public RespConnectionPool(in RespContext template, EndPoint endPoint, int count /// /// Borrow a connection from the pool, using the default template. /// - public IRespConnection GetConnection() => GetConnection(in _defaultTemplate); + public RespConnection GetConnection() => GetConnection(in _defaultTemplate); /// /// Borrow a connection from the pool. /// /// The template context to use for the leased connection; everything except the connection /// will be inherited by the new context. - public IRespConnection GetConnection(in RespContext template) + public RespConnection GetConnection(in RespContext template) { ThrowIfDisposed(); template.CancellationToken.ThrowIfCancellationRequested(); @@ -87,8 +87,7 @@ public IRespConnection GetConnection(in RespContext template) { connection = _createConnection(template.Connection.Configuration); } - - return new PoolWrapper(this, connection, in template); + return new PoolWrapper(this, template.WithConnection(connection)); } private void ThrowIfDisposed() @@ -106,9 +105,9 @@ public void Dispose() } } - private void Return(IRespConnection tail) + private void Return(RespConnection tail) { - if (_isDisposed || !tail.CanWrite || _pool.Count >= _count) + if (_isDisposed || !tail.IsHealthy || _pool.Count >= _count) { tail.Dispose(); } @@ -118,7 +117,7 @@ private void Return(IRespConnection tail) } } - private static IRespConnection CreateConnection(RespConfiguration config, EndPoint endpoint) + private static RespConnection CreateConnection(RespConfiguration config, EndPoint endpoint) { Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.NoDelay = true; @@ -126,71 +125,43 @@ private static IRespConnection CreateConnection(RespConfiguration config, EndPoi return new StreamConnection(config, new NetworkStream(socket)); } - private sealed class PoolWrapper : IRespConnection + private sealed class PoolWrapper( + RespConnectionPool pool, + in RespContext tail) : DecoratorConnection(tail) { - private bool _isDisposed; - private readonly RespConnectionPool _pool; - private readonly IRespConnection _tail; - private readonly RespContext _context; - - public ref readonly RespContext Context => ref _context; - - public PoolWrapper( - RespConnectionPool pool, - IRespConnection tail, - in RespContext template) - { - _pool = pool; - _tail = tail; - _context = template.WithConnection(this); - } - - public void Dispose() - { - _isDisposed = true; - _pool.Return(_tail); - } - - public bool CanWrite => !_isDisposed && _tail.CanWrite; - - public int Outstanding => _tail.Outstanding; - - public RespConfiguration Configuration => _tail.Configuration; - - private void ThrowIfDisposed() - { - if (_isDisposed) Throw(); - static void Throw() => throw new ObjectDisposedException(nameof(PoolWrapper)); - } + protected override bool OwnsConnection => false; - public ValueTask DisposeAsync() + protected override void OnDispose(bool disposing) { - Dispose(); - return default; + if (disposing) + { + pool.Return(Tail); + } + base.OnDispose(disposing); } - public void Send(in RespOperation message) + public override void Send(in RespOperation message) { ThrowIfDisposed(); - _tail.Send(message); + Tail.Send(message); } - public void Send(ReadOnlySpan messages) + internal override void Send(ReadOnlySpan messages) { ThrowIfDisposed(); - _tail.Send(messages); + Tail.Send(messages); } - public Task SendAsync(in RespOperation message) + public override Task SendAsync(in RespOperation message) { ThrowIfDisposed(); - return _tail.SendAsync(message); + return Tail.SendAsync(message); } - public Task SendAsync(ReadOnlyMemory messages) + internal override Task SendAsync(ReadOnlyMemory messages) { ThrowIfDisposed(); - return _tail.SendAsync(messages); + return Tail.SendAsync(messages); } } } diff --git a/src/RESPite/IRespBatch.cs b/src/RESPite/IRespBatch.cs deleted file mode 100644 index 12daa12b9..000000000 --- a/src/RESPite/IRespBatch.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace RESPite; - -public interface IBatchConnection : IRespConnection -{ - Task FlushAsync(); - void Flush(); -} diff --git a/src/RESPite/IRespConnection.cs b/src/RESPite/IRespConnection.cs deleted file mode 100644 index 17c62c727..000000000 --- a/src/RESPite/IRespConnection.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace RESPite; - -public interface IRespConnection : IDisposable, IAsyncDisposable -{ - RespConfiguration Configuration { get; } - bool CanWrite { get; } - int Outstanding { get; } - - /// - /// Gets the default context associates with this connection. - /// - ref readonly RespContext Context { get; } - - void Send(in RespOperation message); - void Send(ReadOnlySpan message); - - Task SendAsync(in RespOperation message); - Task SendAsync(ReadOnlyMemory message); -} diff --git a/src/RESPite/Internal/StreamConnection.cs b/src/RESPite/Internal/StreamConnection.cs index bc348932b..0d629d617 100644 --- a/src/RESPite/Internal/StreamConnection.cs +++ b/src/RESPite/Internal/StreamConnection.cs @@ -7,18 +7,18 @@ using System.Buffers; using System.Collections.Concurrent; using System.Diagnostics; +using System.Net.Mime; using System.Runtime.CompilerServices; using RESPite.Messages; namespace RESPite.Internal; -internal sealed class StreamConnection : IRespConnection +internal sealed class StreamConnection : RespConnection { private bool _isDoomed; private RespScanState _readScanState; private CycleBuffer _readBuffer, _writeBuffer; - private readonly RespContext _context; - public ref readonly RespContext Context => ref _context; + public bool CanWrite => Volatile.Read(ref _readStatus) == WRITER_AVAILABLE; public int Outstanding => _outstanding.Count; @@ -27,14 +27,13 @@ internal sealed class StreamConnection : IRespConnection private readonly Stream tail; private ConcurrentQueue _outstanding = new(); - public RespConfiguration Configuration { get; } - public StreamConnection(RespConfiguration configuration, Stream tail, bool asyncRead = true) + public StreamConnection(in RespContext context, RespConfiguration configuration, Stream tail, bool asyncRead = true) + : base(context, configuration) { - Configuration = configuration; if (!(tail.CanRead && tail.CanWrite)) Throw(); this.tail = tail; - var memoryPool = configuration.GetService>(); + var memoryPool = Configuration.GetService>(); _readBuffer = CycleBuffer.Create(memoryPool); _writeBuffer = CycleBuffer.Create(memoryPool); if (asyncRead) @@ -46,10 +45,12 @@ public StreamConnection(RespConfiguration configuration, Stream tail, bool async new Thread(ReadAll).Start(); } - _context = RespContext.For(this); - static void Throw() => throw new ArgumentException("Stream must be readable and writable", nameof(tail)); } + public StreamConnection(RespConfiguration configuration, Stream tail, bool asyncRead = true) + : this(RespContext.Null, configuration, tail, asyncRead) + { + } public RespMode Mode { get; set; } = RespMode.Resp2; @@ -446,7 +447,7 @@ private void OnRequestUnavailable(in RespOperation message) } } - public void Send(in RespOperation message) + public override void Send(in RespOperation message) { bool releaseRequest = message.Message.TryReserveRequest(message.Token, out var bytes); if (!releaseRequest) @@ -480,7 +481,7 @@ public void Send(in RespOperation message) } } - public void Send(ReadOnlySpan messages) + internal override void Send(ReadOnlySpan messages) { switch (messages.Length) { @@ -536,7 +537,7 @@ public void Send(ReadOnlySpan messages) } } - public Task SendAsync(in RespOperation message) + public override Task SendAsync(in RespOperation message) { bool releaseRequest = message.Message.TryReserveRequest(message.Token, out var bytes); if (!releaseRequest) @@ -603,7 +604,7 @@ static async Task AwaitedSingleWithToken( } } - public Task SendAsync(ReadOnlyMemory messages) + internal override Task SendAsync(ReadOnlyMemory messages) { switch (messages.Length) { @@ -696,21 +697,24 @@ private void Doom() Interlocked.CompareExchange(ref _writeStatus, WRITER_DOOMED, WRITER_AVAILABLE); } - public void Dispose() + protected override void OnDispose(bool disposing) { - _fault ??= new ObjectDisposedException(ToString()); - Doom(); - tail.Dispose(); + if (disposing) + { + _fault ??= new ObjectDisposedException(ToString()); + Doom(); + tail.Dispose(); + } } - public override string ToString() => nameof(StreamConnection); - - public ValueTask DisposeAsync() + protected override ValueTask OnDisposeAsync() { + _fault ??= new ObjectDisposedException(ToString()); + Doom(); #if COREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER return tail.DisposeAsync().AsTask(); #else - Dispose(); + tail.Dispose(); return default; #endif } diff --git a/src/RESPite/RespBatch.cs b/src/RESPite/RespBatch.cs new file mode 100644 index 000000000..488d4f98c --- /dev/null +++ b/src/RESPite/RespBatch.cs @@ -0,0 +1,14 @@ +namespace RESPite; + +public abstract class RespBatch : RespConnection +{ + // a batch doesn't act as a proxy to the tail, so we don't need to DecoratorConnection logic + protected readonly RespConnection Tail; + private protected RespBatch(in RespContext tail) : base(tail) + { + Tail = tail.Connection; + } + + public abstract Task FlushAsync(); + public abstract void Flush(); +} diff --git a/src/RESPite/RespConnection.cs b/src/RESPite/RespConnection.cs new file mode 100644 index 000000000..0f669f9b1 --- /dev/null +++ b/src/RESPite/RespConnection.cs @@ -0,0 +1,163 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using RESPite.Connections.Internal; + +namespace RESPite; + +public abstract class RespConnection : IDisposable, IAsyncDisposable +{ + private bool _isDisposed; + internal bool IsDisposed => _isDisposed; + + private readonly RespContext _context; + public ref readonly RespContext Context => ref _context; + public RespConfiguration Configuration { get; } + + internal virtual bool IsHealthy => !_isDisposed; + + internal virtual int OutstandingOperations { get; } + + // this is the usual usage, since we want context to be preserved + private protected RespConnection(in RespContext tail, RespConfiguration? configuration = null) + { + var conn = tail.Connection; + if (conn is not { IsHealthy: true }) + { + ThrowUnhealthy(); + } + + Configuration = configuration ?? conn.Configuration; + _context = tail.WithConnection(this); + + static void ThrowUnhealthy() => + throw new ArgumentException("A healthy tail connection is required.", nameof(tail)); + } + + // this is atypical - only for use when creating null connections + private protected RespConnection(RespConfiguration? configuration = null) + { + Configuration = configuration ?? RespConfiguration.Default; + _context = default; + _context = _context.WithConnection(this); + Debug.Assert(this is NullConnection); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void ThrowIfDisposed() + { + if (_isDisposed) ThrowDisposed(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void ThrowDisposed() => throw CreateObjectDisposedException(); + + internal Exception CreateObjectDisposedException() => new ObjectDisposedException(GetType().Name); + + public void Dispose() + { + _isDisposed = this is not NullConnection; + OnDispose(true); + } + + protected virtual void OnDispose(bool disposing) + { + } + + public ValueTask DisposeAsync() + { + _isDisposed = this is not NullConnection; + return OnDisposeAsync(); + } + + protected virtual ValueTask OnDisposeAsync() + { + OnDispose(true); + return default; + } + + public abstract void Send(in RespOperation message); + + internal virtual void Send(ReadOnlySpan messages) + { + int i = 0; + try + { + for (i = 0; i < messages.Length; i++) + { + Send(messages[i]); + } + } + catch (Exception ex) + { + MarkFaulted(messages.Slice(i), ex); + throw; + } + } + + public virtual Task SendAsync(in RespOperation message) + { + Send(message); + return Task.CompletedTask; + } + + internal virtual Task SendAsync(ReadOnlyMemory messages) + { + switch (messages.Length) + { + case 0: return Task.CompletedTask; + case 1: return SendAsync(messages.Span[0]); + } + + int i = 0; + try + { + for (; i < messages.Length; i++) + { + var pending = SendAsync(messages.Span[i]); + if (!pending.IsCompleted) + return Awaited(this, pending, messages.Slice(i)); + pending.GetAwaiter().GetResult(); + } + } + catch (Exception ex) + { + MarkFaulted(messages.Span.Slice(i), ex); + throw; + } + + return Task.CompletedTask; + + static async Task Awaited(RespConnection connection, Task pending, ReadOnlyMemory messages) + { + int i = 0; + try + { + await pending.ConfigureAwait(false); + for (i = 1; i < messages.Length; i++) + { + await connection.SendAsync(messages.Span[i]).ConfigureAwait(false); + } + } + catch (Exception ex) + { + MarkFaulted(messages.Span.Slice(i), ex); + throw; + } + } + } + + protected static void MarkFaulted(ReadOnlySpan messages, Exception fault) + { + foreach (var message in messages) + { + try + { + message.Message.TrySetException(message.Token, fault); + } + catch + { + // best efforts + } + } + } +} diff --git a/src/RESPite/RespContext.cs b/src/RESPite/RespContext.cs index e5e7c65a2..f59e6699a 100644 --- a/src/RESPite/RespContext.cs +++ b/src/RESPite/RespContext.cs @@ -1,7 +1,5 @@ using System.Runtime.CompilerServices; -using RESPite.Connections; using RESPite.Connections.Internal; -using RESPite.Messages; namespace RESPite; @@ -10,46 +8,19 @@ namespace RESPite; /// public readonly struct RespContext { - public static ref readonly RespContext Null => ref NullConnection.Instance.Context; + public static ref readonly RespContext Null => ref NullConnection.Default.Context; - private readonly IRespConnection _connection; + private readonly RespConnection _connection; private readonly CancellationToken _cancellationToken; private readonly int _database; private readonly int _flags; private const int FlagsDisableCaptureContext = 1 << 0; - private const string CtorUsageWarning = $"The context from {nameof(IRespConnection)}.{nameof(IRespConnection.Context)} should be preferred, using {nameof(WithCancellationToken)} etc as necessary."; - /// public override string ToString() => _connection?.ToString() ?? "(null)"; - [Obsolete(CtorUsageWarning)] - public RespContext(IRespConnection connection) : this(connection, -1, CancellationToken.None) - { - } - - [Obsolete(CtorUsageWarning)] - public RespContext(IRespConnection connection, CancellationToken cancellationToken) - : this(connection, -1, cancellationToken) - { - } - - /// - /// Transient state for a RESP operation. - /// - [Obsolete(CtorUsageWarning)] - public RespContext( - IRespConnection connection, - int database = -1, - CancellationToken cancellationToken = default) - { - _connection = connection; - _database = database; - _cancellationToken = cancellationToken; - } - - public IRespConnection Connection => _connection; + public RespConnection Connection => _connection; public int Database => _database; public CancellationToken CancellationToken => _cancellationToken; @@ -70,7 +41,7 @@ public RespContext WithDatabase(int database) return clone; } - public RespContext WithConnection(IRespConnection connection) + public RespContext WithConnection(RespConnection connection) { RespContext clone = this; Unsafe.AsRef(in clone._connection) = connection; @@ -86,10 +57,5 @@ public RespContext ConfigureAwait(bool continueOnCapturedContext) return clone; } - public IBatchConnection CreateBatch(int sizeHint = 0) => new BatchConnection(in this, sizeHint); - - internal static RespContext For(IRespConnection connection) -#pragma warning disable CS0618 // Type or member is obsolete - => new(connection); -#pragma warning restore CS0618 // Type or member is obsolete + public RespBatch CreateBatch(int sizeHint = 0) => new BasicBatchConnection(in this, sizeHint); } diff --git a/tests/RESP.Core.Tests/ConnectionFixture.cs b/tests/RESP.Core.Tests/ConnectionFixture.cs index 0b77ebb66..0dca06726 100644 --- a/tests/RESP.Core.Tests/ConnectionFixture.cs +++ b/tests/RESP.Core.Tests/ConnectionFixture.cs @@ -15,7 +15,7 @@ public class ConnectionFixture : IDisposable public void Dispose() => _pool.Dispose(); - public IRespConnection GetConnection() + public RespConnection GetConnection() { var template = _pool.Template.WithCancellationToken(TestContext.Current.CancellationToken); return _pool.GetConnection(template); diff --git a/tests/RESP.Core.Tests/IntegrationTestBase.cs b/tests/RESP.Core.Tests/IntegrationTestBase.cs index 601de5618..c76caff69 100644 --- a/tests/RESP.Core.Tests/IntegrationTestBase.cs +++ b/tests/RESP.Core.Tests/IntegrationTestBase.cs @@ -7,7 +7,7 @@ namespace RESP.Core.Tests; public abstract class IntegrationTestBase(ConnectionFixture fixture, ITestOutputHelper log) { - public IRespConnection GetConnection([CallerMemberName] string caller = "") + public RespConnection GetConnection([CallerMemberName] string caller = "") { var conn = fixture.GetConnection(); // includes cancellation from the test // most of the time, they'll be using a key from Me(), so: pre-emptively nuke it From 38cd9bb64e3cfc9d372d1b97360a01115ec5047d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 30 Aug 2025 10:43:43 +0100 Subject: [PATCH 007/108] Clarify whether WithCancellationToken means "instead of" vs "as well as". --- src/RESPite/RespContext.cs | 64 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/RESPite/RespContext.cs b/src/RESPite/RespContext.cs index f59e6699a..31526ec57 100644 --- a/src/RESPite/RespContext.cs +++ b/src/RESPite/RespContext.cs @@ -26,6 +26,9 @@ public readonly struct RespContext public RespCommandMap CommandMap => _connection.Configuration.CommandMap; + /// + /// REPLACES the associated with this context. + /// public RespContext WithCancellationToken(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -34,6 +37,67 @@ public RespContext WithCancellationToken(CancellationToken cancellationToken) return clone; } + /// + /// COMBINES the associated with this context + /// with an additional cancellation. The returned + /// represents the lifetime of the combined operation, and should be + /// disposed when complete. + /// + public Lifetime WithLinkedCancellationToken(CancellationToken cancellationToken) + { + if (!cancellationToken.CanBeCanceled + || cancellationToken == _cancellationToken) + { + // would have no effect + return new(null, in this, _cancellationToken); + } + + cancellationToken.ThrowIfCancellationRequested(); + if (!_cancellationToken.CanBeCanceled) + { + // we don't currently have cancellation; no need for a link + return new(null, in this, cancellationToken); + } + + var src = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken); + return new(src, in this, src.Token); + } + + public readonly struct Lifetime : IDisposable + { + // Unusual public field; a ref-readonly would be preferable, but by-ref props have restrictions on structs. + // We would rather avoid the copy semantics associated with a regular property getter. + public readonly RespContext Context; + + private readonly CancellationTokenSource? _source; + + internal Lifetime(CancellationTokenSource? source, in RespContext context, CancellationToken cancellationToken) + { + _source = source; + Context = context; // snapshot, we can now mutate this locally + Unsafe.AsRef(in Context._cancellationToken) = cancellationToken; + } + + public void Dispose() + { + var src = _source; + // best effort cleanup, noting that copies may exist + // (which is also why we can't risk TryReset+pool) + Unsafe.AsRef(in _source) = null; + Unsafe.AsRef(in Context._cancellationToken) = AlreadyCanceled; + src?.Dispose(); + } + + private static readonly CancellationToken AlreadyCanceled = CreateCancelledToken(); + + private static CancellationToken CreateCancelledToken() + { + CancellationTokenSource cts = new(); + cts.Cancel(); + return cts.Token; + } + } + public RespContext WithDatabase(int database) { RespContext clone = this; From 8dca0e004254b84c5b765ffabaa43b7039e2c52c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 30 Aug 2025 10:45:47 +0100 Subject: [PATCH 008/108] nit --- src/RESPite/RespContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RESPite/RespContext.cs b/src/RESPite/RespContext.cs index 31526ec57..b6a1f2481 100644 --- a/src/RESPite/RespContext.cs +++ b/src/RESPite/RespContext.cs @@ -85,7 +85,7 @@ public void Dispose() // (which is also why we can't risk TryReset+pool) Unsafe.AsRef(in _source) = null; Unsafe.AsRef(in Context._cancellationToken) = AlreadyCanceled; - src?.Dispose(); + src?.Dispose(); // question: should we cancel on EOL? suggest we defer to CTS on that } private static readonly CancellationToken AlreadyCanceled = CreateCancelledToken(); From 9f510f640ea57e2d0e39231b962cebe861d9000b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 30 Aug 2025 10:46:33 +0100 Subject: [PATCH 009/108] answered own question --- src/RESPite/RespContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RESPite/RespContext.cs b/src/RESPite/RespContext.cs index b6a1f2481..47ede06a0 100644 --- a/src/RESPite/RespContext.cs +++ b/src/RESPite/RespContext.cs @@ -85,7 +85,7 @@ public void Dispose() // (which is also why we can't risk TryReset+pool) Unsafe.AsRef(in _source) = null; Unsafe.AsRef(in Context._cancellationToken) = AlreadyCanceled; - src?.Dispose(); // question: should we cancel on EOL? suggest we defer to CTS on that + src?.Dispose(); // don't cancel on EOL; want consistent behaviour with/without link } private static readonly CancellationToken AlreadyCanceled = CreateCancelledToken(); From 87f07692f2723cc9247c7201f8e5bf6212ab92cc Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 30 Aug 2025 11:48:19 +0100 Subject: [PATCH 010/108] docs --- src/RESPite/Connections/RespConnectionPool.cs | 10 +- src/RESPite/RespConnection.cs | 16 +++ src/RESPite/RespContext.cs | 42 +++++--- src/RESPite/readme.md | 99 +++++++++++++++++++ 4 files changed, 147 insertions(+), 20 deletions(-) create mode 100644 src/RESPite/readme.md diff --git a/src/RESPite/Connections/RespConnectionPool.cs b/src/RESPite/Connections/RespConnectionPool.cs index f41a3cfa4..b155b12ca 100644 --- a/src/RESPite/Connections/RespConnectionPool.cs +++ b/src/RESPite/Connections/RespConnectionPool.cs @@ -64,7 +64,7 @@ public RespConnectionPool(EndPoint endPoint, int count = DefaultCount) } public RespConnectionPool(in RespContext template, EndPoint endPoint, int count = DefaultCount) - : this(template, config => CreateConnection(config, endPoint), count) + : this(template, config => RespConnection.Create(endPoint, config), count) { } @@ -117,14 +117,6 @@ private void Return(RespConnection tail) } } - private static RespConnection CreateConnection(RespConfiguration config, EndPoint endpoint) - { - Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - socket.NoDelay = true; - socket.Connect(endpoint); - return new StreamConnection(config, new NetworkStream(socket)); - } - private sealed class PoolWrapper( RespConnectionPool pool, in RespContext tail) : DecoratorConnection(tail) diff --git a/src/RESPite/RespConnection.cs b/src/RESPite/RespConnection.cs index 0f669f9b1..0f89e832e 100644 --- a/src/RESPite/RespConnection.cs +++ b/src/RESPite/RespConnection.cs @@ -1,6 +1,9 @@ using System.Diagnostics; +using System.Net; +using System.Net.Sockets; using System.Runtime.CompilerServices; using RESPite.Connections.Internal; +using RESPite.Internal; namespace RESPite; @@ -17,6 +20,19 @@ public abstract class RespConnection : IDisposable, IAsyncDisposable internal virtual int OutstandingOperations { get; } + private static EndPoint? _defaultEndPoint; // do not expose externally; vexingly mutable + private static EndPoint DefaultEndPoint => _defaultEndPoint ??= new IPEndPoint(IPAddress.Loopback, 6379); + public static RespConnection Create(Stream stream, RespConfiguration? configuration = null) + => new StreamConnection(configuration ?? RespConfiguration.Default, stream); + + public static RespConnection Create(EndPoint? endpoint = null, RespConfiguration? config = null) + { + Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.NoDelay = true; + socket.Connect(endpoint ?? DefaultEndPoint); + return Create(new NetworkStream(socket), config); + } + // this is the usual usage, since we want context to be preserved private protected RespConnection(in RespContext tail, RespConfiguration? configuration = null) { diff --git a/src/RESPite/RespContext.cs b/src/RESPite/RespContext.cs index 47ede06a0..dbce672a9 100644 --- a/src/RESPite/RespContext.cs +++ b/src/RESPite/RespContext.cs @@ -11,7 +11,7 @@ public readonly struct RespContext public static ref readonly RespContext Null => ref NullConnection.Default.Context; private readonly RespConnection _connection; - private readonly CancellationToken _cancellationToken; + public readonly CancellationToken CancellationToken; private readonly int _database; private readonly int _flags; @@ -22,23 +22,22 @@ public readonly struct RespContext public RespConnection Connection => _connection; public int Database => _database; - public CancellationToken CancellationToken => _cancellationToken; public RespCommandMap CommandMap => _connection.Configuration.CommandMap; /// - /// REPLACES the associated with this context. + /// REPLACES the associated with this context. /// public RespContext WithCancellationToken(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); RespContext clone = this; - Unsafe.AsRef(in clone._cancellationToken) = cancellationToken; + Unsafe.AsRef(in clone.CancellationToken) = cancellationToken; return clone; } /// - /// COMBINES the associated with this context + /// COMBINES the associated with this context /// with an additional cancellation. The returned /// represents the lifetime of the combined operation, and should be /// disposed when complete. @@ -46,23 +45,44 @@ public RespContext WithCancellationToken(CancellationToken cancellationToken) public Lifetime WithLinkedCancellationToken(CancellationToken cancellationToken) { if (!cancellationToken.CanBeCanceled - || cancellationToken == _cancellationToken) + || cancellationToken == CancellationToken) { // would have no effect - return new(null, in this, _cancellationToken); + CancellationToken.ThrowIfCancellationRequested(); + return new(null, in this, CancellationToken); } cancellationToken.ThrowIfCancellationRequested(); - if (!_cancellationToken.CanBeCanceled) + if (!CancellationToken.CanBeCanceled) { // we don't currently have cancellation; no need for a link return new(null, in this, cancellationToken); } - var src = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken); + CancellationToken.ThrowIfCancellationRequested(); + var src = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, CancellationToken); return new(src, in this, src.Token); } + public Lifetime WithTimeout(TimeSpan timeout) + { + if (timeout <= TimeSpan.Zero) Throw(); + CancellationTokenSource src; + if (CancellationToken.CanBeCanceled) + { + CancellationToken.ThrowIfCancellationRequested(); + src = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken); + src.CancelAfter(timeout); + } + else + { + src = new CancellationTokenSource(timeout); + } + static void Throw() => throw new ArgumentOutOfRangeException(nameof(timeout)); + + return new Lifetime(src, in this, src.Token); + } + public readonly struct Lifetime : IDisposable { // Unusual public field; a ref-readonly would be preferable, but by-ref props have restrictions on structs. @@ -75,7 +95,7 @@ internal Lifetime(CancellationTokenSource? source, in RespContext context, Cance { _source = source; Context = context; // snapshot, we can now mutate this locally - Unsafe.AsRef(in Context._cancellationToken) = cancellationToken; + Unsafe.AsRef(in Context.CancellationToken) = cancellationToken; } public void Dispose() @@ -84,7 +104,7 @@ public void Dispose() // best effort cleanup, noting that copies may exist // (which is also why we can't risk TryReset+pool) Unsafe.AsRef(in _source) = null; - Unsafe.AsRef(in Context._cancellationToken) = AlreadyCanceled; + Unsafe.AsRef(in Context.CancellationToken) = AlreadyCanceled; src?.Dispose(); // don't cancel on EOL; want consistent behaviour with/without link } diff --git a/src/RESPite/readme.md b/src/RESPite/readme.md new file mode 100644 index 000000000..4bd628f65 --- /dev/null +++ b/src/RESPite/readme.md @@ -0,0 +1,99 @@ +# RESPite + +RESPite is a high-performance low-level RESP (Redis, etc) library, used as the IO core for +StackExchange.Redis v3+. It is also available for direct use from other places! + +## Getting Started + +RESPite has two key primitives: + +- a *connection*, `RespConnection`. +- a *context*, `RespContext` - which is a connection plus other local ambient context such as database, cancellation, etc. + +The first thing we need, then, is to create a connection. There are many ways to do this, but to +create a connection to the local default Redis instance: + +``` c# +using var conn = RespConnection.Create(); +// ... +``` + +This gives us a single socket-based connection. Usually a *connection* is long-lived and used for +a great many RESP operations, with the `using` here closing socket eventually. + +Once we have a connection, we can start using it immediately, via the default *context*, from +`.Context`. Usually, it is the *context* that we should be passing around, not a connection: +the context *has* a connection plus local ambient configuration. So: + +``` c# +var ctx = conn.Context; +``` + +Once we have a *context*, we can use that to execute commands: + +``` c# +ctx.SomeOperation(...); +``` + +But: what is `SomeOperation(...)`? ***That's up to you.*** + +### Defining commands + +The RESPite libary only handles the RESP layer - it doesn't add the methods associated with Redis +(don't worry: RESPite.Redis does that - we're not animals!). However, in the general case where you +want to add your own RESP methods, we can do exactly that. The easiest way is by letting the tools do +the work for us: + +``` c# +static class MyCommands +{ + [RespOperation("incr")] // arg optional - it would assume "increment" if omitted + public partial static int Increment(this in RespContext ctx, string key); + + [RespOperation("incrby")] + public partial static int Increment(this in RespContext ctx, string key, int value); +} +``` + +Build-time tools will provide the implementation for us, including adding an `async` version. The code +for this isn't *difficult* - simply: it is *unnecessary*, since in most cases the intent can be clearly +understood. This avoids opportunities to fat-finger things (or get things wrong between the synchronous +and asynchronous versions). + +We can now use: + +``` c# +var x = ctx.Increment("mykey"); +var y = await ctx.IncrementAsync("mykey", 42); +``` + +That's *basically* it. If you need more control over how non-trivial commands are formatted and parsed, +APIs exist for that. But for most common scenarios: that's all we need. + +### Cancellation + +Unusually, our `IncrementAsync` method *does not* have a `CancellationToken cancellationToken = default` +parameter; instead, cancellation is conveyed *in the context*. This also means that cancellation works +for *both* the synchronous and asynchronous versions! We can supply our own cancellation: + +``` c# +var ctx = conn.Context.WithCancellationToken(request.CancellationToken); +``` + +Now `ctx` is not just the *default* context - it has the cancellation token we supplied, and it is used +everywhere automatically! If you're thinking "Wait - does that *replace* the cancellation, or +*combine* the two cancellations?", then: have a cookie. The answer is "replace", but we can also combine +- noting that now we need to scope that to a lifetime: + +``` c# +using var lifetime = conn.Context.WithLinkedCancellationToken(request.CancellationToken); +// use lifetime.Context for commands +``` + +This will automatically do the most appropriate thing based on whether neither, one, or both tokens +are cancellable. We can do the same thing with a timeout! + +``` c# +using var lifetime = conn.Context.WithTimeout(TimeSpan.FromSeconds(5)); +// use lifetime.Context for commands +``` From e43419ea08176216798188df82e7822d0ef1fe49 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 30 Aug 2025 12:29:10 +0100 Subject: [PATCH 011/108] words --- src/RESPite/RespContext.cs | 4 ++-- src/RESPite/readme.md | 22 +++++++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/RESPite/RespContext.cs b/src/RESPite/RespContext.cs index dbce672a9..715ba0885 100644 --- a/src/RESPite/RespContext.cs +++ b/src/RESPite/RespContext.cs @@ -89,9 +89,9 @@ public Lifetime WithTimeout(TimeSpan timeout) // We would rather avoid the copy semantics associated with a regular property getter. public readonly RespContext Context; - private readonly CancellationTokenSource? _source; + private readonly IDisposable? _source; - internal Lifetime(CancellationTokenSource? source, in RespContext context, CancellationToken cancellationToken) + internal Lifetime(IDisposable? source, in RespContext context, CancellationToken cancellationToken) { _source = source; Context = context; // snapshot, we can now mutate this locally diff --git a/src/RESPite/readme.md b/src/RESPite/readme.md index 4bd628f65..6bbbb2a56 100644 --- a/src/RESPite/readme.md +++ b/src/RESPite/readme.md @@ -78,22 +78,34 @@ for *both* the synchronous and asynchronous versions! We can supply our own canc ``` c# var ctx = conn.Context.WithCancellationToken(request.CancellationToken); +// use ctx for commands ``` Now `ctx` is not just the *default* context - it has the cancellation token we supplied, and it is used -everywhere automatically! If you're thinking "Wait - does that *replace* the cancellation, or -*combine* the two cancellations?", then: have a cookie. The answer is "replace", but we can also combine +everywhere automatically! The `RespContext` type is cheap and allocation-free; it has no lifetime etc - it +is just a bundle of state required for RESP operations. We can freely `With...` them: + +``` c# +var db = conn.Context.WithDatabase(4).WithCancellationToken(request.CancellationToken); +// use db for commands +``` + +If you're thinking "Wait - if `RespContext` carries cancellation, does `WithCancellationToken(...)` *replace* +the cancellation, or *combine* the two cancellations?", then: have a cookie. The answer is "replace", but we can also combine - noting that now we need to scope that to a lifetime: ``` c# -using var lifetime = conn.Context.WithLinkedCancellationToken(request.CancellationToken); +using var lifetime = db.WithDatabaseWithLinkedCancellationToken(anotherCancellationToken); // use lifetime.Context for commands ``` This will automatically do the most appropriate thing based on whether neither, one, or both tokens -are cancellable. We can do the same thing with a timeout! +are cancellable. We can do the same thing with a timeout: ``` c# -using var lifetime = conn.Context.WithTimeout(TimeSpan.FromSeconds(5)); +using var lifetime = db.WithTimeout(TimeSpan.FromSeconds(5)); // use lifetime.Context for commands ``` + +Note that this timeout applies to the *lifetime*, not individual operations (i.e. if we loop forever +performing fast operations: it will still cancel after five seconds).` From 3d7919c9c95d8359bdaaccdbdb25ca20a4386648 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 30 Aug 2025 12:31:27 +0100 Subject: [PATCH 012/108] formatting --- src/RESPite/readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/RESPite/readme.md b/src/RESPite/readme.md index 6bbbb2a56..95333de75 100644 --- a/src/RESPite/readme.md +++ b/src/RESPite/readme.md @@ -91,8 +91,8 @@ var db = conn.Context.WithDatabase(4).WithCancellationToken(request.Cancellation ``` If you're thinking "Wait - if `RespContext` carries cancellation, does `WithCancellationToken(...)` *replace* -the cancellation, or *combine* the two cancellations?", then: have a cookie. The answer is "replace", but we can also combine -- noting that now we need to scope that to a lifetime: +the cancellation, or *combine* the two cancellations?", then: have a cookie. The answer is "replace", but we can also +combine multiple cancellations, noting that now we need to scope that to a *lifetime*: ``` c# using var lifetime = db.WithDatabaseWithLinkedCancellationToken(anotherCancellationToken); From a0cc13e487914c30a1585c8133f1d83c3652c071 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 30 Aug 2025 13:45:30 +0100 Subject: [PATCH 013/108] more words --- src/RESPite/RespContext.cs | 24 +++++++++++++++++------- src/RESPite/readme.md | 15 ++++++++++++--- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/RESPite/RespContext.cs b/src/RESPite/RespContext.cs index 715ba0885..8e1e4c3c0 100644 --- a/src/RESPite/RespContext.cs +++ b/src/RESPite/RespContext.cs @@ -42,29 +42,32 @@ public RespContext WithCancellationToken(CancellationToken cancellationToken) /// represents the lifetime of the combined operation, and should be /// disposed when complete. /// - public Lifetime WithLinkedCancellationToken(CancellationToken cancellationToken) + public Lifetime WithCombineCancellationToken(CancellationToken cancellationToken) { if (!cancellationToken.CanBeCanceled || cancellationToken == CancellationToken) { // would have no effect CancellationToken.ThrowIfCancellationRequested(); - return new(null, in this, CancellationToken); + return new(in this, null); } cancellationToken.ThrowIfCancellationRequested(); if (!CancellationToken.CanBeCanceled) { // we don't currently have cancellation; no need for a link - return new(null, in this, cancellationToken); + return new(in this, null, cancellationToken); } CancellationToken.ThrowIfCancellationRequested(); var src = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, CancellationToken); - return new(src, in this, src.Token); + return new(in this, src, src.Token); } - public Lifetime WithTimeout(TimeSpan timeout) + public Lifetime WithCombine(IDisposable lifetime) + => new(in this, lifetime); + + public Lifetime WithCombineTimeout(TimeSpan timeout) { if (timeout <= TimeSpan.Zero) Throw(); CancellationTokenSource src; @@ -78,9 +81,10 @@ public Lifetime WithTimeout(TimeSpan timeout) { src = new CancellationTokenSource(timeout); } + static void Throw() => throw new ArgumentOutOfRangeException(nameof(timeout)); - return new Lifetime(src, in this, src.Token); + return new Lifetime(in this, src, src.Token); } public readonly struct Lifetime : IDisposable @@ -91,10 +95,16 @@ public Lifetime WithTimeout(TimeSpan timeout) private readonly IDisposable? _source; - internal Lifetime(IDisposable? source, in RespContext context, CancellationToken cancellationToken) + internal Lifetime(in RespContext context, IDisposable? source) { + Context = context; _source = source; + } + + internal Lifetime(in RespContext context, IDisposable? source, CancellationToken cancellationToken) + { Context = context; // snapshot, we can now mutate this locally + _source = source; Unsafe.AsRef(in Context.CancellationToken) = cancellationToken; } diff --git a/src/RESPite/readme.md b/src/RESPite/readme.md index 95333de75..cdd2048ba 100644 --- a/src/RESPite/readme.md +++ b/src/RESPite/readme.md @@ -95,7 +95,7 @@ the cancellation, or *combine* the two cancellations?", then: have a cookie. The combine multiple cancellations, noting that now we need to scope that to a *lifetime*: ``` c# -using var lifetime = db.WithDatabaseWithLinkedCancellationToken(anotherCancellationToken); +using var lifetime = db.WithDatabase(4).WithCombineCancellationToken(anotherCancellationToken); // use lifetime.Context for commands ``` @@ -103,9 +103,18 @@ This will automatically do the most appropriate thing based on whether neither, are cancellable. We can do the same thing with a timeout: ``` c# -using var lifetime = db.WithTimeout(TimeSpan.FromSeconds(5)); +using var lifetime = db.WithCombineTimeout(TimeSpan.FromSeconds(5)); // use lifetime.Context for commands ``` Note that this timeout applies to the *lifetime*, not individual operations (i.e. if we loop forever -performing fast operations: it will still cancel after five seconds).` +performing fast operations: it will still cancel after five seconds). From the name +`WithCombineTimeout`, you can probably guess that this works *in addition to* the +existing cancellation state. Help yourself to another cookie. + +## Summary + +With the combination of `RespConnection` for the long-lived connection, +`RespContext` for the transient local configuration (via various `With*` methods), +and our automatically generated `[RespCommand]` methods: we can easily and +efficiently talk to a range of RESP databases.~~~~ From 9bdde8b63595a04ff461192b5fa6269fa3fc4f6f Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 30 Aug 2025 13:46:05 +0100 Subject: [PATCH 014/108] I need to disable ctrl+shift+s in md files --- src/RESPite/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RESPite/readme.md b/src/RESPite/readme.md index cdd2048ba..541d89525 100644 --- a/src/RESPite/readme.md +++ b/src/RESPite/readme.md @@ -117,4 +117,4 @@ existing cancellation state. Help yourself to another cookie. With the combination of `RespConnection` for the long-lived connection, `RespContext` for the transient local configuration (via various `With*` methods), and our automatically generated `[RespCommand]` methods: we can easily and -efficiently talk to a range of RESP databases.~~~~ +efficiently talk to a range of RESP databases. From e9f78a340961c5475baacf08b553a796c4a0b17d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 30 Aug 2025 16:02:42 +0100 Subject: [PATCH 015/108] unit tests --- src/RESPite/Internal/RespMessageBase.cs | 47 +++++++----- src/RESPite/RespOperation.cs | 32 +++++++-- src/RESPite/RespOperationT.cs | 8 +-- tests/RESP.Core.Tests/OperationUnitTests.cs | 79 ++++++++++++++++++++- 4 files changed, 136 insertions(+), 30 deletions(-) diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs index e90a75450..474808b0b 100644 --- a/src/RESPite/Internal/RespMessageBase.cs +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading.Tasks.Sources; using RESPite.Messages; @@ -8,6 +9,8 @@ namespace RESPite.Internal; internal abstract class RespMessageBase : IRespMessage, IValueTaskSource { + protected RespMessageBase() => RespOperation.DebugOnAllocateMessage(); + private CancellationToken _cancellationToken; private CancellationTokenRegistration _cancellationTokenRegistration; @@ -46,7 +49,7 @@ protected void InitParser(object? parser) [Conditional("DEBUG")] private void DebugAssertPending() => Debug.Assert( - GetStatus(_asyncCore.Version) == ValueTaskSourceStatus.Pending & !HasFlag(Flag_OutcomeKnown), + _asyncCore.GetStatus(_asyncCore.Version) == ValueTaskSourceStatus.Pending & !HasFlag(Flag_OutcomeKnown), "Message should be in a pending state"); public bool TrySetResult(short token, scoped ReadOnlySpan response) @@ -131,20 +134,25 @@ private bool SetFlag(int flag) public void OnSent(short token) { - if (GetStatus(token) != ValueTaskSourceStatus.Pending || _requestRefCount != 0 || !SetFlag(Flag_Sent)) + if (_asyncCore.GetStatus(token) != ValueTaskSourceStatus.Pending || _requestRefCount != 0 || !SetFlag(Flag_Sent)) { throw new InvalidOperationException( "Operation must be in a pending, unsent state with no request payload. "); } } - public RespMessageBase Init(byte[] oversized, int offset, int length, ArrayPool? pool, CancellationToken cancellation) + public RespMessageBase Init(byte[]? oversized, int offset, int length, ArrayPool? pool, CancellationToken cancellation) { DebugAssertPending(); Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); - _request = new ReadOnlyMemory(oversized, offset, length); - _requestOwner = pool; - _requestRefCount = 1; + + if (oversized is not null) + { + _requestOwner = pool; + _request = new ReadOnlyMemory(oversized, offset, length); + _requestRefCount = 1; + } + if (cancellation.CanBeCanceled) { _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellation); @@ -250,20 +258,26 @@ private bool } } - ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _asyncCore.GetStatus(token); - ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _asyncCore.GetStatus(token); - - /* if they're awaiting our object directly (i.e. we don't need to worry about Task pre-checking things), - then we can tell them that a message hasn't been sent, for example transactions / batches */ + /* asking about the status too early is usually a very bad sign that they're doing + something like awaiting a message in a transaction that hasn't been sent */ public ValueTaskSourceStatus GetStatus(short token) { // we'd rather see a token error, so check that first // (in reality, we expect the token to be right almost always) var status = _asyncCore.GetStatus(token); - if (!HasFlag(Flag_Sent)) ThrowNotSent(); + if ((status == ValueTaskSourceStatus.Pending & !HasFlag(Flag_Sent)) && TrySetExceptionNotSent()) + { + status = ValueTaskSourceStatus.Faulted; + Debug.Assert(_asyncCore.GetStatus(token) == ValueTaskSourceStatus.Faulted, "should be faulted"); + } return status; } + [MethodImpl(MethodImplOptions.NoInlining)] + private bool TrySetExceptionNotSent() + => TrySetException(new InvalidOperationException( + "This command has not yet been sent; awaiting is not possible. If this is a transaction or batch, you must execute that first.")); + private void CheckToken(short token) { if (token != _asyncCore.Version) // use cheap test @@ -272,10 +286,6 @@ private void CheckToken(short token) } } - private static void ThrowNotSent() - => throw new InvalidOperationException( - "This command has not yet been sent; awaiting is not possible. If this is a transaction or batch, you must execute that first."); - public void OnCompleted( Action continuation, object? state, @@ -307,7 +317,10 @@ public TResponse Wait(short token, TimeSpan timeout) case Flag_Complete | Flag_Sent: // already complete return GetResult(token); default: - ThrowNotSent(); + if (TrySetExceptionNotSent()) + { + return GetResult(token); + } break; } diff --git a/src/RESPite/RespOperation.cs b/src/RESPite/RespOperation.cs index 17106dd7a..aaf3f3a0d 100644 --- a/src/RESPite/RespOperation.cs +++ b/src/RESPite/RespOperation.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.ComponentModel; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading.Tasks.Sources; using RESPite.Internal; @@ -16,6 +17,23 @@ namespace RESPite; /// public readonly struct RespOperation : ICriticalNotifyCompletion { +#if DEBUG + [ThreadStatic] + // how many resp-operations have we chewed through? + private static int _debugPerThreadMessageAllocations; + internal static int DebugPerThreadMessageAllocations => _debugPerThreadMessageAllocations; +#else + internal static int DebugPerThreadMessageAllocations => 0; +#endif + + [Conditional("DEBUG")] + internal static void DebugOnAllocateMessage() + { +#if DEBUG + _debugPerThreadMessageAllocations++; +#endif + } + // it is important that this layout remains identical between RespOperation and RespOperation private readonly IRespMessage _message; private readonly short _token; @@ -40,11 +58,9 @@ public static implicit operator ValueTask(in RespOperation operation) => new(operation.Message, operation._token); /// - public Task AsTask() - { - ValueTask vt = this; - return vt.AsTask(); - } + public Task AsTask() => new ValueTask(Message, _token).AsTask(); + + public ValueTask AsValueTask() => new(Message, _token); /// public void Wait(TimeSpan timeout = default) @@ -112,6 +128,7 @@ public readonly struct Remote { private readonly IRespMessage _message; private readonly short _token; + internal Remote(IRespMessage message) { _message = message; @@ -146,9 +163,10 @@ public bool TrySetResult(in ReadOnlySequence response) /// Create a disconnected without a RESP parser; this is only intended for testing purposes. /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - public static RespOperation Create(out Remote remote) + public static RespOperation Create(out Remote remote, CancellationToken token = default) { - var msg = RespMessage.Get(null); + var msg = RespMessage.Get(null) + .Init(null, 0, 0, null, token); remote = new(msg); return new RespOperation(msg); } diff --git a/src/RESPite/RespOperationT.cs b/src/RESPite/RespOperationT.cs index d9cd9fad1..b23f0900b 100644 --- a/src/RESPite/RespOperationT.cs +++ b/src/RESPite/RespOperationT.cs @@ -53,11 +53,9 @@ public static implicit operator ValueTask(in RespOperation operation) => new(operation.TypedMessage, operation._token); /// - public Task AsTask() - { - ValueTask vt = this; - return vt.AsTask(); - } + public Task AsTask() => new ValueTask(TypedMessage, _token).AsTask(); + + public ValueTask AsValueTask() => new(TypedMessage, _token); /// public T Wait(TimeSpan timeout = default) diff --git a/tests/RESP.Core.Tests/OperationUnitTests.cs b/tests/RESP.Core.Tests/OperationUnitTests.cs index 73f93b9f3..68b6fcdfe 100644 --- a/tests/RESP.Core.Tests/OperationUnitTests.cs +++ b/tests/RESP.Core.Tests/OperationUnitTests.cs @@ -1,7 +1,11 @@ using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; using RESPite; using Xunit; +using Xunit.Internal; namespace RESP.Core.Tests; @@ -11,10 +15,66 @@ namespace RESP.Core.Tests; Justification = "This isn't actually async; we're testing an awaitable.")] public class OperationUnitTests { + private static CancellationToken CancellationToken => TestContext.Current.CancellationToken; + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ManuallyImplementedAsync_NotSent(bool sent) + { + var op = RespOperation.Create(out var remote, CancellationToken); + if (sent) remote.OnSent(); + var awaiter = op.GetAwaiter(); + if (sent) + { + Assert.False(awaiter.IsCompleted); + } + else + { + Assert.True(awaiter.IsFaulted); + var ex = Assert.Throws(() => awaiter.GetResult()); + Assert.Contains("This command has not yet been sent", ex.Message); + } + } + + [Fact] + public void UnsentDetectedSync() + { + var op = RespOperation.Create(out var remote, CancellationToken); + var ex = Assert.Throws(() => op.Wait()); + Assert.Contains("This command has not yet been sent", ex.Message); + } + + [Fact] + public async Task UnsentDetected_Operation_Async() + { + var op = RespOperation.Create(out var remote, CancellationToken); + var ex = await Assert.ThrowsAsync(async () => await op); + Assert.Contains("This command has not yet been sent", ex.Message); + } + + [Fact] + public async Task UnsentDetected_ValueTask_Async() + { + var op = RespOperation.Create(out var remote, CancellationToken); + var ex = await Assert.ThrowsAsync(async () => await op.AsValueTask()); + Assert.Contains("This command has not yet been sent", ex.Message); + } + + [Fact] + public async Task UnsentDetected_Task_Async() + { + var op = RespOperation.Create(out var remote, CancellationToken); + var ex = await Assert.ThrowsAsync(async () => await op.AsTask()); + Assert.Contains("This command has not yet been sent", ex.Message); + } + [Fact] public void CanCreateAndCompleteOperation() { - var op = RespOperation.Create(out var remote); + var op = RespOperation.Create(out var remote, CancellationToken); + remote.OnSent(); + // initial state Assert.False(op.IsCanceled); Assert.False(op.IsCompleted); @@ -52,4 +112,21 @@ public void CanCreateAndCompleteOperation() #pragma warning restore xUnit1051 Assert.False(remote.TrySetException(null!)); } + + [Fact] + public void CanCreateAndCompleteWithoutLeaking() + { + int before = RespOperation.DebugPerThreadMessageAllocations; + for (int i = 0; i < 100; i++) + { + var op = RespOperation.Create(out var remote, CancellationToken); + remote.OnSent(); + remote.TrySetResult(default); + Assert.True(op.IsCompleted); + op.Wait(); + } + int after = RespOperation.DebugPerThreadMessageAllocations; + var allocs = after - before; + Debug.Assert(allocs < 2, $"allocations: {allocs}"); + } } From e9cd0c99b7787cb150b0754f1afedda693936e7e Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 30 Aug 2025 16:15:35 +0100 Subject: [PATCH 016/108] more tests --- src/RESPite/Internal/RespMessageBase.cs | 3 +++ src/RESPite/Internal/RespMessageBase_Typed.cs | 2 ++ .../RespMessageBase_Typed_Stateful.cs | 2 ++ tests/RESP.Core.Tests/OperationUnitTests.cs | 21 +++++++++++++++++++ 4 files changed, 28 insertions(+) diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs index 474808b0b..8119ef63a 100644 --- a/src/RESPite/Internal/RespMessageBase.cs +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -193,8 +193,11 @@ public virtual void Reset(bool recycle) _requestRefCount = 0; _flags = 0; _asyncCore.Reset(); + if (recycle) Recycle(); } + protected abstract void Recycle(); + public bool TryReserveRequest(short token, out ReadOnlyMemory payload, bool recordSent = true) { while (true) // redo in case of CEX failure diff --git a/src/RESPite/Internal/RespMessageBase_Typed.cs b/src/RESPite/Internal/RespMessageBase_Typed.cs index 1b368bad3..392c59f55 100644 --- a/src/RESPite/Internal/RespMessageBase_Typed.cs +++ b/src/RESPite/Internal/RespMessageBase_Typed.cs @@ -18,6 +18,8 @@ internal static RespMessage Get(IRespParser? parser) return obj; } + protected override void Recycle() => _threadStaticSpare = this; + private RespMessage() { } protected override TResponse Parse(ref RespReader reader) => _parser!.Parse(ref reader); diff --git a/src/RESPite/Internal/RespMessageBase_Typed_Stateful.cs b/src/RESPite/Internal/RespMessageBase_Typed_Stateful.cs index c0a46f363..12d26c143 100644 --- a/src/RESPite/Internal/RespMessageBase_Typed_Stateful.cs +++ b/src/RESPite/Internal/RespMessageBase_Typed_Stateful.cs @@ -20,6 +20,8 @@ internal static RespMessage Get(in TState state, IRespParser< return obj; } + protected override void Recycle() => _threadStaticSpare = this; + private RespMessage() => Unsafe.SkipInit(out _state); protected override TResponse Parse(ref RespReader reader) => _parser!.Parse(in _state, ref reader); diff --git a/tests/RESP.Core.Tests/OperationUnitTests.cs b/tests/RESP.Core.Tests/OperationUnitTests.cs index 68b6fcdfe..cf14cadee 100644 --- a/tests/RESP.Core.Tests/OperationUnitTests.cs +++ b/tests/RESP.Core.Tests/OperationUnitTests.cs @@ -129,4 +129,25 @@ public void CanCreateAndCompleteWithoutLeaking() var allocs = after - before; Debug.Assert(allocs < 2, $"allocations: {allocs}"); } + + [Fact] + public async Task CanCreateAndCompleteWithoutLeaking_Async() + { + var threadId = Environment.CurrentManagedThreadId; + int before = RespOperation.DebugPerThreadMessageAllocations; + for (int i = 0; i < 100; i++) + { + var op = RespOperation.Create(out var remote, CancellationToken); + remote.OnSent(); + remote.TrySetResult(default); + Assert.True(op.IsCompleted); + await op; + } + int after = RespOperation.DebugPerThreadMessageAllocations; + var allocs = after - before; + Debug.Assert(allocs < 2, $"allocations: {allocs}"); + + // do not expect thread switch + Assert.Equal(threadId, Environment.CurrentManagedThreadId); + } } From f8d5faf421bd99b94b42fab3540c0aca98343194 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sun, 31 Aug 2025 11:41:04 +0100 Subject: [PATCH 017/108] awaitable unit tests --- src/RESPite/Internal/IRespMessage.cs | 8 +- src/RESPite/Internal/RespMessageBase.cs | 154 +++++++++++------ src/RESPite/RespOperation.cs | 35 ++-- src/RESPite/RespOperationT.cs | 6 +- tests/RESP.Core.Tests/OperationUnitTests.cs | 179 +++++++++++++++----- 5 files changed, 269 insertions(+), 113 deletions(-) diff --git a/src/RESPite/Internal/IRespMessage.cs b/src/RESPite/Internal/IRespMessage.cs index 43bc4ff2b..da5342358 100644 --- a/src/RESPite/Internal/IRespMessage.cs +++ b/src/RESPite/Internal/IRespMessage.cs @@ -16,5 +16,11 @@ internal interface IRespMessage : IValueTaskSource bool AllowInlineParsing { get; } short Token { get; } ref readonly CancellationToken CancellationToken { get; } - void OnSent(short token); + bool IsSent(short token); + + void OnCompletedWithNotSentDetection( + Action continuation, + object? state, + short token, + ValueTaskSourceOnCompletedFlags flags); } diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs index 8119ef63a..f403e7829 100644 --- a/src/RESPite/Internal/RespMessageBase.cs +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -36,10 +36,16 @@ private const int protected void InitParser(object? parser) { - if (parser is not null) + if (parser is null) + { + SetFlag(Flag_InlineParser); // F+F + } + else { int flags = Flag_Parser; + // detect parsers that want to manually parse attributes, errors, etc. if (parser is IRespMetadataParser) flags |= Flag_MetadataParser; + // detect fast, internal, non-allocating parsers (int, bool, etc.) if (parser is IRespInlineParser) flags |= Flag_InlineParser; SetFlag(flags); } @@ -47,14 +53,8 @@ protected void InitParser(object? parser) public bool AllowInlineParsing => HasFlag(Flag_InlineParser); - [Conditional("DEBUG")] - private void DebugAssertPending() => Debug.Assert( - _asyncCore.GetStatus(_asyncCore.Version) == ValueTaskSourceStatus.Pending & !HasFlag(Flag_OutcomeKnown), - "Message should be in a pending state"); - public bool TrySetResult(short token, scoped ReadOnlySpan response) { - DebugAssertPending(); if (HasFlag(Flag_OutcomeKnown) | _asyncCore.Version != token) return false; var flags = _flags & (Flag_MetadataParser | Flag_Parser); switch (flags) @@ -69,22 +69,27 @@ public bool TrySetResult(short token, scoped ReadOnlySpan response) reader.MoveNext(); } - return TrySetResult(Parse(ref reader)); + return TrySetResultPrecheckedToken(Parse(ref reader)); } catch (Exception ex) { - return TrySetException(ex); + return TrySetExceptionPrecheckedToken(ex); } default: - return TrySetResult(default(TResponse)!); + return TrySetResultPrecheckedToken(default(TResponse)!); } } public short Token => _asyncCore.Version; + public bool IsSent(short token) + { + CheckToken(token); + return HasFlag(Flag_Sent); + } + public bool TrySetResult(short token, in ReadOnlySequence response) { - DebugAssertPending(); if (HasFlag(Flag_OutcomeKnown) | _asyncCore.Version != token) return false; var flags = _flags & (Flag_MetadataParser | Flag_Parser); switch (flags) @@ -99,14 +104,14 @@ public bool TrySetResult(short token, in ReadOnlySequence response) reader.MoveNext(); } - return TrySetResult(Parse(ref reader)); + return TrySetResultPrecheckedToken(Parse(ref reader)); } catch (Exception ex) { - return TrySetException(ex); + return TrySetExceptionPrecheckedToken(ex); } default: - return TrySetResult(default(TResponse)!); + return TrySetResultPrecheckedToken(default(TResponse)!); } } @@ -132,18 +137,26 @@ private bool SetFlag(int flag) // in the "any" sense private bool HasFlag(int flag) => (Volatile.Read(ref _flags) & flag) != 0; - public void OnSent(short token) + public RespMessageBase Init(bool sent, CancellationToken cancellationToken) { - if (_asyncCore.GetStatus(token) != ValueTaskSourceStatus.Pending || _requestRefCount != 0 || !SetFlag(Flag_Sent)) + Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); + if (sent) SetFlag(Flag_Sent); + if (cancellationToken.CanBeCanceled) { - throw new InvalidOperationException( - "Operation must be in a pending, unsent state with no request payload. "); + _cancellationToken = cancellationToken; + _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellationToken); } + + return this; } - public RespMessageBase Init(byte[]? oversized, int offset, int length, ArrayPool? pool, CancellationToken cancellation) + public RespMessageBase Init( + byte[]? oversized, + int offset, + int length, + ArrayPool? pool, + CancellationToken cancellationToken) { - DebugAssertPending(); Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); if (oversized is not null) @@ -153,24 +166,28 @@ public RespMessageBase Init(byte[]? oversized, int offset, int length _requestRefCount = 1; } - if (cancellation.CanBeCanceled) + if (cancellationToken.CanBeCanceled) { - _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellation); + _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellationToken); } + return this; } - public RespMessageBase SetRequest(ReadOnlyMemory request, IDisposable? owner, CancellationToken cancellation) + public RespMessageBase SetRequest( + ReadOnlyMemory request, + IDisposable? owner, + CancellationToken cancellationToken) { - DebugAssertPending(); Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); _request = request; _requestOwner = owner; _requestRefCount = 1; - if (cancellation.CanBeCanceled) + if (cancellationToken.CanBeCanceled) { - _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellation); + _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellationToken); } + return this; } @@ -210,6 +227,7 @@ public bool TryReserveRequest(short token, out ReadOnlyMemory payload, boo payload = default; return false; } + if (Interlocked.CompareExchange(ref _requestRefCount, checked(oldCount + 1), oldCount) == oldCount) { if (recordSent) SetFlag(Flag_Sent); @@ -264,22 +282,23 @@ private bool /* asking about the status too early is usually a very bad sign that they're doing something like awaiting a message in a transaction that hasn't been sent */ public ValueTaskSourceStatus GetStatus(short token) + => _asyncCore.GetStatus(token); + + [MethodImpl(MethodImplOptions.NoInlining)] + private void ThrowNotSent(short token) { - // we'd rather see a token error, so check that first - // (in reality, we expect the token to be right almost always) - var status = _asyncCore.GetStatus(token); - if ((status == ValueTaskSourceStatus.Pending & !HasFlag(Flag_Sent)) && TrySetExceptionNotSent()) - { - status = ValueTaskSourceStatus.Faulted; - Debug.Assert(_asyncCore.GetStatus(token) == ValueTaskSourceStatus.Faulted, "should be faulted"); - } - return status; + CheckToken(token); // prefer a token explanation + throw new InvalidOperationException( + "This command has not yet been sent; waiting is not possible. If this is a transaction or batch, you must execute that first."); } [MethodImpl(MethodImplOptions.NoInlining)] - private bool TrySetExceptionNotSent() - => TrySetException(new InvalidOperationException( + private void SetNotSentAsync(short token) + { + CheckToken(token); + TrySetExceptionPrecheckedToken(new InvalidOperationException( "This command has not yet been sent; awaiting is not possible. If this is a transaction or batch, you must execute that first.")); + } private void CheckToken(short token) { @@ -289,25 +308,49 @@ private void CheckToken(short token) } } + // this is used from Task/ValueTask; we can't avoid that - in theory + // we *coiuld* sort of make it work for ValueTask, but if anyone + // calls .AsTask() on it, it would fail public void OnCompleted( Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) { + SetFlag(Flag_NoPulse); // async doesn't need to be pulsed _asyncCore.OnCompleted(continuation, state, token, flags); + } + + public void OnCompletedWithNotSentDetection( + Action continuation, + object? state, + short token, + ValueTaskSourceOnCompletedFlags flags) + { + if (!HasFlag(Flag_Sent)) SetNotSentAsync(token); SetFlag(Flag_NoPulse); // async doesn't need to be pulsed + _asyncCore.OnCompleted(continuation, state, token, flags); } // spoof untyped on top of typed void IValueTaskSource.GetResult(short token) => _ = GetResult(token); void IRespMessage.Wait(short token, TimeSpan timeout) => _ = Wait(token, timeout); - private bool TrySetOutcomeKnown() + private bool TrySetOutcomeKnown(short token, bool withSuccess) + => _asyncCore.Version == token && TrySetOutcomeKnownPrecheckedToken(withSuccess); + + private bool TrySetOutcomeKnownPrecheckedToken(bool withSuccess) { - DebugAssertPending(); if (!SetFlag(Flag_OutcomeKnown)) return false; UnregisterCancellation(); + + // configure threading model; failure can be triggered from any thread - *always* + // dispatch to pool; in the success case, we're either on the IO thread + // (if inline-parsing is enabled) - in which case, yes: dispatch - or we've + // already jumped to a pool thread for the parse step. So: the only + // time we want to complete inline is success and not inline-parsing. + _asyncCore.RunContinuationsAsynchronously = !withSuccess | AllowInlineParsing; + return true; } @@ -320,10 +363,7 @@ public TResponse Wait(short token, TimeSpan timeout) case Flag_Complete | Flag_Sent: // already complete return GetResult(token); default: - if (TrySetExceptionNotSent()) - { - return GetResult(token); - } + ThrowNotSent(token); // always throws break; } @@ -356,7 +396,7 @@ public TResponse Wait(short token, TimeSpan timeout) } UnregisterCancellation(); - if (isTimeout) TrySetTimeout(); + if (isTimeout) TrySetTimeoutPrecheckedToken(); return GetResult(token); @@ -364,18 +404,18 @@ public TResponse Wait(short token, TimeSpan timeout) "This operation cannot be waited because it entered async/await mode - most likely by calling AsTask()"); } - private bool TrySetResult(TResponse response) + private bool TrySetResultPrecheckedToken(TResponse response) { - if (!TrySetOutcomeKnown()) return false; + if (!TrySetOutcomeKnownPrecheckedToken(true)) return false; _asyncCore.SetResult(response); SetFullyComplete(success: true); return true; } - private bool TrySetTimeout() + private bool TrySetTimeoutPrecheckedToken() { - if (!TrySetOutcomeKnown()) return false; + if (!TrySetOutcomeKnownPrecheckedToken(false)) return false; _asyncCore.SetException(new TimeoutException()); SetFullyComplete(success: false); @@ -389,26 +429,28 @@ public bool TrySetCanceled(short token, CancellationToken cancellationToken = de // use our own token if nothing more specific supplied cancellationToken = _cancellationToken; } - return TrySetCanceled(cancellationToken); + + return token == _asyncCore.Version && TrySetCanceledPrecheckedToken(cancellationToken); } - // this is the path used by cancellation registration callbacks; always use our own token - void IRespMessage.TrySetCanceled() => TrySetCanceled(_cancellationToken); + // this is the path used by cancellation registration callbacks; always use our own + // cancellation token, and we must trust the version token + void IRespMessage.TrySetCanceled() => TrySetCanceledPrecheckedToken(_cancellationToken); - private bool TrySetCanceled(CancellationToken cancellationToken) + private bool TrySetCanceledPrecheckedToken(CancellationToken cancellationToken) { - if (!TrySetOutcomeKnown()) return false; + if (!TrySetOutcomeKnownPrecheckedToken(false)) return false; _asyncCore.SetException(new OperationCanceledException(cancellationToken)); SetFullyComplete(success: false); return true; } public bool TrySetException(short token, Exception exception) - => token == _asyncCore.Version && TrySetException(exception); + => token == _asyncCore.Version && TrySetExceptionPrecheckedToken(exception); - private bool TrySetException(Exception exception) + private bool TrySetExceptionPrecheckedToken(Exception exception) { - if (!TrySetOutcomeKnown()) return false; // first winner only + if (!TrySetOutcomeKnownPrecheckedToken(false)) return false; // first winner only _asyncCore.SetException(exception); SetFullyComplete(success: false); return true; diff --git a/src/RESPite/RespOperation.cs b/src/RESPite/RespOperation.cs index aaf3f3a0d..ca5b9ce1c 100644 --- a/src/RESPite/RespOperation.cs +++ b/src/RESPite/RespOperation.cs @@ -21,6 +21,7 @@ namespace RESPite; [ThreadStatic] // how many resp-operations have we chewed through? private static int _debugPerThreadMessageAllocations; + internal static int DebugPerThreadMessageAllocations => _debugPerThreadMessageAllocations; #else internal static int DebugPerThreadMessageAllocations => 0; @@ -46,6 +47,7 @@ internal RespOperation(IRespMessage message, bool disableCaptureContext = false) _disableCaptureContext = disableCaptureContext; } + public bool IsSent => Message.IsSent(_token); internal IRespMessage Message => _message ?? ThrowNoMessage(); internal static IRespMessage ThrowNoMessage() @@ -92,7 +94,7 @@ public void OnCompleted(Action continuation) ? ValueTaskSourceOnCompletedFlags.FlowExecutionContext : ValueTaskSourceOnCompletedFlags.FlowExecutionContext | ValueTaskSourceOnCompletedFlags.UseSchedulingContext; - Message.OnCompleted(InvokeState, continuation, _token, flags); + Message.OnCompletedWithNotSentDetection(InvokeState, continuation, _token, flags); } /// @@ -102,7 +104,7 @@ public void UnsafeOnCompleted(Action continuation) var flags = _disableCaptureContext ? ValueTaskSourceOnCompletedFlags.None : ValueTaskSourceOnCompletedFlags.UseSchedulingContext; - Message.OnCompleted(InvokeState, continuation, _token, flags); + Message.OnCompletedWithNotSentDetection(InvokeState, continuation, _token, flags); } /// @@ -135,10 +137,7 @@ internal Remote(IRespMessage message) _token = message.Token; } - /// - /// Record the operation as sent. - /// - public void OnSent() => _message.OnSent(_token); + public bool IsTokenMatch => _token == _message.Token; /// public bool TrySetCanceled(CancellationToken cancellationToken = default) @@ -163,10 +162,12 @@ public bool TrySetResult(in ReadOnlySequence response) /// Create a disconnected without a RESP parser; this is only intended for testing purposes. /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - public static RespOperation Create(out Remote remote, CancellationToken token = default) + public static RespOperation Create( + out Remote remote, + bool sent = true, + CancellationToken cancellationToken = default) { - var msg = RespMessage.Get(null) - .Init(null, 0, 0, null, token); + var msg = RespMessage.Get(null).Init(sent, cancellationToken); remote = new(msg); return new RespOperation(msg); } @@ -176,9 +177,13 @@ public static RespOperation Create(out Remote remote, CancellationToken token = /// /// The result of the operation. [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - public static RespOperation Create(IRespParser parser, out Remote remote) + public static RespOperation Create( + IRespParser? parser, + out Remote remote, + bool sent = true, + CancellationToken cancellationToken = default) { - var msg = RespMessage.Get(parser); + var msg = RespMessage.Get(parser).Init(sent, cancellationToken); remote = new(msg); return new RespOperation(msg); } @@ -191,10 +196,12 @@ public static RespOperation Create(IRespParser parser [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public static RespOperation Create( in TState state, - IRespParser parser, - out Remote remote) + IRespParser? parser, + out Remote remote, + bool sent = true, + CancellationToken cancellationToken = default) { - var msg = RespMessage.Get(in state, parser); + var msg = RespMessage.Get(in state, parser).Init(sent, cancellationToken); remote = new(msg); return new RespOperation(msg); } diff --git a/src/RESPite/RespOperationT.cs b/src/RESPite/RespOperationT.cs index b23f0900b..aa4a0c55b 100644 --- a/src/RESPite/RespOperationT.cs +++ b/src/RESPite/RespOperationT.cs @@ -82,9 +82,11 @@ public void OnCompleted(Action continuation) ? ValueTaskSourceOnCompletedFlags.FlowExecutionContext : ValueTaskSourceOnCompletedFlags.FlowExecutionContext | ValueTaskSourceOnCompletedFlags.UseSchedulingContext; - TypedMessage.OnCompleted(RespOperation.InvokeState, continuation, _token, flags); + TypedMessage.OnCompletedWithNotSentDetection(RespOperation.InvokeState, continuation, _token, flags); } + public bool IsSent => TypedMessage.IsSent(_token); + /// public void UnsafeOnCompleted(Action continuation) { @@ -92,7 +94,7 @@ public void UnsafeOnCompleted(Action continuation) var flags = _disableCaptureContext ? ValueTaskSourceOnCompletedFlags.None : ValueTaskSourceOnCompletedFlags.UseSchedulingContext; - TypedMessage.OnCompleted(RespOperation.InvokeState, continuation, _token, flags); + TypedMessage.OnCompletedWithNotSentDetection(RespOperation.InvokeState, continuation, _token, flags); } /// diff --git a/tests/RESP.Core.Tests/OperationUnitTests.cs b/tests/RESP.Core.Tests/OperationUnitTests.cs index cf14cadee..5872a080f 100644 --- a/tests/RESP.Core.Tests/OperationUnitTests.cs +++ b/tests/RESP.Core.Tests/OperationUnitTests.cs @@ -18,62 +18,161 @@ public class OperationUnitTests private static CancellationToken CancellationToken => TestContext.Current.CancellationToken; [Theory] - [InlineData(true)] - [InlineData(false)] - public void ManuallyImplementedAsync_NotSent(bool sent) + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(false, true)] + public void ManuallyImplementedAsync_NotSent_Untyped(bool sent, bool @unsafe) { - var op = RespOperation.Create(out var remote, CancellationToken); - if (sent) remote.OnSent(); + var op = RespOperation.Create(out var remote, sent, cancellationToken: CancellationToken); + Assert.Equal(sent, op.IsSent); var awaiter = op.GetAwaiter(); + Assert.False(awaiter.IsCompleted, "not completed first IsCompleted check"); + + if (@unsafe) + { + op.UnsafeOnCompleted(() => { }); + } + else + { + op.OnCompleted(() => { }); + } + if (sent) { - Assert.False(awaiter.IsCompleted); + Assert.False(awaiter.IsCompleted, "incomplete after OnCompleted"); + Assert.True(remote.TrySetResult(default)); + awaiter.GetResult(); } else { - Assert.True(awaiter.IsFaulted); + Assert.True(awaiter.IsFaulted, "faulted after OnCompleted"); + Assert.False(remote.TrySetResult(default)); var ex = Assert.Throws(() => awaiter.GetResult()); Assert.Contains("This command has not yet been sent", ex.Message); } + Assert.Throws(() => awaiter.GetResult()); } - [Fact] + [Theory] + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(false, true)] + public void ManuallyImplementedAsync_NotSent_Typed(bool sent, bool @unsafe) + { + var op = RespOperation.Create(null, out var remote, sent, cancellationToken: CancellationToken); + Assert.Equal(sent, op.IsSent); + var awaiter = op.GetAwaiter(); + Assert.False(awaiter.IsCompleted, "not completed first IsCompleted check"); + + if (@unsafe) + { + op.UnsafeOnCompleted(() => { }); + } + else + { + op.OnCompleted(() => { }); + } + + if (sent) + { + Assert.False(awaiter.IsCompleted, "incomplete after OnCompleted"); + Assert.True(remote.TrySetResult(default)); + awaiter.GetResult(); + } + else + { + Assert.True(awaiter.IsFaulted, "faulted after OnCompleted"); + Assert.False(remote.TrySetResult(default)); + var ex = Assert.Throws(() => awaiter.GetResult()); + Assert.Contains("This command has not yet been sent", ex.Message); + } + Assert.Throws(() => awaiter.GetResult()); + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(false, true)] + public void ManuallyImplementedAsync_NotSent_Stateful(bool sent, bool @unsafe) + { + var op = RespOperation.Create("abc", null, out var remote, sent, CancellationToken); + Assert.Equal(sent, op.IsSent); + var awaiter = op.GetAwaiter(); + Assert.False(awaiter.IsCompleted, "not completed first IsCompleted check"); + + if (@unsafe) + { + op.UnsafeOnCompleted(() => { }); + } + else + { + op.OnCompleted(() => { }); + } + + if (sent) + { + Assert.False(awaiter.IsCompleted, "incomplete after OnCompleted"); + Assert.True(remote.TrySetResult(default)); + awaiter.GetResult(); + } + else + { + Assert.True(awaiter.IsFaulted, "faulted after OnCompleted"); + Assert.False(remote.TrySetResult(default)); + var ex = Assert.Throws(() => awaiter.GetResult()); + Assert.Contains("This command has not yet been sent", ex.Message); + } + Assert.Throws(() => awaiter.GetResult()); + } + + [Fact(Timeout = 1000)] public void UnsentDetectedSync() { - var op = RespOperation.Create(out var remote, CancellationToken); + var op = RespOperation.Create(out var remote, false, CancellationToken); var ex = Assert.Throws(() => op.Wait()); Assert.Contains("This command has not yet been sent", ex.Message); } - [Fact] + [Fact(Timeout = 1000)] public async Task UnsentDetected_Operation_Async() { - var op = RespOperation.Create(out var remote, CancellationToken); + var op = RespOperation.Create(out var remote, false, CancellationToken); + Assert.False(op.IsCompleted); var ex = await Assert.ThrowsAsync(async () => await op); Assert.Contains("This command has not yet been sent", ex.Message); } - [Fact] - public async Task UnsentDetected_ValueTask_Async() + [Fact(Timeout = 1000)] + public async Task UnsentNotDetected_ValueTask_Async() { - var op = RespOperation.Create(out var remote, CancellationToken); - var ex = await Assert.ThrowsAsync(async () => await op.AsValueTask()); - Assert.Contains("This command has not yet been sent", ex.Message); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken); + cts.CancelAfter(100); + var op = RespOperation.Create(out var remote, false, cts.Token); + var ex = await Assert.ThrowsAsync(async () => await op.AsValueTask()); + Assert.True(ex.CancellationToken.IsCancellationRequested, "CT token should be intact"); + Assert.True(cts.Token != CancellationToken, "should not be test CT"); + Assert.True(cts.Token == ex.CancellationToken, "should be local CT"); } - [Fact] - public async Task UnsentDetected_Task_Async() + [Fact(Timeout = 1000)] + public async Task UnsentNotDetected_Task_Async() { - var op = RespOperation.Create(out var remote, CancellationToken); - var ex = await Assert.ThrowsAsync(async () => await op.AsTask()); - Assert.Contains("This command has not yet been sent", ex.Message); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken); + cts.CancelAfter(100); + var op = RespOperation.Create(out var remote, false, cts.Token); + var ex = await Assert.ThrowsAsync(async () => await op.AsTask()); + Assert.True(ex.CancellationToken.IsCancellationRequested, "CT token should be intact"); + Assert.True(cts.Token != CancellationToken, "should not be test CT"); + Assert.True(cts.Token == ex.CancellationToken, "should be local CT"); } - [Fact] + [Fact(Timeout = 1000)] public void CanCreateAndCompleteOperation() { - var op = RespOperation.Create(out var remote, CancellationToken); - remote.OnSent(); + var op = RespOperation.Create(out var remote, cancellationToken: CancellationToken); // initial state Assert.False(op.IsCanceled); @@ -96,53 +195,53 @@ public void CanCreateAndCompleteOperation() Assert.False(remote.TrySetException(null!)); // can get result + Assert.True(remote.IsTokenMatch, "should match before GetResult"); op.GetResult(); + Assert.False(remote.IsTokenMatch, "should have reset token"); // but only once, after that: bad things - Assert.Throws(() => op.GetResult()); - Assert.Throws(() => op.IsCanceled); - Assert.Throws(() => op.IsCompleted); - Assert.Throws(() => op.IsCompletedSuccessfully); - Assert.Throws(() => op.IsFaulted); + Assert.Throws(() => op.GetResult()); + Assert.Throws(() => op.IsCanceled); + Assert.Throws(() => op.IsCompleted); + Assert.Throws(() => op.IsCompletedSuccessfully); + Assert.Throws(() => op.IsFaulted); // additional completions continue to fail - Assert.False(remote.TrySetResult(default)); -#pragma warning disable xUnit1051 - Assert.False(remote.TrySetCanceled()); -#pragma warning restore xUnit1051 - Assert.False(remote.TrySetException(null!)); + Assert.False(remote.TrySetResult(default), "TrySetResult"); + Assert.False(remote.TrySetCanceled(CancellationToken), "TrySetCanceled"); + Assert.False(remote.TrySetException(null!), "TrySetException"); } - [Fact] + [Fact(Timeout = 1000)] public void CanCreateAndCompleteWithoutLeaking() { int before = RespOperation.DebugPerThreadMessageAllocations; for (int i = 0; i < 100; i++) { - var op = RespOperation.Create(out var remote, CancellationToken); - remote.OnSent(); + var op = RespOperation.Create(out var remote, cancellationToken: CancellationToken); remote.TrySetResult(default); Assert.True(op.IsCompleted); op.Wait(); } + int after = RespOperation.DebugPerThreadMessageAllocations; var allocs = after - before; Debug.Assert(allocs < 2, $"allocations: {allocs}"); } - [Fact] + [Fact(Timeout = 1000)] public async Task CanCreateAndCompleteWithoutLeaking_Async() { var threadId = Environment.CurrentManagedThreadId; int before = RespOperation.DebugPerThreadMessageAllocations; for (int i = 0; i < 100; i++) { - var op = RespOperation.Create(out var remote, CancellationToken); - remote.OnSent(); + var op = RespOperation.Create(out var remote, cancellationToken: CancellationToken); remote.TrySetResult(default); Assert.True(op.IsCompleted); await op; } + int after = RespOperation.DebugPerThreadMessageAllocations; var allocs = after - before; Debug.Assert(allocs < 2, $"allocations: {allocs}"); From cb0b10d35c8b227dea651608fe131f8f3c758b6d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sun, 31 Aug 2025 11:41:04 +0100 Subject: [PATCH 018/108] block buffer serializer --- src/RESPite/Internal/BlockBuffer.cs | 235 ++++++++++++++++++ src/RESPite/Internal/BlockBufferSerializer.cs | 74 ++++++ src/RESPite/Internal/IRespMessage.cs | 8 +- src/RESPite/Internal/RespMessageBase.cs | 154 +++++++----- .../SynchronizedBlockBufferSerializer.cs | 46 ++++ .../ThreadLocalBlockBufferSerializer.cs | 21 ++ src/RESPite/RESPite.csproj | 9 + src/RESPite/RespOperation.cs | 35 +-- src/RESPite/RespOperationT.cs | 6 +- tests/RESP.Core.Tests/BlockBufferTests.cs | 135 ++++++++++ .../{BufferTests.cs => CycleBufferTests.cs} | 2 +- tests/RESP.Core.Tests/OperationUnitTests.cs | 179 ++++++++++--- 12 files changed, 790 insertions(+), 114 deletions(-) create mode 100644 src/RESPite/Internal/BlockBuffer.cs create mode 100644 src/RESPite/Internal/BlockBufferSerializer.cs create mode 100644 src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs create mode 100644 src/RESPite/Internal/ThreadLocalBlockBufferSerializer.cs create mode 100644 tests/RESP.Core.Tests/BlockBufferTests.cs rename tests/RESP.Core.Tests/{BufferTests.cs => CycleBufferTests.cs} (99%) diff --git a/src/RESPite/Internal/BlockBuffer.cs b/src/RESPite/Internal/BlockBuffer.cs new file mode 100644 index 000000000..d1f3b2d8c --- /dev/null +++ b/src/RESPite/Internal/BlockBuffer.cs @@ -0,0 +1,235 @@ +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace RESPite.Internal; + +internal abstract partial class BlockBufferSerializer +{ + private protected sealed class BlockBuffer : IDisposable + { + private BlockBuffer(BlockBufferSerializer parent, int minCapacity) + { + _arrayPool = parent._arrayPool; + _buffer = _arrayPool.Rent(minCapacity); +#if DEBUG + _parent = parent; + parent.DebugBufferCreated(); +#endif + } + + private int _refCount = 1; + private int _finalizedOffset, _writeOffset; + private readonly ArrayPool _arrayPool; + private byte[] _buffer; +#if DEBUG + private int _finalizedCount; + private BlockBufferSerializer _parent; +#endif + + public override string ToString() => +#if DEBUG + $"{_finalizedCount} messages; " + +#endif + $"{_finalizedOffset} finalized bytes; writing: {NonFinalizedData.Length} bytes, {Available} available; observers: {_refCount}"; + + private int Available => _buffer.Length - _writeOffset; + public Memory UncommittedMemory => _buffer.AsMemory(_writeOffset); + public Span UncommittedSpan => _buffer.AsSpan(_writeOffset); + + // decrease ref-count; dispose if necessary + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + if (Interlocked.Decrement(ref _refCount) <= 0) Recycle(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] // called rarely vs Dispose + private void Recycle() + { + var count = Volatile.Read(ref _refCount); + if (count == 0) + { + _arrayPool.Return(_buffer); +#if DEBUG + GC.SuppressFinalize(this); + _parent.DebugBufferRecycled(); +#endif + } + + Debug.Assert(count == 0, $"over-disposal? count={count}"); + } + +#if DEBUG + ~BlockBuffer() + { + _parent.DebugBufferLeaked(); + } +#endif + + public static BlockBuffer GetBuffer(BlockBufferSerializer parent, int sizeHint) + { + // note this isn't an actual "max", just a max of what we guarantee; we give the caller + // whatever is left in the buffer; the clamped hint just decides whether we need a *new* buffer + const int MinSize = 16, MaxSize = 128; + sizeHint = Math.Min(Math.Max(sizeHint, MinSize), MaxSize); + + var buffer = parent.Buffer; // most common path is "exists, with enough data" + return buffer is not null && buffer.AvailableWithResetIfUseful() >= sizeHint + ? buffer + : GetBufferSlow(parent, sizeHint); + } + + // would it be useful and possible to reset? i.e. if all finalized chunks have been returned, + private int AvailableWithResetIfUseful() + { + if (_finalizedOffset != 0 // at least some chunks have been finalized + && Volatile.Read(ref _refCount) == 1 // all finalized chunks returned + & _writeOffset == _finalizedOffset) // we're not in the middle of serializing something new + { + _writeOffset = _finalizedOffset = 0; // swipe left + } + + return _buffer.Length - _writeOffset; + } + + private static BlockBuffer GetBufferSlow(BlockBufferSerializer parent, int minBytes) + { + // note clamp on size hint has already been applied + const int DefaultBufferSize = 2048; + var buffer = parent.Buffer; + if (buffer is null) + { + // first buffer + return parent.Buffer = new BlockBuffer(parent, DefaultBufferSize); + } + + Debug.Assert(minBytes > buffer.Available, "existing buffer has capacity - why are we here?"); + + if (buffer.TryResizeFor(minBytes)) + { + Debug.Assert(buffer.Available >= minBytes); + return buffer; + } + + // We've tried reset and resize - no more tricks; we need to move to a new buffer, starting with a + // capacity for any existing data in this message, plus the new chunk we're adding. + var nonFinalizedBytes = buffer.NonFinalizedData; + var newBuffer = new BlockBuffer(parent, Math.Max(nonFinalizedBytes.Length + minBytes, DefaultBufferSize)); + + // copy the existing message data, if any (the previous message might have finished near the + // boundary, in which case we might not have written anything yet) + newBuffer.CopyFrom(nonFinalizedBytes); + Debug.Assert(newBuffer.Available >= minBytes, "should have requested extra capacity"); + + // the ~emperor~ buffer is dead; long live the ~emperor~ buffer + parent.Buffer = newBuffer; + buffer.MarkComplete(); + return newBuffer; + } + + // used for elective reset (rather than "because we ran out of space") + public static void Clear(BlockBufferSerializer parent) + { + if (parent.Buffer is { } buffer) + { + parent.Buffer = null; + buffer.MarkComplete(); + } + } + + private void MarkComplete() + { + // record that the old buffer no longer logically has any non-committed bytes (mostly just for ToString()) + _writeOffset = _finalizedOffset; + Debug.Assert(IsNonCommittedEmpty); + Dispose(); // decrement the observer + } + + private void CopyFrom(Span source) + { + source.CopyTo(UncommittedSpan); + _writeOffset += source.Length; + } + + private Span NonFinalizedData => _buffer.AsSpan( + _finalizedOffset, _writeOffset - _finalizedOffset); + + private bool TryResizeFor(int extraBytes) + { + if (_finalizedOffset == 0 & // we can only do this if there are no other messages in the buffer + Volatile.Read(ref _refCount) == 1) // and no-one else is looking (we already tried reset) + { + // we're already on the boundary - don't scrimp; just do the math from the end of the buffer + byte[] newArray = _arrayPool.Rent(_buffer.Length + extraBytes); + // copy the existing data (we always expect some, since we've clamped extraBytes to be + // much smaller than the default buffer size) + NonFinalizedData.CopyTo(newArray); + _arrayPool.Return(_buffer); + _buffer = newArray; + return true; + } + + return false; + } + + public static void Advance(BlockBufferSerializer parent, int count) + { + if (count == 0) return; + if (count < 0) ThrowOutOfRange(); + var buffer = parent.Buffer; + if (buffer is null || buffer.Available <= count) ThrowOutOfRange(); + buffer._writeOffset += count; + + [DoesNotReturn] + static void ThrowOutOfRange() => throw new ArgumentOutOfRangeException(nameof(count)); + } + + public void RevertUnfinalized(BlockBufferSerializer parent) + { + // undo any writes (something went wrong during serialize) + _finalizedOffset = _writeOffset; + } + + private ReadOnlyMemory Finalize(out IDisposable? block) + { + var length = _writeOffset - _finalizedOffset; + Debug.Assert(length > 0, "already checked this in FinalizeMessage!"); + ReadOnlyMemory chunk = new(_buffer, _finalizedOffset, length); + _finalizedOffset = _writeOffset; // move the write head +#if DEBUG + _finalizedCount++; + _parent.DebugMessageFinalized(length); +#endif + Interlocked.Increment(ref _refCount); // add an observer + block = this; + return chunk; + } + + private bool IsNonCommittedEmpty => _finalizedOffset == _writeOffset; + + public static ReadOnlyMemory FinalizeMessage(BlockBufferSerializer parent, out IDisposable? block) + { + var buffer = parent.Buffer; + if (buffer is null || buffer.IsNonCommittedEmpty) + { +#if DEBUG // still count it for logging purposes + if (buffer is not null) buffer._finalizedCount++; + parent.DebugMessageFinalized(0); +#endif + return DefaultFinalize(out block); + } + + return buffer.Finalize(out block); + } + + // very rare: means either no buffer *ever*, or we're finalizing an empty message (which isn't valid RESP!) + [MethodImpl(MethodImplOptions.NoInlining)] + private static ReadOnlyMemory DefaultFinalize(out IDisposable? block) + { + block = null; + return default; + } + } +} diff --git a/src/RESPite/Internal/BlockBufferSerializer.cs b/src/RESPite/Internal/BlockBufferSerializer.cs new file mode 100644 index 000000000..57e0e5e40 --- /dev/null +++ b/src/RESPite/Internal/BlockBufferSerializer.cs @@ -0,0 +1,74 @@ +using System.Buffers; +using System.Diagnostics; +using RESPite.Messages; + +namespace RESPite.Internal; + +/// +/// Provides abstracted access to a buffer-writing API. Conveniently, we only give the caller +/// RespWriter - which they cannot export (ref-type), thus we never actually give the +/// public caller our IBufferWriter{byte}. Likewise, note that serialization is synchronous, +/// i.e. never switches thread during an operation. This gives us quite a bit of flexibility. +/// There are two main uses of BlockBufferSerializer: +/// 1. thread-local: ambient, used for random messages so that each thread is quietly packing +/// a thread-specific buffer; zero concurrency because of [ThreadStatic] hackery. +/// 2. batching: RespBatch hosts a serializer that reflects the batch we're building; successive +/// commands in the same batch are written adjacently in a shared buffer - we explicitly +/// detect and reject concurrency attempts in a batch (which is fair: a batch has order). +/// +internal abstract partial class BlockBufferSerializer(ArrayPool? arrayPool = null) : IBufferWriter +{ + private readonly ArrayPool _arrayPool = arrayPool ?? ArrayPool.Shared; + private protected abstract BlockBuffer? Buffer { get; set; } + + Memory IBufferWriter.GetMemory(int sizeHint) => BlockBuffer.GetBuffer(this, sizeHint).UncommittedMemory; + + Span IBufferWriter.GetSpan(int sizeHint) => BlockBuffer.GetBuffer(this, sizeHint).UncommittedSpan; + + void IBufferWriter.Advance(int count) => BlockBuffer.Advance(this, count); + + public void Clear() => BlockBuffer.Clear(this); + + public virtual ReadOnlyMemory Serialize( + ReadOnlySpan command, + in TRequest request, + IRespFormatter formatter, + out IDisposable? block) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + try + { + var writer = new RespWriter(this); + formatter.Format(command, ref writer, request); + writer.Flush(); + return BlockBuffer.FinalizeMessage(this, out block); + } + catch + { + Buffer?.RevertUnfinalized(this); + throw; + } + } + +#if DEBUG + private int _countAdded, _countRecycled, _countLeaked, _countMessages; + private long _countMessageBytes; + public int CountLeaked => Volatile.Read(ref _countLeaked); + public int CountRecycled => Volatile.Read(ref _countRecycled); + public int CountAdded => Volatile.Read(ref _countAdded); + public int CountMessages => Volatile.Read(ref _countMessages); + public long CountMessageBytes => Volatile.Read(ref _countMessageBytes); + private void DebugBufferLeaked() => Interlocked.Increment(ref _countLeaked); + + private void DebugBufferRecycled() => Interlocked.Increment(ref _countRecycled); + + private void DebugBufferCreated() => Interlocked.Increment(ref _countAdded); + private void DebugMessageFinalized(int bytes) + { + Interlocked.Increment(ref _countMessages); + Interlocked.Add(ref _countMessageBytes, bytes); + } +#endif +} diff --git a/src/RESPite/Internal/IRespMessage.cs b/src/RESPite/Internal/IRespMessage.cs index 43bc4ff2b..da5342358 100644 --- a/src/RESPite/Internal/IRespMessage.cs +++ b/src/RESPite/Internal/IRespMessage.cs @@ -16,5 +16,11 @@ internal interface IRespMessage : IValueTaskSource bool AllowInlineParsing { get; } short Token { get; } ref readonly CancellationToken CancellationToken { get; } - void OnSent(short token); + bool IsSent(short token); + + void OnCompletedWithNotSentDetection( + Action continuation, + object? state, + short token, + ValueTaskSourceOnCompletedFlags flags); } diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs index 8119ef63a..f403e7829 100644 --- a/src/RESPite/Internal/RespMessageBase.cs +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -36,10 +36,16 @@ private const int protected void InitParser(object? parser) { - if (parser is not null) + if (parser is null) + { + SetFlag(Flag_InlineParser); // F+F + } + else { int flags = Flag_Parser; + // detect parsers that want to manually parse attributes, errors, etc. if (parser is IRespMetadataParser) flags |= Flag_MetadataParser; + // detect fast, internal, non-allocating parsers (int, bool, etc.) if (parser is IRespInlineParser) flags |= Flag_InlineParser; SetFlag(flags); } @@ -47,14 +53,8 @@ protected void InitParser(object? parser) public bool AllowInlineParsing => HasFlag(Flag_InlineParser); - [Conditional("DEBUG")] - private void DebugAssertPending() => Debug.Assert( - _asyncCore.GetStatus(_asyncCore.Version) == ValueTaskSourceStatus.Pending & !HasFlag(Flag_OutcomeKnown), - "Message should be in a pending state"); - public bool TrySetResult(short token, scoped ReadOnlySpan response) { - DebugAssertPending(); if (HasFlag(Flag_OutcomeKnown) | _asyncCore.Version != token) return false; var flags = _flags & (Flag_MetadataParser | Flag_Parser); switch (flags) @@ -69,22 +69,27 @@ public bool TrySetResult(short token, scoped ReadOnlySpan response) reader.MoveNext(); } - return TrySetResult(Parse(ref reader)); + return TrySetResultPrecheckedToken(Parse(ref reader)); } catch (Exception ex) { - return TrySetException(ex); + return TrySetExceptionPrecheckedToken(ex); } default: - return TrySetResult(default(TResponse)!); + return TrySetResultPrecheckedToken(default(TResponse)!); } } public short Token => _asyncCore.Version; + public bool IsSent(short token) + { + CheckToken(token); + return HasFlag(Flag_Sent); + } + public bool TrySetResult(short token, in ReadOnlySequence response) { - DebugAssertPending(); if (HasFlag(Flag_OutcomeKnown) | _asyncCore.Version != token) return false; var flags = _flags & (Flag_MetadataParser | Flag_Parser); switch (flags) @@ -99,14 +104,14 @@ public bool TrySetResult(short token, in ReadOnlySequence response) reader.MoveNext(); } - return TrySetResult(Parse(ref reader)); + return TrySetResultPrecheckedToken(Parse(ref reader)); } catch (Exception ex) { - return TrySetException(ex); + return TrySetExceptionPrecheckedToken(ex); } default: - return TrySetResult(default(TResponse)!); + return TrySetResultPrecheckedToken(default(TResponse)!); } } @@ -132,18 +137,26 @@ private bool SetFlag(int flag) // in the "any" sense private bool HasFlag(int flag) => (Volatile.Read(ref _flags) & flag) != 0; - public void OnSent(short token) + public RespMessageBase Init(bool sent, CancellationToken cancellationToken) { - if (_asyncCore.GetStatus(token) != ValueTaskSourceStatus.Pending || _requestRefCount != 0 || !SetFlag(Flag_Sent)) + Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); + if (sent) SetFlag(Flag_Sent); + if (cancellationToken.CanBeCanceled) { - throw new InvalidOperationException( - "Operation must be in a pending, unsent state with no request payload. "); + _cancellationToken = cancellationToken; + _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellationToken); } + + return this; } - public RespMessageBase Init(byte[]? oversized, int offset, int length, ArrayPool? pool, CancellationToken cancellation) + public RespMessageBase Init( + byte[]? oversized, + int offset, + int length, + ArrayPool? pool, + CancellationToken cancellationToken) { - DebugAssertPending(); Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); if (oversized is not null) @@ -153,24 +166,28 @@ public RespMessageBase Init(byte[]? oversized, int offset, int length _requestRefCount = 1; } - if (cancellation.CanBeCanceled) + if (cancellationToken.CanBeCanceled) { - _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellation); + _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellationToken); } + return this; } - public RespMessageBase SetRequest(ReadOnlyMemory request, IDisposable? owner, CancellationToken cancellation) + public RespMessageBase SetRequest( + ReadOnlyMemory request, + IDisposable? owner, + CancellationToken cancellationToken) { - DebugAssertPending(); Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); _request = request; _requestOwner = owner; _requestRefCount = 1; - if (cancellation.CanBeCanceled) + if (cancellationToken.CanBeCanceled) { - _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellation); + _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellationToken); } + return this; } @@ -210,6 +227,7 @@ public bool TryReserveRequest(short token, out ReadOnlyMemory payload, boo payload = default; return false; } + if (Interlocked.CompareExchange(ref _requestRefCount, checked(oldCount + 1), oldCount) == oldCount) { if (recordSent) SetFlag(Flag_Sent); @@ -264,22 +282,23 @@ private bool /* asking about the status too early is usually a very bad sign that they're doing something like awaiting a message in a transaction that hasn't been sent */ public ValueTaskSourceStatus GetStatus(short token) + => _asyncCore.GetStatus(token); + + [MethodImpl(MethodImplOptions.NoInlining)] + private void ThrowNotSent(short token) { - // we'd rather see a token error, so check that first - // (in reality, we expect the token to be right almost always) - var status = _asyncCore.GetStatus(token); - if ((status == ValueTaskSourceStatus.Pending & !HasFlag(Flag_Sent)) && TrySetExceptionNotSent()) - { - status = ValueTaskSourceStatus.Faulted; - Debug.Assert(_asyncCore.GetStatus(token) == ValueTaskSourceStatus.Faulted, "should be faulted"); - } - return status; + CheckToken(token); // prefer a token explanation + throw new InvalidOperationException( + "This command has not yet been sent; waiting is not possible. If this is a transaction or batch, you must execute that first."); } [MethodImpl(MethodImplOptions.NoInlining)] - private bool TrySetExceptionNotSent() - => TrySetException(new InvalidOperationException( + private void SetNotSentAsync(short token) + { + CheckToken(token); + TrySetExceptionPrecheckedToken(new InvalidOperationException( "This command has not yet been sent; awaiting is not possible. If this is a transaction or batch, you must execute that first.")); + } private void CheckToken(short token) { @@ -289,25 +308,49 @@ private void CheckToken(short token) } } + // this is used from Task/ValueTask; we can't avoid that - in theory + // we *coiuld* sort of make it work for ValueTask, but if anyone + // calls .AsTask() on it, it would fail public void OnCompleted( Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) { + SetFlag(Flag_NoPulse); // async doesn't need to be pulsed _asyncCore.OnCompleted(continuation, state, token, flags); + } + + public void OnCompletedWithNotSentDetection( + Action continuation, + object? state, + short token, + ValueTaskSourceOnCompletedFlags flags) + { + if (!HasFlag(Flag_Sent)) SetNotSentAsync(token); SetFlag(Flag_NoPulse); // async doesn't need to be pulsed + _asyncCore.OnCompleted(continuation, state, token, flags); } // spoof untyped on top of typed void IValueTaskSource.GetResult(short token) => _ = GetResult(token); void IRespMessage.Wait(short token, TimeSpan timeout) => _ = Wait(token, timeout); - private bool TrySetOutcomeKnown() + private bool TrySetOutcomeKnown(short token, bool withSuccess) + => _asyncCore.Version == token && TrySetOutcomeKnownPrecheckedToken(withSuccess); + + private bool TrySetOutcomeKnownPrecheckedToken(bool withSuccess) { - DebugAssertPending(); if (!SetFlag(Flag_OutcomeKnown)) return false; UnregisterCancellation(); + + // configure threading model; failure can be triggered from any thread - *always* + // dispatch to pool; in the success case, we're either on the IO thread + // (if inline-parsing is enabled) - in which case, yes: dispatch - or we've + // already jumped to a pool thread for the parse step. So: the only + // time we want to complete inline is success and not inline-parsing. + _asyncCore.RunContinuationsAsynchronously = !withSuccess | AllowInlineParsing; + return true; } @@ -320,10 +363,7 @@ public TResponse Wait(short token, TimeSpan timeout) case Flag_Complete | Flag_Sent: // already complete return GetResult(token); default: - if (TrySetExceptionNotSent()) - { - return GetResult(token); - } + ThrowNotSent(token); // always throws break; } @@ -356,7 +396,7 @@ public TResponse Wait(short token, TimeSpan timeout) } UnregisterCancellation(); - if (isTimeout) TrySetTimeout(); + if (isTimeout) TrySetTimeoutPrecheckedToken(); return GetResult(token); @@ -364,18 +404,18 @@ public TResponse Wait(short token, TimeSpan timeout) "This operation cannot be waited because it entered async/await mode - most likely by calling AsTask()"); } - private bool TrySetResult(TResponse response) + private bool TrySetResultPrecheckedToken(TResponse response) { - if (!TrySetOutcomeKnown()) return false; + if (!TrySetOutcomeKnownPrecheckedToken(true)) return false; _asyncCore.SetResult(response); SetFullyComplete(success: true); return true; } - private bool TrySetTimeout() + private bool TrySetTimeoutPrecheckedToken() { - if (!TrySetOutcomeKnown()) return false; + if (!TrySetOutcomeKnownPrecheckedToken(false)) return false; _asyncCore.SetException(new TimeoutException()); SetFullyComplete(success: false); @@ -389,26 +429,28 @@ public bool TrySetCanceled(short token, CancellationToken cancellationToken = de // use our own token if nothing more specific supplied cancellationToken = _cancellationToken; } - return TrySetCanceled(cancellationToken); + + return token == _asyncCore.Version && TrySetCanceledPrecheckedToken(cancellationToken); } - // this is the path used by cancellation registration callbacks; always use our own token - void IRespMessage.TrySetCanceled() => TrySetCanceled(_cancellationToken); + // this is the path used by cancellation registration callbacks; always use our own + // cancellation token, and we must trust the version token + void IRespMessage.TrySetCanceled() => TrySetCanceledPrecheckedToken(_cancellationToken); - private bool TrySetCanceled(CancellationToken cancellationToken) + private bool TrySetCanceledPrecheckedToken(CancellationToken cancellationToken) { - if (!TrySetOutcomeKnown()) return false; + if (!TrySetOutcomeKnownPrecheckedToken(false)) return false; _asyncCore.SetException(new OperationCanceledException(cancellationToken)); SetFullyComplete(success: false); return true; } public bool TrySetException(short token, Exception exception) - => token == _asyncCore.Version && TrySetException(exception); + => token == _asyncCore.Version && TrySetExceptionPrecheckedToken(exception); - private bool TrySetException(Exception exception) + private bool TrySetExceptionPrecheckedToken(Exception exception) { - if (!TrySetOutcomeKnown()) return false; // first winner only + if (!TrySetOutcomeKnownPrecheckedToken(false)) return false; // first winner only _asyncCore.SetException(exception); SetFullyComplete(success: false); return true; diff --git a/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs b/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs new file mode 100644 index 000000000..b11d5618e --- /dev/null +++ b/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs @@ -0,0 +1,46 @@ +using RESPite.Messages; + +namespace RESPite.Internal; + +internal partial class BlockBufferSerializer +{ + internal static BlockBufferSerializer Create() => new SynchronizedBlockBufferSerializer(); + + /// + /// Used for things like . + /// + private sealed class SynchronizedBlockBufferSerializer : BlockBufferSerializer + { + private protected override BlockBuffer? Buffer { get; set; } // simple per-instance auto-prop + + // use lock-based synchronization + public override ReadOnlyMemory Serialize( + ReadOnlySpan command, + in TRequest request, + IRespFormatter formatter, + out IDisposable? block) + { + bool haveLock = false; + try // note that "lock" unrolls to something very similar; we're not adding anything unusual here + { + // in reality, we *expect* people to not attempt to use batches concurrently, *and* + // we expect serialization to be very fast, but: out of an abundance of caution, + // add a timeout - just to avoid surprises (since people can write their own formatters) + Monitor.TryEnter(this, LockTimeout, ref haveLock); + if (!haveLock) ThrowTimeout(); + return base.Serialize(command, in request, formatter, out block); + } + finally + { + if (haveLock) Monitor.Exit(this); + } + + static void ThrowTimeout() => throw new TimeoutException( + "It took a long time to get access to the serialization-buffer. This is very odd - please " + + "ask on GitHub, but *as a guess*, you have a custom RESP formatter that is really slow *and* " + + "you are using concurrent access to a RESP batch / transaction."); + } + + private static readonly TimeSpan LockTimeout = TimeSpan.FromSeconds(5); + } +} diff --git a/src/RESPite/Internal/ThreadLocalBlockBufferSerializer.cs b/src/RESPite/Internal/ThreadLocalBlockBufferSerializer.cs new file mode 100644 index 000000000..1c1895ff4 --- /dev/null +++ b/src/RESPite/Internal/ThreadLocalBlockBufferSerializer.cs @@ -0,0 +1,21 @@ +namespace RESPite.Internal; + +internal partial class BlockBufferSerializer +{ + internal static BlockBufferSerializer Shared => ThreadLocalBlockBufferSerializer.Instance; + private sealed class ThreadLocalBlockBufferSerializer : BlockBufferSerializer + { + private ThreadLocalBlockBufferSerializer() { } + public static readonly ThreadLocalBlockBufferSerializer Instance = new(); + + [ThreadStatic] + // side-step concurrency using per-thread semantics + private static BlockBuffer? _perTreadBuffer; + + private protected override BlockBuffer? Buffer + { + get => _perTreadBuffer; + set => _perTreadBuffer = value; + } + } +} diff --git a/src/RESPite/RESPite.csproj b/src/RESPite/RESPite.csproj index cfb5f5b87..e11cf0941 100644 --- a/src/RESPite/RESPite.csproj +++ b/src/RESPite/RESPite.csproj @@ -46,6 +46,15 @@ RespReader.cs + + BlockBufferSerializer.cs + + + BlockBufferSerializer.cs + + + BlockBufferSerializer.cs + diff --git a/src/RESPite/RespOperation.cs b/src/RESPite/RespOperation.cs index aaf3f3a0d..ca5b9ce1c 100644 --- a/src/RESPite/RespOperation.cs +++ b/src/RESPite/RespOperation.cs @@ -21,6 +21,7 @@ namespace RESPite; [ThreadStatic] // how many resp-operations have we chewed through? private static int _debugPerThreadMessageAllocations; + internal static int DebugPerThreadMessageAllocations => _debugPerThreadMessageAllocations; #else internal static int DebugPerThreadMessageAllocations => 0; @@ -46,6 +47,7 @@ internal RespOperation(IRespMessage message, bool disableCaptureContext = false) _disableCaptureContext = disableCaptureContext; } + public bool IsSent => Message.IsSent(_token); internal IRespMessage Message => _message ?? ThrowNoMessage(); internal static IRespMessage ThrowNoMessage() @@ -92,7 +94,7 @@ public void OnCompleted(Action continuation) ? ValueTaskSourceOnCompletedFlags.FlowExecutionContext : ValueTaskSourceOnCompletedFlags.FlowExecutionContext | ValueTaskSourceOnCompletedFlags.UseSchedulingContext; - Message.OnCompleted(InvokeState, continuation, _token, flags); + Message.OnCompletedWithNotSentDetection(InvokeState, continuation, _token, flags); } /// @@ -102,7 +104,7 @@ public void UnsafeOnCompleted(Action continuation) var flags = _disableCaptureContext ? ValueTaskSourceOnCompletedFlags.None : ValueTaskSourceOnCompletedFlags.UseSchedulingContext; - Message.OnCompleted(InvokeState, continuation, _token, flags); + Message.OnCompletedWithNotSentDetection(InvokeState, continuation, _token, flags); } /// @@ -135,10 +137,7 @@ internal Remote(IRespMessage message) _token = message.Token; } - /// - /// Record the operation as sent. - /// - public void OnSent() => _message.OnSent(_token); + public bool IsTokenMatch => _token == _message.Token; /// public bool TrySetCanceled(CancellationToken cancellationToken = default) @@ -163,10 +162,12 @@ public bool TrySetResult(in ReadOnlySequence response) /// Create a disconnected without a RESP parser; this is only intended for testing purposes. /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - public static RespOperation Create(out Remote remote, CancellationToken token = default) + public static RespOperation Create( + out Remote remote, + bool sent = true, + CancellationToken cancellationToken = default) { - var msg = RespMessage.Get(null) - .Init(null, 0, 0, null, token); + var msg = RespMessage.Get(null).Init(sent, cancellationToken); remote = new(msg); return new RespOperation(msg); } @@ -176,9 +177,13 @@ public static RespOperation Create(out Remote remote, CancellationToken token = /// /// The result of the operation. [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - public static RespOperation Create(IRespParser parser, out Remote remote) + public static RespOperation Create( + IRespParser? parser, + out Remote remote, + bool sent = true, + CancellationToken cancellationToken = default) { - var msg = RespMessage.Get(parser); + var msg = RespMessage.Get(parser).Init(sent, cancellationToken); remote = new(msg); return new RespOperation(msg); } @@ -191,10 +196,12 @@ public static RespOperation Create(IRespParser parser [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public static RespOperation Create( in TState state, - IRespParser parser, - out Remote remote) + IRespParser? parser, + out Remote remote, + bool sent = true, + CancellationToken cancellationToken = default) { - var msg = RespMessage.Get(in state, parser); + var msg = RespMessage.Get(in state, parser).Init(sent, cancellationToken); remote = new(msg); return new RespOperation(msg); } diff --git a/src/RESPite/RespOperationT.cs b/src/RESPite/RespOperationT.cs index b23f0900b..aa4a0c55b 100644 --- a/src/RESPite/RespOperationT.cs +++ b/src/RESPite/RespOperationT.cs @@ -82,9 +82,11 @@ public void OnCompleted(Action continuation) ? ValueTaskSourceOnCompletedFlags.FlowExecutionContext : ValueTaskSourceOnCompletedFlags.FlowExecutionContext | ValueTaskSourceOnCompletedFlags.UseSchedulingContext; - TypedMessage.OnCompleted(RespOperation.InvokeState, continuation, _token, flags); + TypedMessage.OnCompletedWithNotSentDetection(RespOperation.InvokeState, continuation, _token, flags); } + public bool IsSent => TypedMessage.IsSent(_token); + /// public void UnsafeOnCompleted(Action continuation) { @@ -92,7 +94,7 @@ public void UnsafeOnCompleted(Action continuation) var flags = _disableCaptureContext ? ValueTaskSourceOnCompletedFlags.None : ValueTaskSourceOnCompletedFlags.UseSchedulingContext; - TypedMessage.OnCompleted(RespOperation.InvokeState, continuation, _token, flags); + TypedMessage.OnCompletedWithNotSentDetection(RespOperation.InvokeState, continuation, _token, flags); } /// diff --git a/tests/RESP.Core.Tests/BlockBufferTests.cs b/tests/RESP.Core.Tests/BlockBufferTests.cs new file mode 100644 index 000000000..270507b16 --- /dev/null +++ b/tests/RESP.Core.Tests/BlockBufferTests.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using RESPite; +using RESPite.Internal; +using Xunit; + +namespace RESP.Core.Tests; + +public class BlockBufferTests(ITestOutputHelper log) +{ + private void Log(ReadOnlySpan span) + { +#if NET + log.WriteLine(Encoding.UTF8.GetString(span)); +#else + unsafe + { + fixed (byte* p = span) + { + log.WriteLine(Encoding.UTF8.GetString(p, span.Length)); + } + } +#endif + } + + [Fact] + public void CanCreateAndWriteSimpleBuffer() + { + var buffer = BlockBufferSerializer.Create(); + var a = buffer.Serialize("get"u8, "abc", RespFormatters.Key.String, out var blockA); + var b = buffer.Serialize("get"u8, "def", RespFormatters.Key.String, out var blockB); + var c = buffer.Serialize("get"u8, "ghi", RespFormatters.Key.String, out var blockC); + buffer.Clear(); + Assert.Equal(1, buffer.CountAdded); + Assert.Equal(3, buffer.CountMessages); + Assert.Equal(66, buffer.CountMessageBytes); // contents shown/verified below + Assert.Equal(0, buffer.CountRecycled); + Assert.Equal(0, buffer.CountLeaked); + + // check the payloads + Log(a.Span); + Assert.True(a.Span.SequenceEqual("*2\r\n$3\r\nget\r\n$3\r\nabc\r\n"u8)); + Log(a.Span); + Assert.True(b.Span.SequenceEqual("*2\r\n$3\r\nget\r\n$3\r\ndef\r\n"u8)); + Log(c.Span); + Assert.True(c.Span.SequenceEqual("*2\r\n$3\r\nget\r\n$3\r\nghi\r\n"u8)); + blockA?.Dispose(); + blockB?.Dispose(); + Assert.Equal(0, buffer.CountRecycled); + Assert.Equal(0, buffer.CountLeaked); + blockC?.Dispose(); + Assert.Equal(1, buffer.CountRecycled); + Assert.Equal(0, buffer.CountLeaked); + } + + [Fact] + public void CanWriteLotsOfBuffers_WithCheapReset() // when messages are consumed before more are added + { + var buffer = BlockBufferSerializer.Create(); + Assert.Equal(0, buffer.CountAdded); + Assert.Equal(0, buffer.CountRecycled); + Assert.Equal(0, buffer.CountLeaked); + Assert.Equal(0, buffer.CountMessages); + for (int i = 0; i < 5000; i++) + { + var a = buffer.Serialize("get"u8, "abc", RespFormatters.Key.String, out var blockA); + var b = buffer.Serialize("get"u8, "def", RespFormatters.Key.String, out var blockB); + var c = buffer.Serialize("get"u8, "ghi", RespFormatters.Key.String, out var blockC); + blockA?.Dispose(); + blockB?.Dispose(); + blockC?.Dispose(); + Assert.True(MemoryMarshal.TryGetArray(a, out var aSegment)); + Assert.True(MemoryMarshal.TryGetArray(b, out var bSegment)); + Assert.True(MemoryMarshal.TryGetArray(c, out var cSegment)); + Assert.Equal(0, aSegment.Offset); + Assert.Equal(22, aSegment.Count); + Assert.Equal(22, bSegment.Offset); + Assert.Equal(22, bSegment.Count); + Assert.Equal(44, cSegment.Offset); + Assert.Equal(22, cSegment.Count); + Assert.Same(aSegment.Array, bSegment.Array); + Assert.Same(aSegment.Array, cSegment.Array); + } + Assert.Equal(1, buffer.CountAdded); + Assert.Equal(0, buffer.CountRecycled); + Assert.Equal(0, buffer.CountLeaked); + Assert.Equal(15_000, buffer.CountMessages); + + buffer.Clear(); + Assert.Equal(1, buffer.CountAdded); + Assert.Equal(1, buffer.CountRecycled); + Assert.Equal(0, buffer.CountLeaked); + Assert.Equal(15_000, buffer.CountMessages); + } + + [Fact] + public void CanWriteLotsOfBuffers() + { + var buffer = BlockBufferSerializer.Create(); + List blocks = new(15_000); + Assert.Equal(0, buffer.CountAdded); + Assert.Equal(0, buffer.CountRecycled); + Assert.Equal(0, buffer.CountLeaked); + Assert.Equal(0, buffer.CountMessages); + for (int i = 0; i < 5000; i++) + { + _ = buffer.Serialize("get"u8, "abc", RespFormatters.Key.String, out var block); + if (block is not null) blocks.Add(block); + _ = buffer.Serialize("get"u8, "def", RespFormatters.Key.String, out block); + if (block is not null) blocks.Add(block); + _ = buffer.Serialize("get"u8, "ghi", RespFormatters.Key.String, out block); + if (block is not null) blocks.Add(block); + } + // Each buffer is 2048 by default, so: 93 per buffer; at least 162 buffers. + // In reality, we apply some round-ups and minimum buffer sizes, which pushes it a little higher, but: not much. + Assert.Equal(15_000, buffer.CountMessages); + Assert.Equal(171, buffer.CountAdded); + Assert.Equal(0, buffer.CountRecycled); + Assert.Equal(0, buffer.CountLeaked); + + buffer.Clear(); + Assert.Equal(15_000, buffer.CountMessages); + Assert.Equal(171, buffer.CountAdded); + Assert.Equal(0, buffer.CountRecycled); + Assert.Equal(0, buffer.CountLeaked); + + foreach (var block in blocks) block.Dispose(); + Assert.Equal(15_000, buffer.CountMessages); + Assert.Equal(171, buffer.CountAdded); + Assert.Equal(171, buffer.CountRecycled); + Assert.Equal(0, buffer.CountLeaked); + } +} diff --git a/tests/RESP.Core.Tests/BufferTests.cs b/tests/RESP.Core.Tests/CycleBufferTests.cs similarity index 99% rename from tests/RESP.Core.Tests/BufferTests.cs rename to tests/RESP.Core.Tests/CycleBufferTests.cs index faa507293..ff0722aec 100644 --- a/tests/RESP.Core.Tests/BufferTests.cs +++ b/tests/RESP.Core.Tests/CycleBufferTests.cs @@ -8,7 +8,7 @@ namespace RESP.Core.Tests; -public class BufferTests +public class CycleBufferTests { [Fact] public void SimpleUsage() diff --git a/tests/RESP.Core.Tests/OperationUnitTests.cs b/tests/RESP.Core.Tests/OperationUnitTests.cs index cf14cadee..5872a080f 100644 --- a/tests/RESP.Core.Tests/OperationUnitTests.cs +++ b/tests/RESP.Core.Tests/OperationUnitTests.cs @@ -18,62 +18,161 @@ public class OperationUnitTests private static CancellationToken CancellationToken => TestContext.Current.CancellationToken; [Theory] - [InlineData(true)] - [InlineData(false)] - public void ManuallyImplementedAsync_NotSent(bool sent) + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(false, true)] + public void ManuallyImplementedAsync_NotSent_Untyped(bool sent, bool @unsafe) { - var op = RespOperation.Create(out var remote, CancellationToken); - if (sent) remote.OnSent(); + var op = RespOperation.Create(out var remote, sent, cancellationToken: CancellationToken); + Assert.Equal(sent, op.IsSent); var awaiter = op.GetAwaiter(); + Assert.False(awaiter.IsCompleted, "not completed first IsCompleted check"); + + if (@unsafe) + { + op.UnsafeOnCompleted(() => { }); + } + else + { + op.OnCompleted(() => { }); + } + if (sent) { - Assert.False(awaiter.IsCompleted); + Assert.False(awaiter.IsCompleted, "incomplete after OnCompleted"); + Assert.True(remote.TrySetResult(default)); + awaiter.GetResult(); } else { - Assert.True(awaiter.IsFaulted); + Assert.True(awaiter.IsFaulted, "faulted after OnCompleted"); + Assert.False(remote.TrySetResult(default)); var ex = Assert.Throws(() => awaiter.GetResult()); Assert.Contains("This command has not yet been sent", ex.Message); } + Assert.Throws(() => awaiter.GetResult()); } - [Fact] + [Theory] + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(false, true)] + public void ManuallyImplementedAsync_NotSent_Typed(bool sent, bool @unsafe) + { + var op = RespOperation.Create(null, out var remote, sent, cancellationToken: CancellationToken); + Assert.Equal(sent, op.IsSent); + var awaiter = op.GetAwaiter(); + Assert.False(awaiter.IsCompleted, "not completed first IsCompleted check"); + + if (@unsafe) + { + op.UnsafeOnCompleted(() => { }); + } + else + { + op.OnCompleted(() => { }); + } + + if (sent) + { + Assert.False(awaiter.IsCompleted, "incomplete after OnCompleted"); + Assert.True(remote.TrySetResult(default)); + awaiter.GetResult(); + } + else + { + Assert.True(awaiter.IsFaulted, "faulted after OnCompleted"); + Assert.False(remote.TrySetResult(default)); + var ex = Assert.Throws(() => awaiter.GetResult()); + Assert.Contains("This command has not yet been sent", ex.Message); + } + Assert.Throws(() => awaiter.GetResult()); + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(false, true)] + public void ManuallyImplementedAsync_NotSent_Stateful(bool sent, bool @unsafe) + { + var op = RespOperation.Create("abc", null, out var remote, sent, CancellationToken); + Assert.Equal(sent, op.IsSent); + var awaiter = op.GetAwaiter(); + Assert.False(awaiter.IsCompleted, "not completed first IsCompleted check"); + + if (@unsafe) + { + op.UnsafeOnCompleted(() => { }); + } + else + { + op.OnCompleted(() => { }); + } + + if (sent) + { + Assert.False(awaiter.IsCompleted, "incomplete after OnCompleted"); + Assert.True(remote.TrySetResult(default)); + awaiter.GetResult(); + } + else + { + Assert.True(awaiter.IsFaulted, "faulted after OnCompleted"); + Assert.False(remote.TrySetResult(default)); + var ex = Assert.Throws(() => awaiter.GetResult()); + Assert.Contains("This command has not yet been sent", ex.Message); + } + Assert.Throws(() => awaiter.GetResult()); + } + + [Fact(Timeout = 1000)] public void UnsentDetectedSync() { - var op = RespOperation.Create(out var remote, CancellationToken); + var op = RespOperation.Create(out var remote, false, CancellationToken); var ex = Assert.Throws(() => op.Wait()); Assert.Contains("This command has not yet been sent", ex.Message); } - [Fact] + [Fact(Timeout = 1000)] public async Task UnsentDetected_Operation_Async() { - var op = RespOperation.Create(out var remote, CancellationToken); + var op = RespOperation.Create(out var remote, false, CancellationToken); + Assert.False(op.IsCompleted); var ex = await Assert.ThrowsAsync(async () => await op); Assert.Contains("This command has not yet been sent", ex.Message); } - [Fact] - public async Task UnsentDetected_ValueTask_Async() + [Fact(Timeout = 1000)] + public async Task UnsentNotDetected_ValueTask_Async() { - var op = RespOperation.Create(out var remote, CancellationToken); - var ex = await Assert.ThrowsAsync(async () => await op.AsValueTask()); - Assert.Contains("This command has not yet been sent", ex.Message); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken); + cts.CancelAfter(100); + var op = RespOperation.Create(out var remote, false, cts.Token); + var ex = await Assert.ThrowsAsync(async () => await op.AsValueTask()); + Assert.True(ex.CancellationToken.IsCancellationRequested, "CT token should be intact"); + Assert.True(cts.Token != CancellationToken, "should not be test CT"); + Assert.True(cts.Token == ex.CancellationToken, "should be local CT"); } - [Fact] - public async Task UnsentDetected_Task_Async() + [Fact(Timeout = 1000)] + public async Task UnsentNotDetected_Task_Async() { - var op = RespOperation.Create(out var remote, CancellationToken); - var ex = await Assert.ThrowsAsync(async () => await op.AsTask()); - Assert.Contains("This command has not yet been sent", ex.Message); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken); + cts.CancelAfter(100); + var op = RespOperation.Create(out var remote, false, cts.Token); + var ex = await Assert.ThrowsAsync(async () => await op.AsTask()); + Assert.True(ex.CancellationToken.IsCancellationRequested, "CT token should be intact"); + Assert.True(cts.Token != CancellationToken, "should not be test CT"); + Assert.True(cts.Token == ex.CancellationToken, "should be local CT"); } - [Fact] + [Fact(Timeout = 1000)] public void CanCreateAndCompleteOperation() { - var op = RespOperation.Create(out var remote, CancellationToken); - remote.OnSent(); + var op = RespOperation.Create(out var remote, cancellationToken: CancellationToken); // initial state Assert.False(op.IsCanceled); @@ -96,53 +195,53 @@ public void CanCreateAndCompleteOperation() Assert.False(remote.TrySetException(null!)); // can get result + Assert.True(remote.IsTokenMatch, "should match before GetResult"); op.GetResult(); + Assert.False(remote.IsTokenMatch, "should have reset token"); // but only once, after that: bad things - Assert.Throws(() => op.GetResult()); - Assert.Throws(() => op.IsCanceled); - Assert.Throws(() => op.IsCompleted); - Assert.Throws(() => op.IsCompletedSuccessfully); - Assert.Throws(() => op.IsFaulted); + Assert.Throws(() => op.GetResult()); + Assert.Throws(() => op.IsCanceled); + Assert.Throws(() => op.IsCompleted); + Assert.Throws(() => op.IsCompletedSuccessfully); + Assert.Throws(() => op.IsFaulted); // additional completions continue to fail - Assert.False(remote.TrySetResult(default)); -#pragma warning disable xUnit1051 - Assert.False(remote.TrySetCanceled()); -#pragma warning restore xUnit1051 - Assert.False(remote.TrySetException(null!)); + Assert.False(remote.TrySetResult(default), "TrySetResult"); + Assert.False(remote.TrySetCanceled(CancellationToken), "TrySetCanceled"); + Assert.False(remote.TrySetException(null!), "TrySetException"); } - [Fact] + [Fact(Timeout = 1000)] public void CanCreateAndCompleteWithoutLeaking() { int before = RespOperation.DebugPerThreadMessageAllocations; for (int i = 0; i < 100; i++) { - var op = RespOperation.Create(out var remote, CancellationToken); - remote.OnSent(); + var op = RespOperation.Create(out var remote, cancellationToken: CancellationToken); remote.TrySetResult(default); Assert.True(op.IsCompleted); op.Wait(); } + int after = RespOperation.DebugPerThreadMessageAllocations; var allocs = after - before; Debug.Assert(allocs < 2, $"allocations: {allocs}"); } - [Fact] + [Fact(Timeout = 1000)] public async Task CanCreateAndCompleteWithoutLeaking_Async() { var threadId = Environment.CurrentManagedThreadId; int before = RespOperation.DebugPerThreadMessageAllocations; for (int i = 0; i < 100; i++) { - var op = RespOperation.Create(out var remote, CancellationToken); - remote.OnSent(); + var op = RespOperation.Create(out var remote, cancellationToken: CancellationToken); remote.TrySetResult(default); Assert.True(op.IsCompleted); await op; } + int after = RespOperation.DebugPerThreadMessageAllocations; var allocs = after - before; Debug.Assert(allocs < 2, $"allocations: {allocs}"); From 76cd60eb277386e68dfd42584aff5c020b985708 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 1 Sep 2025 11:27:16 +0100 Subject: [PATCH 019/108] new test: #if parts for release buld --- tests/RESP.Core.Tests/BlockBufferTests.cs | 36 ++++++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/tests/RESP.Core.Tests/BlockBufferTests.cs b/tests/RESP.Core.Tests/BlockBufferTests.cs index 270507b16..10d2d3d8f 100644 --- a/tests/RESP.Core.Tests/BlockBufferTests.cs +++ b/tests/RESP.Core.Tests/BlockBufferTests.cs @@ -33,12 +33,13 @@ public void CanCreateAndWriteSimpleBuffer() var b = buffer.Serialize("get"u8, "def", RespFormatters.Key.String, out var blockB); var c = buffer.Serialize("get"u8, "ghi", RespFormatters.Key.String, out var blockC); buffer.Clear(); +#if DEBUG Assert.Equal(1, buffer.CountAdded); Assert.Equal(3, buffer.CountMessages); Assert.Equal(66, buffer.CountMessageBytes); // contents shown/verified below Assert.Equal(0, buffer.CountRecycled); Assert.Equal(0, buffer.CountLeaked); - +#endif // check the payloads Log(a.Span); Assert.True(a.Span.SequenceEqual("*2\r\n$3\r\nget\r\n$3\r\nabc\r\n"u8)); @@ -48,21 +49,27 @@ public void CanCreateAndWriteSimpleBuffer() Assert.True(c.Span.SequenceEqual("*2\r\n$3\r\nget\r\n$3\r\nghi\r\n"u8)); blockA?.Dispose(); blockB?.Dispose(); +#if DEBUG Assert.Equal(0, buffer.CountRecycled); Assert.Equal(0, buffer.CountLeaked); +#endif blockC?.Dispose(); +#if DEBUG Assert.Equal(1, buffer.CountRecycled); Assert.Equal(0, buffer.CountLeaked); +#endif } [Fact] public void CanWriteLotsOfBuffers_WithCheapReset() // when messages are consumed before more are added { var buffer = BlockBufferSerializer.Create(); +#if DEBUG Assert.Equal(0, buffer.CountAdded); Assert.Equal(0, buffer.CountRecycled); Assert.Equal(0, buffer.CountLeaked); Assert.Equal(0, buffer.CountMessages); +#endif for (int i = 0; i < 5000; i++) { var a = buffer.Serialize("get"u8, "abc", RespFormatters.Key.String, out var blockA); @@ -83,16 +90,19 @@ public void CanWriteLotsOfBuffers_WithCheapReset() // when messages are consumed Assert.Same(aSegment.Array, bSegment.Array); Assert.Same(aSegment.Array, cSegment.Array); } +#if DEBUG Assert.Equal(1, buffer.CountAdded); Assert.Equal(0, buffer.CountRecycled); Assert.Equal(0, buffer.CountLeaked); Assert.Equal(15_000, buffer.CountMessages); - +#endif buffer.Clear(); +#if DEBUG Assert.Equal(1, buffer.CountAdded); Assert.Equal(1, buffer.CountRecycled); Assert.Equal(0, buffer.CountLeaked); Assert.Equal(15_000, buffer.CountMessages); +#endif } [Fact] @@ -100,10 +110,12 @@ public void CanWriteLotsOfBuffers() { var buffer = BlockBufferSerializer.Create(); List blocks = new(15_000); +#if DEBUG Assert.Equal(0, buffer.CountAdded); Assert.Equal(0, buffer.CountRecycled); Assert.Equal(0, buffer.CountLeaked); Assert.Equal(0, buffer.CountMessages); +#endif for (int i = 0; i < 5000; i++) { _ = buffer.Serialize("get"u8, "abc", RespFormatters.Key.String, out var block); @@ -113,23 +125,31 @@ public void CanWriteLotsOfBuffers() _ = buffer.Serialize("get"u8, "ghi", RespFormatters.Key.String, out block); if (block is not null) blocks.Add(block); } - // Each buffer is 2048 by default, so: 93 per buffer; at least 162 buffers. + + // Each buffer is 2048 by default, so: 93 per buffer; at least 162 buffers (looking at CountAdded). // In reality, we apply some round-ups and minimum buffer sizes, which pushes it a little higher, but: not much. + // However, the runtime can also choose to issue bigger leases than we expect, pushing it down! What matters + // isn't the specific number, but: that it isn't huge. +#if DEBUG Assert.Equal(15_000, buffer.CountMessages); - Assert.Equal(171, buffer.CountAdded); + Assert.True(buffer.CountAdded < 200, "too many buffers used"); Assert.Equal(0, buffer.CountRecycled); Assert.Equal(0, buffer.CountLeaked); - +#endif buffer.Clear(); +#if DEBUG Assert.Equal(15_000, buffer.CountMessages); - Assert.Equal(171, buffer.CountAdded); + Assert.True(buffer.CountAdded < 200, "too many buffers used"); Assert.Equal(0, buffer.CountRecycled); Assert.Equal(0, buffer.CountLeaked); +#endif foreach (var block in blocks) block.Dispose(); +#if DEBUG Assert.Equal(15_000, buffer.CountMessages); - Assert.Equal(171, buffer.CountAdded); - Assert.Equal(171, buffer.CountRecycled); + Assert.True(buffer.CountAdded < 200, "too many buffers used"); + Assert.Equal(buffer.CountAdded, buffer.CountRecycled); Assert.Equal(0, buffer.CountLeaked); +#endif } } From 927b6aa2e23fa6e648b6602e46bd96a137a12f37 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 1 Sep 2025 14:04:33 +0100 Subject: [PATCH 020/108] runnable --- .../RespCommandGenerator.cs | 25 +- src/RESPite.Benchmark/BenchmarkBase.cs | 20 ++ .../Internal/BasicBatchConnection.cs | 12 +- .../Internal/DecoratorConnection.cs | 8 +- .../Connections/Internal/NullConnection.cs | 8 +- .../Internal/SynchronizedConnection.cs | 24 +- src/RESPite/Connections/RespConnectionPool.cs | 16 +- src/RESPite/Internal/BlockBuffer.cs | 6 + src/RESPite/Internal/BlockBufferSerializer.cs | 16 +- src/RESPite/Internal/DebugCounters.cs | 52 +++- src/RESPite/Internal/RespMessageBase.cs | 22 +- src/RESPite/Internal/StreamConnection.cs | 17 +- .../SynchronizedBlockBufferSerializer.cs | 3 +- src/RESPite/RespConnection.cs | 29 ++- src/RESPite/RespContext.cs | 5 +- src/RESPite/RespContextExtensions.cs | 227 ++++++++++++++---- src/RESPite/RespFormatters.cs | 42 +++- src/RESPite/RespOperationBuilder.cs | 30 +-- tests/RESP.Core.Tests/BlockBufferTests.cs | 18 +- 19 files changed, 416 insertions(+), 164 deletions(-) diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index aee2f7751..9639f1c3d 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -428,7 +428,6 @@ void WriteMethod(bool asAsync) bool useDirectCall = method.Context is { Length: > 0 } & formatter is { Length: > 0 } & parser is { Length: > 0 }; - useDirectCall = false; // disable for now if (string.IsNullOrWhiteSpace(method.Context)) { NewLine().Append("=> throw new NotSupportedException(\"No RespContext available\");"); @@ -461,19 +460,29 @@ void WriteMethod(bool asAsync) if (useDirectCall) // avoid the intermediate step when possible { - /* - sb = NewLine().Append("=> ").Append(context).A "global::RESPite.Messages.something.Send") - .Append(asAsync ? "Async" : "") + sb = NewLine().Append("=> ").Append(method.Context).Append(".Send") .Append('<'); WriteTuple( method.Parameters, sb, isSharedFormatter ? TupleMode.SyntheticNames : TupleMode.NamedTuple); - sb.Append(", ").Append(method.ReturnType).Append(">(").Append(method.Context).Append(", ") - .Append(csValue).Append("u8").Append(", "); + if (!string.IsNullOrWhiteSpace(method.ReturnType)) + { + sb.Append(", ").Append(method.ReturnType); + } + sb.Append(">(").Append(csValue).Append("u8").Append(", "); WriteTuple(method.Parameters, sb, TupleMode.Values); - sb.Append(", ").Append(formatter).Append(", ").Append(parser).Append(");"); - */ + sb.Append(", ").Append(formatter).Append(", ").Append(parser).Append(")"); + if (asAsync) + { + sb.Append(".AsValueTask()"); + } + else + { + sb.Append(".Wait(").Append(method.Context).Append(".SyncTimeout)"); + } + + sb.Append(";"); } indent--; diff --git a/src/RESPite.Benchmark/BenchmarkBase.cs b/src/RESPite.Benchmark/BenchmarkBase.cs index 624bc8bd9..33b401449 100644 --- a/src/RESPite.Benchmark/BenchmarkBase.cs +++ b/src/RESPite.Benchmark/BenchmarkBase.cs @@ -506,6 +506,26 @@ protected async Task RunAsync( Console.WriteLine(); } + if (counters.BufferCreatedCount != 0 || + counters.BufferRecycledCount != 0 | counters.BufferMessageCount != 0) + { + Console.Write("Buffers"); + if (counters.BufferCreatedCount != 0) + { + Console.Write( + $"; created: {counters.BufferCreatedCount:#,###,##0}, {FormatBytes(counters.BufferTotalBytes)}"); + // always write recycled count - it being zero is important + Console.Write($"; recycled: {counters.BufferRecycledCount:#,###,##0}"); + } + + if (counters.BufferMessageCount != 0) + { + Console.Write( + $"; {counters.BufferMessageCount:#,###,##0} messages, {FormatBytes(counters.BufferMessageBytes)}"); + } + Console.WriteLine(); + } + static string FormatBytes(long bytes) { const long K = 1024, M = K * K, G = M * K, T = G * K; diff --git a/src/RESPite/Connections/Internal/BasicBatchConnection.cs b/src/RESPite/Connections/Internal/BasicBatchConnection.cs index e6738e8da..4659bdfe9 100644 --- a/src/RESPite/Connections/Internal/BasicBatchConnection.cs +++ b/src/RESPite/Connections/Internal/BasicBatchConnection.cs @@ -65,7 +65,7 @@ internal override int OutstandingOperations } } - public override void Send(in RespOperation message) + public override void Write(in RespOperation message) { lock (_unsent) { @@ -74,7 +74,7 @@ public override void Send(in RespOperation message) } } - internal override void Send(ReadOnlySpan messages) + internal override void Write(ReadOnlySpan messages) { if (messages.Length != 0) { @@ -140,7 +140,7 @@ public override Task FlushAsync() return count switch { 0 => Task.CompletedTask, - 1 => Tail.SendAsync(single!), + 1 => Tail.WriteAsync(single!), _ => SendAndRecycleAsync(Tail, oversized, count), }; @@ -148,7 +148,7 @@ static async Task SendAndRecycleAsync(RespConnection tail, RespOperation[] overs { try { - await tail.SendAsync(oversized.AsMemory(0, count)).ConfigureAwait(false); + await tail.WriteAsync(oversized.AsMemory(0, count)).ConfigureAwait(false); ArrayPool.Shared.Return(oversized); // only on success, in case captured } catch (Exception ex) @@ -171,13 +171,13 @@ public override void Flush() case 0: return; case 1: - Tail.Send(single!); + Tail.Write(single!); return; } try { - Tail.Send(oversized.AsSpan(0, count)); + Tail.Write(oversized.AsSpan(0, count)); } catch (Exception ex) { diff --git a/src/RESPite/Connections/Internal/DecoratorConnection.cs b/src/RESPite/Connections/Internal/DecoratorConnection.cs index d5745e3f0..efd86e636 100644 --- a/src/RESPite/Connections/Internal/DecoratorConnection.cs +++ b/src/RESPite/Connections/Internal/DecoratorConnection.cs @@ -25,11 +25,11 @@ protected override ValueTask OnDisposeAsync() => // Note that default behaviour *does not* add a dispose check, as it // assumes that the connection is "owned", and therefore the tail will throw. - public override void Send(in RespOperation message) => Tail.Send(message); + public override void Write(in RespOperation message) => Tail.Write(message); - internal override void Send(ReadOnlySpan messages) => Tail.Send(messages); + internal override void Write(ReadOnlySpan messages) => Tail.Write(messages); - public override Task SendAsync(in RespOperation message) => Tail.SendAsync(in message); + public override Task WriteAsync(in RespOperation message) => Tail.WriteAsync(in message); - internal override Task SendAsync(ReadOnlyMemory messages) => Tail.SendAsync(messages); + internal override Task WriteAsync(ReadOnlyMemory messages) => Tail.WriteAsync(messages); } diff --git a/src/RESPite/Connections/Internal/NullConnection.cs b/src/RESPite/Connections/Internal/NullConnection.cs index a7f807674..fecfcdc1e 100644 --- a/src/RESPite/Connections/Internal/NullConnection.cs +++ b/src/RESPite/Connections/Internal/NullConnection.cs @@ -9,19 +9,21 @@ public static NullConnection WithConfiguration(RespConfiguration configuration) public static readonly NullConnection Default = new(RespConfiguration.Default); + internal override int OutstandingOperations => 0; + private NullConnection(RespConfiguration configuration) : base(configuration) { } private const string SendErrorMessage = "Null connections do not support sending messages."; - public override void Send(in RespOperation message) + public override void Write(in RespOperation message) { message.Message.TrySetException(message.Token, new NotSupportedException(SendErrorMessage)); } - public override Task SendAsync(in RespOperation message) + public override Task WriteAsync(in RespOperation message) { - Send(message); + Write(message); return Task.CompletedTask; } } diff --git a/src/RESPite/Connections/Internal/SynchronizedConnection.cs b/src/RESPite/Connections/Internal/SynchronizedConnection.cs index efe5aecd1..644aeac1c 100644 --- a/src/RESPite/Connections/Internal/SynchronizedConnection.cs +++ b/src/RESPite/Connections/Internal/SynchronizedConnection.cs @@ -22,12 +22,12 @@ protected override ValueTask OnDisposeAsync() } internal override bool IsHealthy => _semaphore.CurrentCount > 0 & base.IsHealthy; - public override void Send(in RespOperation message) + public override void Write(in RespOperation message) { try { _semaphore.Wait(message.CancellationToken); - Tail.Send(message); + Tail.Write(message); } catch (Exception ex) { @@ -40,20 +40,20 @@ public override void Send(in RespOperation message) } } - internal override void Send(ReadOnlySpan messages) + internal override void Write(ReadOnlySpan messages) { switch (messages.Length) { case 0: return; case 1: - Send(messages[0]); + Write(messages[0]); return; } try { _semaphore.Wait(messages[0].CancellationToken); - Tail.Send(messages); + Tail.Write(messages); } catch (Exception ex) { @@ -66,7 +66,7 @@ internal override void Send(ReadOnlySpan messages) } } - public override Task SendAsync(in RespOperation message) + public override Task WriteAsync(in RespOperation message) { bool haveLock = false; try @@ -78,7 +78,7 @@ public override Task SendAsync(in RespOperation message) return FullAsync(this, message); } - var pending = Tail.SendAsync(message); + var pending = Tail.WriteAsync(message); if (!pending.IsCompleted) { DebugCounters.OnPipelineSendAsync(); @@ -114,7 +114,7 @@ static async Task FullAsync(SynchronizedConnection @this, RespOperation message) try { - await @this.Tail.SendAsync(message).ConfigureAwait(false); + await @this.Tail.WriteAsync(message).ConfigureAwait(false); } finally { @@ -135,12 +135,12 @@ private async Task AwaitAndReleaseLock(Task pending) } } - internal override Task SendAsync(ReadOnlyMemory messages) + internal override Task WriteAsync(ReadOnlyMemory messages) { switch (messages.Length) { case 0: return Task.CompletedTask; - case 1: return SendAsync(messages.Span[0]); + case 1: return WriteAsync(messages.Span[0]); } bool haveLock = false; @@ -153,7 +153,7 @@ internal override Task SendAsync(ReadOnlyMemory messages) return FullAsync(this, messages); } - var pending = Tail.SendAsync(messages); + var pending = Tail.WriteAsync(messages); if (!pending.IsCompleted) { DebugCounters.OnPipelineSendAsync(); @@ -182,7 +182,7 @@ static async Task FullAsync(SynchronizedConnection @this, ReadOnlyMemory messages) + internal override void Write(ReadOnlySpan messages) { ThrowIfDisposed(); - Tail.Send(messages); + Tail.Write(messages); } - public override Task SendAsync(in RespOperation message) + public override Task WriteAsync(in RespOperation message) { ThrowIfDisposed(); - return Tail.SendAsync(message); + return Tail.WriteAsync(message); } - internal override Task SendAsync(ReadOnlyMemory messages) + internal override Task WriteAsync(ReadOnlyMemory messages) { ThrowIfDisposed(); - return Tail.SendAsync(messages); + return Tail.WriteAsync(messages); } } } diff --git a/src/RESPite/Internal/BlockBuffer.cs b/src/RESPite/Internal/BlockBuffer.cs index d1f3b2d8c..0e3b4d746 100644 --- a/src/RESPite/Internal/BlockBuffer.cs +++ b/src/RESPite/Internal/BlockBuffer.cs @@ -13,6 +13,7 @@ private BlockBuffer(BlockBufferSerializer parent, int minCapacity) { _arrayPool = parent._arrayPool; _buffer = _arrayPool.Rent(minCapacity); + DebugCounters.OnBufferCapacity(_buffer.Length); #if DEBUG _parent = parent; parent.DebugBufferCreated(); @@ -145,6 +146,9 @@ private void MarkComplete() _writeOffset = _finalizedOffset; Debug.Assert(IsNonCommittedEmpty); Dispose(); // decrement the observer + #if DEBUG + DebugCounters.OnBufferCompleted(_finalizedCount, _finalizedOffset); + #endif } private void CopyFrom(Span source) @@ -163,6 +167,8 @@ private bool TryResizeFor(int extraBytes) { // we're already on the boundary - don't scrimp; just do the math from the end of the buffer byte[] newArray = _arrayPool.Rent(_buffer.Length + extraBytes); + DebugCounters.OnBufferCapacity(newArray.Length - _buffer.Length); // account for extra only + // copy the existing data (we always expect some, since we've clamped extraBytes to be // much smaller than the default buffer size) NonFinalizedData.CopyTo(newArray); diff --git a/src/RESPite/Internal/BlockBufferSerializer.cs b/src/RESPite/Internal/BlockBufferSerializer.cs index 57e0e5e40..ba757982c 100644 --- a/src/RESPite/Internal/BlockBufferSerializer.cs +++ b/src/RESPite/Internal/BlockBufferSerializer.cs @@ -30,6 +30,7 @@ internal abstract partial class BlockBufferSerializer(ArrayPool? arrayPool public void Clear() => BlockBuffer.Clear(this); public virtual ReadOnlyMemory Serialize( + RespCommandMap? commandMap, ReadOnlySpan command, in TRequest request, IRespFormatter formatter, @@ -41,6 +42,7 @@ public virtual ReadOnlyMemory Serialize( try { var writer = new RespWriter(this); + writer.CommandMap = commandMap; formatter.Format(command, ref writer, request); writer.Flush(); return BlockBuffer.FinalizeMessage(this, out block); @@ -60,11 +62,21 @@ public virtual ReadOnlyMemory Serialize( public int CountAdded => Volatile.Read(ref _countAdded); public int CountMessages => Volatile.Read(ref _countMessages); public long CountMessageBytes => Volatile.Read(ref _countMessageBytes); + private void DebugBufferLeaked() => Interlocked.Increment(ref _countLeaked); - private void DebugBufferRecycled() => Interlocked.Increment(ref _countRecycled); + private void DebugBufferRecycled() + { + Interlocked.Increment(ref _countRecycled); + DebugCounters.OnBufferRecycled(); + } + + private void DebugBufferCreated() + { + Interlocked.Increment(ref _countAdded); + DebugCounters.OnBufferCreated(); + } - private void DebugBufferCreated() => Interlocked.Increment(ref _countAdded); private void DebugMessageFinalized(int bytes) { Interlocked.Increment(ref _countMessages); diff --git a/src/RESPite/Internal/DebugCounters.cs b/src/RESPite/Internal/DebugCounters.cs index 3f527978e..82829a996 100644 --- a/src/RESPite/Internal/DebugCounters.cs +++ b/src/RESPite/Internal/DebugCounters.cs @@ -23,10 +23,19 @@ internal partial class DebugCounters _tallyBatchWriteCount, _tallyBatchWriteFullPageCount, _tallyBatchWritePartialPageCount, - _tallyBatchWriteMessageCount; + _tallyBatchWriteMessageCount, + _tallyBufferCreatedCount, + _tallyBufferRecycledCount, + _tallyBufferMessageCount; - private static long _tallyWriteBytes, _tallyReadBytes, _tallyCopyOutBytes, _tallyDiscardAverage; + private static long _tallyWriteBytes, + _tallyReadBytes, + _tallyCopyOutBytes, + _tallyDiscardAverage, + _tallyBufferMessageBytes, + _tallyBufferTotalBytes; #endif + [Conditional("DEBUG")] internal static void OnRead(int bytes) { @@ -50,6 +59,7 @@ public static void OnBatchWriteFullPage() Interlocked.Increment(ref _tallyBatchWriteFullPageCount); #endif } + public static void OnBatchWritePartialPage() { #if DEBUG @@ -141,6 +151,38 @@ public static void OnPipelineFullSync() #endif } + [Conditional("DEBUG")] + public static void OnBufferCreated() + { +#if DEBUG + Interlocked.Increment(ref _tallyBufferCreatedCount); +#endif + } + + [Conditional("DEBUG")] + public static void OnBufferRecycled() + { +#if DEBUG + Interlocked.Increment(ref _tallyBufferRecycledCount); +#endif + } + + [Conditional("DEBUG")] + public static void OnBufferCompleted(int messageCount, int messageBytes) + { +#if DEBUG + Interlocked.Add(ref _tallyBufferMessageCount, messageCount); + Interlocked.Add(ref _tallyBufferMessageBytes, messageBytes); +#endif + } + + public static void OnBufferCapacity(int bytes) + { +#if DEBUG + Interlocked.Add(ref _tallyBufferTotalBytes, bytes); +#endif + } + private DebugCounters() { } @@ -178,5 +220,11 @@ private static void EstimatedMovingRangeAverage(ref long field, long value) public int BatchWriteFullPageCount { get; } = Interlocked.Exchange(ref _tallyBatchWriteFullPageCount, 0); public int BatchWritePartialPageCount { get; } = Interlocked.Exchange(ref _tallyBatchWritePartialPageCount, 0); public int BatchWriteMessageCount { get; } = Interlocked.Exchange(ref _tallyBatchWriteMessageCount, 0); + + public int BufferCreatedCount { get; } = Interlocked.Exchange(ref _tallyBufferCreatedCount, 0); + public int BufferRecycledCount { get; } = Interlocked.Exchange(ref _tallyBufferRecycledCount, 0); + public int BufferMessageCount { get; } = Interlocked.Exchange(ref _tallyBufferMessageCount, 0); + public long BufferMessageBytes { get; } = Interlocked.Exchange(ref _tallyBufferMessageBytes, 0); + public long BufferTotalBytes { get; } = Interlocked.Exchange(ref _tallyBufferTotalBytes, 0); #endif } diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs index f403e7829..5816f7fd4 100644 --- a/src/RESPite/Internal/RespMessageBase.cs +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -139,6 +139,7 @@ private bool SetFlag(int flag) public RespMessageBase Init(bool sent, CancellationToken cancellationToken) { + Debug.Assert(_flags == 0, "flags should be zero"); Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); if (sent) SetFlag(Flag_Sent); if (cancellationToken.CanBeCanceled) @@ -157,8 +158,8 @@ public RespMessageBase Init( ArrayPool? pool, CancellationToken cancellationToken) { + Debug.Assert(_flags == 0, "flags should be zero"); Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); - if (oversized is not null) { _requestOwner = pool; @@ -174,7 +175,7 @@ public RespMessageBase Init( return this; } - public RespMessageBase SetRequest( + public RespMessageBase Init( ReadOnlyMemory request, IDisposable? owner, CancellationToken cancellationToken) @@ -257,19 +258,17 @@ private bool { if (oldCount == 1) // we were the last one; recycle { - if (_requestOwner is ArrayPool pool) + if (_requestOwner is IDisposable owner) + { + owner.Dispose(); + } + else if (_requestOwner is ArrayPool pool) { if (MemoryMarshal.TryGetArray(_request, out var segment)) { pool.Return(segment.Array!); } } - - if (_requestOwner is IDisposable owner) - { - owner.Dispose(); - } - _request = default; _requestOwner = null; } @@ -317,6 +316,7 @@ public void OnCompleted( short token, ValueTaskSourceOnCompletedFlags flags) { + CheckToken(token); SetFlag(Flag_NoPulse); // async doesn't need to be pulsed _asyncCore.OnCompleted(continuation, state, token, flags); } @@ -327,6 +327,7 @@ public void OnCompletedWithNotSentDetection( short token, ValueTaskSourceOnCompletedFlags flags) { + CheckToken(token); if (!HasFlag(Flag_Sent)) SetNotSentAsync(token); SetFlag(Flag_NoPulse); // async doesn't need to be pulsed _asyncCore.OnCompleted(continuation, state, token, flags); @@ -343,6 +344,7 @@ private bool TrySetOutcomeKnownPrecheckedToken(bool withSuccess) { if (!SetFlag(Flag_OutcomeKnown)) return false; UnregisterCancellation(); + TryReleaseRequest(); // we won't be needing this again // configure threading model; failure can be triggered from any thread - *always* // dispatch to pool; in the success case, we're either on the IO thread @@ -371,7 +373,7 @@ public TResponse Wait(short token, TimeSpan timeout) CheckToken(token); lock (this) { - switch (Volatile.Read(ref _flags) & Flag_Complete | Flag_NoPulse) + switch (Volatile.Read(ref _flags) & (Flag_Complete | Flag_NoPulse)) { case Flag_NoPulse | Flag_Complete: case Flag_Complete: diff --git a/src/RESPite/Internal/StreamConnection.cs b/src/RESPite/Internal/StreamConnection.cs index 0d629d617..48552ed52 100644 --- a/src/RESPite/Internal/StreamConnection.cs +++ b/src/RESPite/Internal/StreamConnection.cs @@ -19,9 +19,8 @@ internal sealed class StreamConnection : RespConnection private RespScanState _readScanState; private CycleBuffer _readBuffer, _writeBuffer; - public bool CanWrite => Volatile.Read(ref _readStatus) == WRITER_AVAILABLE; - - public int Outstanding => _outstanding.Count; + internal override int OutstandingOperations => _outstanding.Count; + internal override bool IsHealthy => !_isDoomed; public Task Reader { get; private set; } = Task.CompletedTask; @@ -447,7 +446,7 @@ private void OnRequestUnavailable(in RespOperation message) } } - public override void Send(in RespOperation message) + public override void Write(in RespOperation message) { bool releaseRequest = message.Message.TryReserveRequest(message.Token, out var bytes); if (!releaseRequest) @@ -481,14 +480,14 @@ public override void Send(in RespOperation message) } } - internal override void Send(ReadOnlySpan messages) + internal override void Write(ReadOnlySpan messages) { switch (messages.Length) { case 0: return; case 1: - Send(messages[0]); + Write(messages[0]); return; } @@ -537,7 +536,7 @@ internal override void Send(ReadOnlySpan messages) } } - public override Task SendAsync(in RespOperation message) + public override Task WriteAsync(in RespOperation message) { bool releaseRequest = message.Message.TryReserveRequest(message.Token, out var bytes); if (!releaseRequest) @@ -604,14 +603,14 @@ static async Task AwaitedSingleWithToken( } } - internal override Task SendAsync(ReadOnlyMemory messages) + internal override Task WriteAsync(ReadOnlyMemory messages) { switch (messages.Length) { case 0: return Task.CompletedTask; case 1: - return SendAsync(messages.Span[0]); + return WriteAsync(messages.Span[0]); default: return CombineAndSendMultipleAsync(this, messages); } diff --git a/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs b/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs index b11d5618e..caeeefc26 100644 --- a/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs +++ b/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs @@ -15,6 +15,7 @@ private sealed class SynchronizedBlockBufferSerializer : BlockBufferSerializer // use lock-based synchronization public override ReadOnlyMemory Serialize( + RespCommandMap? commandMap, ReadOnlySpan command, in TRequest request, IRespFormatter formatter, @@ -28,7 +29,7 @@ public override ReadOnlyMemory Serialize( // add a timeout - just to avoid surprises (since people can write their own formatters) Monitor.TryEnter(this, LockTimeout, ref haveLock); if (!haveLock) ThrowTimeout(); - return base.Serialize(command, in request, formatter, out block); + return base.Serialize(commandMap, command, in request, formatter, out block); } finally { diff --git a/src/RESPite/RespConnection.cs b/src/RESPite/RespConnection.cs index 0f89e832e..b29537d15 100644 --- a/src/RESPite/RespConnection.cs +++ b/src/RESPite/RespConnection.cs @@ -18,7 +18,11 @@ public abstract class RespConnection : IDisposable, IAsyncDisposable internal virtual bool IsHealthy => !_isDisposed; - internal virtual int OutstandingOperations { get; } + internal virtual BlockBufferSerializer Serializer => BlockBufferSerializer.Shared; + + internal abstract int OutstandingOperations { get; } + internal readonly RespCommandMap? NonDefaultCommandMap; // prevent checking this each write + public TimeSpan SyncTimeout { get; } private static EndPoint? _defaultEndPoint; // do not expose externally; vexingly mutable private static EndPoint DefaultEndPoint => _defaultEndPoint ??= new IPEndPoint(IPAddress.Loopback, 6379); @@ -45,6 +49,11 @@ private protected RespConnection(in RespContext tail, RespConfiguration? configu Configuration = configuration ?? conn.Configuration; _context = tail.WithConnection(this); + // hoist and pre-check the command map once per connection + var commandMap = Configuration.CommandMap; + NonDefaultCommandMap = ReferenceEquals(commandMap, RespCommandMap.Default) ? null : commandMap; + SyncTimeout = Configuration.SyncTimeout; // snapshot to reduce indirection + static void ThrowUnhealthy() => throw new ArgumentException("A healthy tail connection is required.", nameof(tail)); } @@ -91,16 +100,16 @@ protected virtual ValueTask OnDisposeAsync() return default; } - public abstract void Send(in RespOperation message); + public abstract void Write(in RespOperation message); - internal virtual void Send(ReadOnlySpan messages) + internal virtual void Write(ReadOnlySpan messages) { int i = 0; try { for (i = 0; i < messages.Length; i++) { - Send(messages[i]); + Write(messages[i]); } } catch (Exception ex) @@ -110,18 +119,18 @@ internal virtual void Send(ReadOnlySpan messages) } } - public virtual Task SendAsync(in RespOperation message) + public virtual Task WriteAsync(in RespOperation message) { - Send(message); + Write(message); return Task.CompletedTask; } - internal virtual Task SendAsync(ReadOnlyMemory messages) + internal virtual Task WriteAsync(ReadOnlyMemory messages) { switch (messages.Length) { case 0: return Task.CompletedTask; - case 1: return SendAsync(messages.Span[0]); + case 1: return WriteAsync(messages.Span[0]); } int i = 0; @@ -129,7 +138,7 @@ internal virtual Task SendAsync(ReadOnlyMemory messages) { for (; i < messages.Length; i++) { - var pending = SendAsync(messages.Span[i]); + var pending = WriteAsync(messages.Span[i]); if (!pending.IsCompleted) return Awaited(this, pending, messages.Slice(i)); pending.GetAwaiter().GetResult(); @@ -151,7 +160,7 @@ static async Task Awaited(RespConnection connection, Task pending, ReadOnlyMemor await pending.ConfigureAwait(false); for (i = 1; i < messages.Length; i++) { - await connection.SendAsync(messages.Span[i]).ConfigureAwait(false); + await connection.WriteAsync(messages.Span[i]).ConfigureAwait(false); } } catch (Exception ex) diff --git a/src/RESPite/RespContext.cs b/src/RESPite/RespContext.cs index 8e1e4c3c0..eca30ecce 100644 --- a/src/RESPite/RespContext.cs +++ b/src/RESPite/RespContext.cs @@ -1,5 +1,7 @@ using System.Runtime.CompilerServices; using RESPite.Connections.Internal; +using RESPite.Internal; +using RESPite.Messages; namespace RESPite; @@ -23,7 +25,8 @@ public readonly struct RespContext public RespConnection Connection => _connection; public int Database => _database; - public RespCommandMap CommandMap => _connection.Configuration.CommandMap; + public RespCommandMap CommandMap => _connection.NonDefaultCommandMap ?? RespCommandMap.Default; + public TimeSpan SyncTimeout => _connection.SyncTimeout; /// /// REPLACES the associated with this context. diff --git a/src/RESPite/RespContextExtensions.cs b/src/RESPite/RespContextExtensions.cs index 96fae7d2b..e977c5f60 100644 --- a/src/RESPite/RespContextExtensions.cs +++ b/src/RESPite/RespContextExtensions.cs @@ -9,24 +9,32 @@ public static class RespContextExtensions public static RespOperationBuilder Command( this in RespContext context, ReadOnlySpan command, - TRequest value, + TRequest request, IRespFormatter formatter) - => new(in context, command, value, formatter); +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + => new(in context, command, request, formatter); - /* - public static RespOperationBuilder Command( + /* not sure that default formatters (RespFormatters.Get) make sense + public static RespOperationBuilder Command( this in RespContext context, ReadOnlySpan command, - T value) - => new(in context, command, value, RespFormatters.Get()); -*/ + in TRequest value) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + => new(in context, command, value, RespFormatters.Get()); + */ public static RespOperationBuilder Command(this in RespContext context, ReadOnlySpan command) => new(in context, command, false, RespFormatters.Empty); - /* - public static RespOperationBuilder Command(this in RespContext context, ReadOnlySpan command, - string value, bool isKey) + public static RespOperationBuilder Command( + this in RespContext context, + ReadOnlySpan command, + string value, + bool isKey) => new(in context, command, value, RespFormatters.String(isKey)); public static RespOperationBuilder Command( @@ -35,7 +43,31 @@ public static RespOperationBuilder Command( byte[] value, bool isKey) => new(in context, command, value, RespFormatters.ByteArray(isKey)); - */ + + /// + /// Creates an operation and synchronously writes it to the connection. + /// + /// The type of the request data being sent. + public static RespOperation Send( + this in RespContext context, + ReadOnlySpan command, + in TRequest request, + IRespFormatter formatter, + IRespParser parser) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + var op = CreateOperation(context, command, request, formatter, parser); + context.Connection.Write(op); + return op; + } + + /// + /// Creates an operation and synchronously writes it to the connection. + /// + /// The type of the request data being sent. + /// The type of the response data being received. public static RespOperation Send( this in RespContext context, ReadOnlySpan command, @@ -46,73 +78,162 @@ public static RespOperation Send( where TRequest : allows ref struct #endif { - var oversized = Serialize( - context.CommandMap, command, in request, formatter, out int length); - var msg = RespMessage.Get(parser) - .Init(oversized, 0, length, ArrayPool.Shared, context.CancellationToken); - RespOperation operation = new(msg); - context.Connection.Send(operation); - return operation; + var op = CreateOperation(context, command, request, formatter, parser); + context.Connection.Write(op); + return op; } + /// + /// Creates an operation and synchronously writes it to the connection. + /// + /// The type of the request data being sent. + /// The type of state data required by the parser. + /// The type of the response data being received. public static RespOperation Send( this in RespContext context, ReadOnlySpan command, in TRequest request, - in TState state, IRespFormatter formatter, + in TState state, IRespParser parser) #if NET9_0_OR_GREATER where TRequest : allows ref struct #endif { - var oversized = Serialize( - context.CommandMap, command, in request, formatter, out int length); - var msg = RespMessage.Get(in state, parser) - .Init(oversized, 0, length, ArrayPool.Shared, context.CancellationToken); - RespOperation operation = new(msg); - context.Connection.Send(operation); - return operation; + var op = CreateOperation(context, command, request, formatter, in state, parser); + context.Connection.Write(op); + return op; } - private static byte[] Serialize( - RespCommandMap commandMap, + /// + /// Creates an operation and asynchronously writes it to the connection, awaiting the completion of the underlying write. + /// + /// The type of the request data being sent. + public static ValueTask SendAsync( + this in RespContext context, ReadOnlySpan command, in TRequest request, IRespFormatter formatter, - out int length) + IRespParser parser) #if NET9_0_OR_GREATER where TRequest : allows ref struct #endif { - throw new NotImplementedException(); - /* - int size = 0; + var op = CreateOperation(context, command, request, formatter, parser); + var write = context.Connection.WriteAsync(op); + if (!write.IsCompleted) return AwaitedVoid(op, write); + write.GetAwaiter().GetResult(); + return new(op); - if (formatter is IRespSizeEstimator estimator) + static async ValueTask AwaitedVoid(RespOperation op, Task write) { - size = estimator.EstimateSize(command, request); + await write.ConfigureAwait(false); + return op; } + } + /// + /// Creates an operation and asynchronously writes it to the connection, awaiting the completion of the underlying write. + /// + /// The type of the request data being sent. + /// The type of the response data being received. + public static ValueTask> SendAsync( + this in RespContext context, + ReadOnlySpan command, + in TRequest request, + IRespFormatter formatter, + IRespParser parser) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + var op = CreateOperation(context, command, request, formatter, parser); + var write = context.Connection.WriteAsync(op); + if (!write.IsCompleted) return Awaited(op, write); + write.GetAwaiter().GetResult(); + return new(op); + } - var buffer = AmbientBufferWriter.Get(size); - try - { - var writer = new RespWriter(buffer); - if (!ReferenceEquals(commandMap, RespCommandMap.Default)) - { - writer.CommandMap = commandMap; - } - - formatter.Format(command, ref writer, request); - writer.Flush(); - return buffer.Detach(out length); - } - catch - { - buffer.Reset(); - throw; - } - */ + /// + /// Creates an operation and asynchronously writes it to the connection, awaiting the completion of the underlying write. + /// + /// The type of the request data being sent. + /// The type of state data required by the parser. + /// The type of the response data being received. + public static ValueTask> SendAsync( + this in RespContext context, + ReadOnlySpan command, + in TRequest request, + IRespFormatter formatter, + in TState state, + IRespParser parser) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + var op = CreateOperation(context, command, request, formatter, in state, parser); + var write = context.Connection.WriteAsync(op); + if (!write.IsCompleted) return Awaited(op, write); + write.GetAwaiter().GetResult(); + return new(op); + } + + private static async ValueTask> Awaited(RespOperation op, Task write) + { + await write.ConfigureAwait(false); + return op; + } + + public static RespOperation CreateOperation( + in RespContext context, // deliberately not "this" + ReadOnlySpan command, + in TRequest request, + IRespFormatter formatter, + IRespParser parser) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + var conn = context.Connection; + var memory = + conn.Serializer.Serialize(conn.NonDefaultCommandMap, command, request, formatter, out var block); + var msg = RespMessage.Get(parser).Init(memory, block, context.CancellationToken); + return new(msg); + } + + public static RespOperation CreateOperation( + in RespContext context, // deliberately not "this" + ReadOnlySpan command, + in TRequest request, + IRespFormatter formatter, + IRespParser parser) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + var conn = context.Connection; + var memory = + conn.Serializer.Serialize(conn.NonDefaultCommandMap, command, request, formatter, out var block); + var msg = RespMessage.Get(parser).Init(memory, block, context.CancellationToken); + return new(msg); + } + + public static RespOperation CreateOperation( + in RespContext context, // deliberately not "this" + ReadOnlySpan command, + in TRequest request, + IRespFormatter formatter, + in TState state, + IRespParser parser) +#if NET9_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + var conn = context.Connection; + var memory = + conn.Serializer.Serialize(conn.NonDefaultCommandMap, command, request, formatter, out var block); + var msg = RespMessage.Get(in state, parser) + .Init(memory, block, context.CancellationToken); + return new(msg); } } diff --git a/src/RESPite/RespFormatters.cs b/src/RESPite/RespFormatters.cs index 8680d484b..5fbe1681c 100644 --- a/src/RESPite/RespFormatters.cs +++ b/src/RESPite/RespFormatters.cs @@ -5,17 +5,21 @@ namespace RESPite; public static class RespFormatters { public static IRespFormatter String(bool isKey) => isKey ? Key.String : Value.String; + public static IRespFormatter> Chars(bool isKey) => isKey ? Key.Chars : Value.Chars; public static IRespFormatter ByteArray(bool isKey) => isKey ? Key.ByteArray : Value.ByteArray; + public static IRespFormatter> Bytes(bool isKey) => isKey ? Key.Bytes : Value.Bytes; public static IRespFormatter Empty => EmptyFormatter.Instance; public static class Key { - // ReSharper disable once MemberHidesStaticFromOuterClass + // ReSharper disable MemberHidesStaticFromOuterClass public static IRespFormatter String => Formatter.Default; - // ReSharper disable once MemberHidesStaticFromOuterClass + public static IRespFormatter> Chars => Formatter.Default; public static IRespFormatter ByteArray => Formatter.Default; - - internal sealed class Formatter : IRespFormatter, IRespFormatter + public static IRespFormatter> Bytes => Formatter.Default; + // ReSharper restore MemberHidesStaticFromOuterClass + internal sealed class Formatter : IRespFormatter, IRespFormatter, + IRespFormatter>, IRespFormatter> { private Formatter() { } public static readonly Formatter Default = new(); @@ -30,17 +34,29 @@ public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in writer.WriteCommand(command, 1); writer.WriteKey(value); } + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in ReadOnlyMemory value) + { + writer.WriteCommand(command, 1); + writer.WriteKey(value); + } + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in ReadOnlyMemory value) + { + writer.WriteCommand(command, 1); + writer.WriteKey(value); + } } } public static class Value { - // ReSharper disable once MemberHidesStaticFromOuterClass + // ReSharper disable MemberHidesStaticFromOuterClass public static IRespFormatter String => Formatter.Default; - // ReSharper disable once MemberHidesStaticFromOuterClass + public static IRespFormatter> Chars => Formatter.Default; public static IRespFormatter ByteArray => Formatter.Default; - - internal sealed class Formatter : IRespFormatter, IRespFormatter + public static IRespFormatter> Bytes => Formatter.Default; + // ReSharper restore MemberHidesStaticFromOuterClass + internal sealed class Formatter : IRespFormatter, IRespFormatter, + IRespFormatter>, IRespFormatter> { private Formatter() { } public static readonly Formatter Default = new(); @@ -55,6 +71,16 @@ public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in writer.WriteCommand(command, 1); writer.WriteBulkString(value); } + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in ReadOnlyMemory value) + { + writer.WriteCommand(command, 1); + writer.WriteBulkString(value); + } + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in ReadOnlyMemory value) + { + writer.WriteCommand(command, 1); + writer.WriteBulkString(value); + } } } diff --git a/src/RESPite/RespOperationBuilder.cs b/src/RESPite/RespOperationBuilder.cs index e59ac5a3d..e686cb724 100644 --- a/src/RESPite/RespOperationBuilder.cs +++ b/src/RESPite/RespOperationBuilder.cs @@ -5,7 +5,7 @@ namespace RESPite; public readonly ref struct RespOperationBuilder( in RespContext context, ReadOnlySpan command, - TRequest value, + TRequest request, IRespFormatter formatter) #if NET9_0_OR_GREATER where TRequest : allows ref struct @@ -13,38 +13,32 @@ public readonly ref struct RespOperationBuilder( { private readonly RespContext _context = context; private readonly ReadOnlySpan _command = command; - private readonly TRequest _value = value; // cannot inline to .ctor because of "allows ref struct" + private readonly TRequest request = request; // cannot inline to .ctor because of "allows ref struct" public TResponse Wait() - => Send(RespParsers.Get()).Wait(); + => Send(RespParsers.Get()).Wait(_context.SyncTimeout); public TResponse Wait(IRespParser parser) - => Send(parser).Wait(); + => Send(parser).Wait(_context.SyncTimeout); public TResponse Wait(in TState state) - => Send(in state, RespParsers.Get()).Wait(); + => Send(in state, RespParsers.Get()).Wait(_context.SyncTimeout); public TResponse Wait(in TState state, IRespParser parser) - => Send(in state, parser).Wait(); + => Send(in state, parser).Wait(_context.SyncTimeout); - public void Wait() => Send(RespParsers.Success).Wait(); + public void Wait() => Send(RespParsers.Success).Wait(_context.SyncTimeout); public RespOperation Send() - => Send(RespParsers.Get()); + => _context.Send(_command, request, formatter, RespParsers.Get()); public RespOperation Send(IRespParser parser) - { - _ = _context; - _ = formatter; - throw new NotImplementedException(); - } + => _context.Send(_command, request, formatter, parser); - public RespOperation Send() => Send(RespParsers.Success); + public RespOperation Send() => _context.Send(_command, request, formatter, RespParsers.Success); public RespOperation Send(in TState state) - => Send(in state, RespParsers.Get()); + => _context.Send(_command, request, formatter, in state, RespParsers.Get()); public RespOperation Send(in TState state, IRespParser parser) - { - throw new NotImplementedException(); - } + => _context.Send(_command, request, formatter, in state, parser); } diff --git a/tests/RESP.Core.Tests/BlockBufferTests.cs b/tests/RESP.Core.Tests/BlockBufferTests.cs index 10d2d3d8f..1bef30319 100644 --- a/tests/RESP.Core.Tests/BlockBufferTests.cs +++ b/tests/RESP.Core.Tests/BlockBufferTests.cs @@ -29,9 +29,9 @@ private void Log(ReadOnlySpan span) public void CanCreateAndWriteSimpleBuffer() { var buffer = BlockBufferSerializer.Create(); - var a = buffer.Serialize("get"u8, "abc", RespFormatters.Key.String, out var blockA); - var b = buffer.Serialize("get"u8, "def", RespFormatters.Key.String, out var blockB); - var c = buffer.Serialize("get"u8, "ghi", RespFormatters.Key.String, out var blockC); + var a = buffer.Serialize(null, "get"u8, "abc", RespFormatters.Key.String, out var blockA); + var b = buffer.Serialize(null, "get"u8, "def", RespFormatters.Key.String, out var blockB); + var c = buffer.Serialize(null, "get"u8, "ghi", RespFormatters.Key.String, out var blockC); buffer.Clear(); #if DEBUG Assert.Equal(1, buffer.CountAdded); @@ -72,9 +72,9 @@ public void CanWriteLotsOfBuffers_WithCheapReset() // when messages are consumed #endif for (int i = 0; i < 5000; i++) { - var a = buffer.Serialize("get"u8, "abc", RespFormatters.Key.String, out var blockA); - var b = buffer.Serialize("get"u8, "def", RespFormatters.Key.String, out var blockB); - var c = buffer.Serialize("get"u8, "ghi", RespFormatters.Key.String, out var blockC); + var a = buffer.Serialize(null, "get"u8, "abc", RespFormatters.Key.String, out var blockA); + var b = buffer.Serialize(null, "get"u8, "def", RespFormatters.Key.String, out var blockB); + var c = buffer.Serialize(null, "get"u8, "ghi", RespFormatters.Key.String, out var blockC); blockA?.Dispose(); blockB?.Dispose(); blockC?.Dispose(); @@ -118,11 +118,11 @@ public void CanWriteLotsOfBuffers() #endif for (int i = 0; i < 5000; i++) { - _ = buffer.Serialize("get"u8, "abc", RespFormatters.Key.String, out var block); + _ = buffer.Serialize(null, "get"u8, "abc", RespFormatters.Key.String, out var block); if (block is not null) blocks.Add(block); - _ = buffer.Serialize("get"u8, "def", RespFormatters.Key.String, out block); + _ = buffer.Serialize(null, "get"u8, "def", RespFormatters.Key.String, out block); if (block is not null) blocks.Add(block); - _ = buffer.Serialize("get"u8, "ghi", RespFormatters.Key.String, out block); + _ = buffer.Serialize(null, "get"u8, "ghi", RespFormatters.Key.String, out block); if (block is not null) blocks.Add(block); } From a470b9fded04264491ae06926a69c45ce63e47d1 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 1 Sep 2025 16:40:51 +0100 Subject: [PATCH 021/108] hunt perf glitch --- Directory.Build.props | 1 + src/RESPite.Benchmark/BenchmarkBase.cs | 4 ++ src/RESPite.Benchmark/NewCoreBenchmark.cs | 1 + src/RESPite.Benchmark/Program.cs | 6 +- .../RESPite.Benchmark.csproj | 1 - .../Internal/BasicBatchConnection.cs | 71 ++++++++++++------- .../Internal/DecoratorConnection.cs | 34 +++++++++ .../Connections/Internal/NullConnection.cs | 8 +++ src/RESPite/Connections/RespConnectionPool.cs | 14 ++++ src/RESPite/Internal/StreamConnection.cs | 24 ++++++- src/RESPite/RespBatch.cs | 8 +++ src/RESPite/RespConnection.cs | 29 +++++++- 12 files changed, 168 insertions(+), 33 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index bc079a5ff..a0b807249 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -33,6 +33,7 @@ false true 00240000048000009400000006020000002400005253413100040000010001007791a689e9d8950b44a9a8886baad2ea180e7a8a854f158c9b98345ca5009cdd2362c84f368f1c3658c132b3c0f74e44ff16aeb2e5b353b6e0fe02f923a050470caeac2bde47a2238a9c7125ed7dab14f486a5a64558df96640933b9f2b6db188fc4a820f96dce963b662fa8864adbff38e5b4542343f162ecdc6dad16912fff + LatestMajor preview diff --git a/src/RESPite.Benchmark/BenchmarkBase.cs b/src/RESPite.Benchmark/BenchmarkBase.cs index 33b401449..4ec50ebb1 100644 --- a/src/RESPite.Benchmark/BenchmarkBase.cs +++ b/src/RESPite.Benchmark/BenchmarkBase.cs @@ -369,6 +369,10 @@ protected async Task RunAsync( { Console.Write($", {PipelineMode}: {PipelineDepth:#,##0}"); } + else + { + Console.Write(", sequential"); + } Console.WriteLine(")"); } diff --git a/src/RESPite.Benchmark/NewCoreBenchmark.cs b/src/RESPite.Benchmark/NewCoreBenchmark.cs index c0df677ec..ecd5478c3 100644 --- a/src/RESPite.Benchmark/NewCoreBenchmark.cs +++ b/src/RESPite.Benchmark/NewCoreBenchmark.cs @@ -30,6 +30,7 @@ public NewCoreBenchmark(string[] args) : base(args) _clients = new RespContext[ClientCount]; _connectionPool = new(count: Multiplexed ? 1 : ClientCount); + _connectionPool.ConnectionError += (sender, args) => Program.WriteException(args.Exception, args.Operation); _pairs = new (string, byte[])[10]; for (var i = 0; i < 10; i++) diff --git a/src/RESPite.Benchmark/Program.cs b/src/RESPite.Benchmark/Program.cs index 694c09a11..c7af0e861 100644 --- a/src/RESPite.Benchmark/Program.cs +++ b/src/RESPite.Benchmark/Program.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Threading.Tasks; namespace RESPite.Benchmark; @@ -54,8 +55,10 @@ private static async Task Main(string[] args) } } - internal static void WriteException(Exception? ex) + internal static void WriteException(Exception? ex, [CallerMemberName] string operation = "") { + Console.Error.WriteLine(); + Console.Error.WriteLine($"### EXCEPTION: {operation}"); while (ex is not null) { Console.Error.WriteLine(); @@ -73,5 +76,6 @@ internal static void WriteException(Exception? ex) ex = ex.InnerException; } + Console.Error.WriteLine(); } } diff --git a/src/RESPite.Benchmark/RESPite.Benchmark.csproj b/src/RESPite.Benchmark/RESPite.Benchmark.csproj index a12a9ec55..36b6d3c6a 100644 --- a/src/RESPite.Benchmark/RESPite.Benchmark.csproj +++ b/src/RESPite.Benchmark/RESPite.Benchmark.csproj @@ -14,7 +14,6 @@ True false false - LatestMajor diff --git a/src/RESPite/Connections/Internal/BasicBatchConnection.cs b/src/RESPite/Connections/Internal/BasicBatchConnection.cs index 4659bdfe9..c571e0fd6 100644 --- a/src/RESPite/Connections/Internal/BasicBatchConnection.cs +++ b/src/RESPite/Connections/Internal/BasicBatchConnection.cs @@ -52,8 +52,6 @@ items will be added */ base.OnDispose(disposing); } - internal override bool IsHealthy => base.IsHealthy & Tail.IsHealthy; - internal override int OutstandingOperations { get @@ -134,15 +132,25 @@ private int Flush(out RespOperation[] oversized, out RespOperation single) } } + public override event EventHandler? ConnectionError; + public override Task FlushAsync() { - var count = Flush(out var oversized, out var single); - return count switch + try + { + var count = Flush(out var oversized, out var single); + return count switch + { + 0 => Task.CompletedTask, + 1 => Tail.WriteAsync(single!), + _ => SendAndRecycleAsync(Tail, oversized, count), + }; + } + catch (Exception ex) { - 0 => Task.CompletedTask, - 1 => Tail.WriteAsync(single!), - _ => SendAndRecycleAsync(Tail, oversized, count), - }; + OnConnectionError(ConnectionError, ex); + throw; + } static async Task SendAndRecycleAsync(RespConnection tail, RespOperation[] oversized, int count) { @@ -153,26 +161,43 @@ static async Task SendAndRecycleAsync(RespConnection tail, RespOperation[] overs } catch (Exception ex) { - foreach (var message in oversized.AsSpan(0, count)) - { - message.Message.TrySetException(message.Token, ex); - } - + TrySetException(oversized.AsSpan(0, count), ex); throw; } } } + private static void TrySetException(ReadOnlySpan messages, Exception ex) + { + foreach (var message in messages) + { + message.Message.TrySetException(message.Token, ex); + } + } + public override void Flush() { - var count = Flush(out var oversized, out var single); - switch (count) + string operation = nameof(Flush); + int count; + RespOperation[] oversized; + RespOperation single; + try { - case 0: - return; - case 1: - Tail.Write(single!); - return; + count = Flush(out oversized, out single); + switch (count) + { + case 0: + return; + case 1: + operation = nameof(Tail.Write); + Tail.Write(single!); + return; + } + } + catch (Exception ex) + { + OnConnectionError(ConnectionError, ex, operation); + throw; } try @@ -181,11 +206,7 @@ public override void Flush() } catch (Exception ex) { - foreach (var message in oversized.AsSpan(0, count)) - { - message.Message.TrySetException(message.Token, ex); - } - + TrySetException(oversized.AsSpan(0, count), ex); throw; } finally diff --git a/src/RESPite/Connections/Internal/DecoratorConnection.cs b/src/RESPite/Connections/Internal/DecoratorConnection.cs index efd86e636..af74f9fad 100644 --- a/src/RESPite/Connections/Internal/DecoratorConnection.cs +++ b/src/RESPite/Connections/Internal/DecoratorConnection.cs @@ -10,6 +10,8 @@ public DecoratorConnection(in RespContext tail, RespConfiguration? configuration Tail = tail.Connection; } + internal override void ThrowIfUnhealthy() => Tail.ThrowIfUnhealthy(); + protected virtual bool OwnsConnection => true; internal override bool IsHealthy => base.IsHealthy & Tail.IsHealthy; @@ -32,4 +34,36 @@ protected override ValueTask OnDisposeAsync() => public override Task WriteAsync(in RespOperation message) => Tail.WriteAsync(in message); internal override Task WriteAsync(ReadOnlyMemory messages) => Tail.WriteAsync(messages); + + private event EventHandler? PrivateConnectionError; // to wrap "sender" + private EventHandler? _onConnectionError; // local lazy callback + public override event EventHandler? ConnectionError + { + add + { + if (value is not null) + { + if (PrivateConnectionError is null) + { + Tail.ConnectionError += _onConnectionError ??= OnConnectionError; + } + + PrivateConnectionError += value; + } + } + remove + { + if (value is not null) + { + PrivateConnectionError -= value; + if (PrivateConnectionError is null) // last unsubscribe + { + Tail.ConnectionError -= _onConnectionError; + } + } + } + } + + private void OnConnectionError(object? sender, RespConnectionErrorEventArgs e) + => PrivateConnectionError?.Invoke(this, e); // mask sender } diff --git a/src/RESPite/Connections/Internal/NullConnection.cs b/src/RESPite/Connections/Internal/NullConnection.cs index fecfcdc1e..79cd5954f 100644 --- a/src/RESPite/Connections/Internal/NullConnection.cs +++ b/src/RESPite/Connections/Internal/NullConnection.cs @@ -26,4 +26,12 @@ public override Task WriteAsync(in RespOperation message) Write(message); return Task.CompletedTask; } + + public override event EventHandler? ConnectionError + { + add { } + remove { } + } + + internal override void ThrowIfUnhealthy() { } } diff --git a/src/RESPite/Connections/RespConnectionPool.cs b/src/RESPite/Connections/RespConnectionPool.cs index 7d08739e5..b305f8cc8 100644 --- a/src/RESPite/Connections/RespConnectionPool.cs +++ b/src/RESPite/Connections/RespConnectionPool.cs @@ -23,6 +23,12 @@ public sealed class RespConnectionPool : IDisposable public ref readonly RespContext Template => ref _defaultTemplate; + public event EventHandler? ConnectionError; + private void OnConnectionError(object? sender, RespConnection.RespConnectionErrorEventArgs e) + => ConnectionError?.Invoke(this, e); // mask sender + + private readonly EventHandler _onConnectionError; + public RespConnectionPool( in RespContext template, Func createConnection, @@ -34,6 +40,7 @@ public RespConnectionPool( // swap out the connection for a dummy (retaining the configuration) var configuredConnection = NullConnection.WithConfiguration(template.Connection.Configuration); _defaultTemplate = template.WithConnection(configuredConnection); + _onConnectionError = OnConnectionError; } public RespConnectionPool( @@ -86,6 +93,7 @@ public RespConnection GetConnection(in RespContext template) if (!_pool.TryDequeue(out var connection)) { connection = _createConnection(template.Connection.Configuration); + connection.ConnectionError += _onConnectionError; } return new PoolWrapper(this, template.WithConnection(connection)); } @@ -123,6 +131,12 @@ private sealed class PoolWrapper( { protected override bool OwnsConnection => false; + private const string ConnectionErrorNotSupportedMessage = $"{nameof(ConnectionError)} events are not supported on pooled connections; use {nameof(RespConnectionPool)}.{nameof(RespConnectionPool.ConnectionError)} instead"; + public override event EventHandler? ConnectionError + { + add => throw new NotSupportedException(ConnectionErrorNotSupportedMessage); + remove => throw new NotSupportedException(ConnectionErrorNotSupportedMessage); + } protected override void OnDispose(bool disposing) { if (disposing) diff --git a/src/RESPite/Internal/StreamConnection.cs b/src/RESPite/Internal/StreamConnection.cs index 48552ed52..5eed443cc 100644 --- a/src/RESPite/Internal/StreamConnection.cs +++ b/src/RESPite/Internal/StreamConnection.cs @@ -1,4 +1,4 @@ -#define PARSE_DETAIL // additional trace info in CommitAndParseFrames +// #define PARSE_DETAIL // additional trace info in CommitAndParseFrames #if DEBUG #define PARSE_DETAIL // always enable this in debug builds @@ -46,6 +46,7 @@ public StreamConnection(in RespContext context, RespConfiguration configuration, static void Throw() => throw new ArgumentException("Stream must be readable and writable", nameof(tail)); } + public StreamConnection(RespConfiguration configuration, Stream tail, bool asyncRead = true) : this(RespContext.Null, configuration, tail, asyncRead) { @@ -271,7 +272,15 @@ private void ReadAll() } } - private void OnReadException(Exception ex) + internal override void ThrowIfUnhealthy() + { + if (_fault is { } fault) Throw(fault); + base.ThrowIfUnhealthy(); + + static void Throw(Exception fault) => throw new InvalidOperationException("Connection is unhealthy", fault); + } + + private void OnReadException(Exception ex, [CallerMemberName] string operation = "") { _fault ??= ex; Volatile.Write(ref _readStatus, READER_FAILED); @@ -281,6 +290,8 @@ private void OnReadException(Exception ex) { pending.Message.TrySetException(pending.Token, ex); } + + OnConnectionError(ConnectionError, ex, operation); } private void OnReadAllFinally() @@ -476,6 +487,7 @@ public override void Write(in RespOperation message) ActivationHelper.DebugBreak(); ReleaseWriter(WRITER_DOOMED); if (releaseRequest) message.Message.ReleaseRequest(); + OnConnectionError(ConnectionError, ex); throw; } } @@ -532,6 +544,7 @@ internal override void Write(ReadOnlySpan messages) message.Message.TrySetException(message.Token, ex); } + OnConnectionError(ConnectionError, ex); throw; } } @@ -575,6 +588,7 @@ public override Task WriteAsync(in RespOperation message) ActivationHelper.DebugBreak(); ReleaseWriter(WRITER_DOOMED); if (releaseRequest) message.Message.ReleaseRequest(); + OnConnectionError(ConnectionError, ex); throw; } @@ -595,9 +609,10 @@ static async Task AwaitedSingleWithToken( @this.ReleaseWriter(); message.ReleaseRequest(); } - catch + catch (Exception ex) { @this.ReleaseWriter(WRITER_DOOMED); + OnConnectionError(@this.ConnectionError, ex, $"{nameof(WriteAsync)}:{nameof(AwaitedSingleWithToken)}"); throw; } } @@ -616,6 +631,8 @@ internal override Task WriteAsync(ReadOnlyMemory messages) } } + public override event EventHandler? ConnectionError; // use simple handler + private async Task CombineAndSendMultipleAsync(StreamConnection @this, ReadOnlyMemory messages) { TakeWriter(); @@ -686,6 +703,7 @@ private async Task CombineAndSendMultipleAsync(StreamConnection @this, ReadOnlyM message.Message.TrySetException(message.Token, ex); } + OnConnectionError(ConnectionError, ex); throw; } } diff --git a/src/RESPite/RespBatch.cs b/src/RESPite/RespBatch.cs index 488d4f98c..bdeb0a037 100644 --- a/src/RESPite/RespBatch.cs +++ b/src/RESPite/RespBatch.cs @@ -11,4 +11,12 @@ private protected RespBatch(in RespContext tail) : base(tail) public abstract Task FlushAsync(); public abstract void Flush(); + + internal override void ThrowIfUnhealthy() + { + Tail.ThrowIfUnhealthy(); + base.ThrowIfUnhealthy(); + } + + internal override bool IsHealthy => base.IsHealthy & Tail.IsHealthy; } diff --git a/src/RESPite/RespConnection.cs b/src/RESPite/RespConnection.cs index b29537d15..4483bbd53 100644 --- a/src/RESPite/RespConnection.cs +++ b/src/RESPite/RespConnection.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; using System.Runtime.CompilerServices; @@ -9,12 +10,26 @@ namespace RESPite; public abstract class RespConnection : IDisposable, IAsyncDisposable { + public sealed class RespConnectionErrorEventArgs(Exception exception, [CallerMemberName] string operation = "") : EventArgs + { + public Exception Exception { get; } = exception; + public string Operation { get; } = operation; + } + private bool _isDisposed; internal bool IsDisposed => _isDisposed; private readonly RespContext _context; public ref readonly RespContext Context => ref _context; public RespConfiguration Configuration { get; } + public abstract event EventHandler? ConnectionError; + private protected static void OnConnectionError( + EventHandler? handler, + Exception exception, + [CallerMemberName] string operation = "") + { + handler?.Invoke(null, new(exception, operation)); + } internal virtual bool IsHealthy => !_isDisposed; @@ -43,7 +58,9 @@ private protected RespConnection(in RespContext tail, RespConfiguration? configu var conn = tail.Connection; if (conn is not { IsHealthy: true }) { - ThrowUnhealthy(); + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (conn is null) ThrowNullTail(); // trust no-one + else conn.ThrowIfUnhealthy(); } Configuration = configuration ?? conn.Configuration; @@ -54,8 +71,14 @@ private protected RespConnection(in RespContext tail, RespConfiguration? configu NonDefaultCommandMap = ReferenceEquals(commandMap, RespCommandMap.Default) ? null : commandMap; SyncTimeout = Configuration.SyncTimeout; // snapshot to reduce indirection - static void ThrowUnhealthy() => - throw new ArgumentException("A healthy tail connection is required.", nameof(tail)); + [DoesNotReturn] + static void ThrowNullTail() => + throw new ArgumentException("No tail connection provided.", nameof(tail)); + } + + internal virtual void ThrowIfUnhealthy() + { + if (_isDisposed) ThrowDisposed(); } // this is atypical - only for use when creating null connections From 804a2098593af383132a154f59093adee7734ec2 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 1 Sep 2025 16:49:43 +0100 Subject: [PATCH 022/108] packable --- src/RESPite.Benchmark/RESPite.Benchmark.csproj | 1 + src/RESPite.Benchmark/readme.md | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 src/RESPite.Benchmark/readme.md diff --git a/src/RESPite.Benchmark/RESPite.Benchmark.csproj b/src/RESPite.Benchmark/RESPite.Benchmark.csproj index 36b6d3c6a..be171656d 100644 --- a/src/RESPite.Benchmark/RESPite.Benchmark.csproj +++ b/src/RESPite.Benchmark/RESPite.Benchmark.csproj @@ -20,5 +20,6 @@ + diff --git a/src/RESPite.Benchmark/readme.md b/src/RESPite.Benchmark/readme.md new file mode 100644 index 000000000..6767895f4 --- /dev/null +++ b/src/RESPite.Benchmark/readme.md @@ -0,0 +1,18 @@ +# resp-benchmark + +The `resp-benchmark` tool is a command-line "RESP" benchmark client, comparable to `redis-benchmark`, and +many of the arguments are the same. This is mostly for internal team usage, but is included here for +reference. + +Example usage: + +``` bash +> dotnet tool install -g RESPite.Benchmark + +# basic usage +> resp-benchmark + +# 50 clients, pipeline to 100, multiplexed, 1M operations, only test incr, loop +> resp-benchmark -c 50 -P 100 -n 1000000 +m -t incr -l + +``` \ No newline at end of file From d602b7b004948fccb13555106c950291dff2e3f2 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 1 Sep 2025 17:29:49 +0100 Subject: [PATCH 023/108] nits --- src/RESPite.Benchmark/BenchmarkBase.cs | 2 +- src/RESPite.Benchmark/NewCoreBenchmark.cs | 2 +- src/RESPite/Internal/BlockBuffer.cs | 2 +- src/RESPite/Messages/RespReader.cs | 20 ++++++++++++-------- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/RESPite.Benchmark/BenchmarkBase.cs b/src/RESPite.Benchmark/BenchmarkBase.cs index 4ec50ebb1..bd367b0e9 100644 --- a/src/RESPite.Benchmark/BenchmarkBase.cs +++ b/src/RESPite.Benchmark/BenchmarkBase.cs @@ -442,7 +442,7 @@ protected async Task RunAsync( catch (Exception ex) { if (Quiet) Console.WriteLine(); - Console.Error.WriteLine(ex.Message); + Program.WriteException(ex, name); } finally { diff --git a/src/RESPite.Benchmark/NewCoreBenchmark.cs b/src/RESPite.Benchmark/NewCoreBenchmark.cs index ecd5478c3..f330e001e 100644 --- a/src/RESPite.Benchmark/NewCoreBenchmark.cs +++ b/src/RESPite.Benchmark/NewCoreBenchmark.cs @@ -231,7 +231,7 @@ internal static partial class RedisCommands internal static partial int LPush(this in RespContext ctx, string key, byte[] payload); [RespCommand(Formatter = "LPushFormatter.Instance")] - internal static partial void LPush(this in RespContext ctx, string key, byte[] payload, int count); + internal static partial int LPush(this in RespContext ctx, string key, byte[] payload, int count); private sealed class LPushFormatter : IRespFormatter<(string Key, byte[] Payload, int Count)> { diff --git a/src/RESPite/Internal/BlockBuffer.cs b/src/RESPite/Internal/BlockBuffer.cs index 0e3b4d746..426144586 100644 --- a/src/RESPite/Internal/BlockBuffer.cs +++ b/src/RESPite/Internal/BlockBuffer.cs @@ -185,7 +185,7 @@ public static void Advance(BlockBufferSerializer parent, int count) if (count == 0) return; if (count < 0) ThrowOutOfRange(); var buffer = parent.Buffer; - if (buffer is null || buffer.Available <= count) ThrowOutOfRange(); + if (buffer is null || buffer.Available < count) ThrowOutOfRange(); buffer._writeOffset += count; [DoesNotReturn] diff --git a/src/RESPite/Messages/RespReader.cs b/src/RESPite/Messages/RespReader.cs index c96ad43ce..a22713892 100644 --- a/src/RESPite/Messages/RespReader.cs +++ b/src/RESPite/Messages/RespReader.cs @@ -1557,15 +1557,19 @@ public readonly decimal ReadDecimal() public readonly bool ReadBoolean() { var span = Buffer(stackalloc byte[2]); - if (span.Length == 1) + switch (span.Length) { - switch (span[0]) - { - case (byte)'0' when Prefix == RespPrefix.Integer: return false; - case (byte)'1' when Prefix == RespPrefix.Integer: return true; - case (byte)'f' when Prefix == RespPrefix.Boolean: return false; - case (byte)'t' when Prefix == RespPrefix.Boolean: return true; - } + case 1: + switch (span[0]) + { + case (byte)'0' when Prefix == RespPrefix.Integer: return false; + case (byte)'1' when Prefix == RespPrefix.Integer: return true; + case (byte)'f' when Prefix == RespPrefix.Boolean: return false; + case (byte)'t' when Prefix == RespPrefix.Boolean: return true; + } + + break; + case 2 when Prefix == RespPrefix.SimpleString && IsOK(): return true; } ThrowFormatException(); return false; From 9051a156756a1fbf2982fc1a5c4b40c25d361404 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 1 Sep 2025 19:30:40 +0100 Subject: [PATCH 024/108] add random scrambles when recycling (debug only) --- src/RESPite/Internal/BlockBuffer.cs | 2 ++ src/RESPite/Internal/CycleBuffer.cs | 1 + src/RESPite/Internal/RespMessageBase.cs | 1 + .../Internal/RespOperationExtensions.cs | 24 +++++++++++++++++-- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/RESPite/Internal/BlockBuffer.cs b/src/RESPite/Internal/BlockBuffer.cs index 426144586..f1a90d4e0 100644 --- a/src/RESPite/Internal/BlockBuffer.cs +++ b/src/RESPite/Internal/BlockBuffer.cs @@ -52,6 +52,7 @@ private void Recycle() var count = Volatile.Read(ref _refCount); if (count == 0) { + _buffer.DebugScramble(); _arrayPool.Return(_buffer); #if DEBUG GC.SuppressFinalize(this); @@ -172,6 +173,7 @@ private bool TryResizeFor(int extraBytes) // copy the existing data (we always expect some, since we've clamped extraBytes to be // much smaller than the default buffer size) NonFinalizedData.CopyTo(newArray); + _buffer.DebugScramble(); _arrayPool.Return(_buffer); _buffer = newArray; return true; diff --git a/src/RESPite/Internal/CycleBuffer.cs b/src/RESPite/Internal/CycleBuffer.cs index 7bd4ca4b5..a0d827679 100644 --- a/src/RESPite/Internal/CycleBuffer.cs +++ b/src/RESPite/Internal/CycleBuffer.cs @@ -610,6 +610,7 @@ public void DebugAssertValidChain([CallerMemberName] string blame = "") public void AppendOrRecycle(Segment segment, int maxDepth) { + segment.Memory.DebugScramble(); var node = this; while (maxDepth-- > 0 && node is not null) { diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs index 5816f7fd4..bfa050c37 100644 --- a/src/RESPite/Internal/RespMessageBase.cs +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -258,6 +258,7 @@ private bool { if (oldCount == 1) // we were the last one; recycle { + _request.DebugScramble(); if (_requestOwner is IDisposable owner) { owner.Dispose(); diff --git a/src/RESPite/Internal/RespOperationExtensions.cs b/src/RESPite/Internal/RespOperationExtensions.cs index 23ed0f952..1d0d0887c 100644 --- a/src/RESPite/Internal/RespOperationExtensions.cs +++ b/src/RESPite/Internal/RespOperationExtensions.cs @@ -1,8 +1,10 @@ -using System.Runtime.CompilerServices; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace RESPite.Internal; -public static class RespOperationExtensions +internal static class RespOperationExtensions { #if PREVIEW_LANGVER extension(in RespOperation operation) @@ -15,4 +17,22 @@ public static class RespOperationExtensions ref Unsafe.AsRef(in operation)); } #endif + + // if we're recycling a buffer, we need to consider it trashable by other threads; for + // debug purposes, force this by overwriting with *****, aka the meaning of life + [Conditional("DEBUG")] + internal static void DebugScramble(this Span value) + => value.Fill(42); + + [Conditional("DEBUG")] + internal static void DebugScramble(this Memory value) + => value.Span.Fill(42); + + [Conditional("DEBUG")] + internal static void DebugScramble(this ReadOnlyMemory value) + => MemoryMarshal.AsMemory(value).Span.Fill(42); + + [Conditional("DEBUG")] + internal static void DebugScramble(this byte[] value) + => value.AsSpan().Fill(42); } From 8e728a8be79259845261eede4689496384e93435 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 1 Sep 2025 20:42:00 +0100 Subject: [PATCH 025/108] packaging --- Build.csproj | 1 + src/RESPite/RESPite.csproj | 1 + 2 files changed, 2 insertions(+) diff --git a/Build.csproj b/Build.csproj index 3e16e801c..41fb15b0c 100644 --- a/Build.csproj +++ b/Build.csproj @@ -1,5 +1,6 @@ + diff --git a/src/RESPite/RESPite.csproj b/src/RESPite/RESPite.csproj index e11cf0941..c784a295c 100644 --- a/src/RESPite/RESPite.csproj +++ b/src/RESPite/RESPite.csproj @@ -17,6 +17,7 @@ + From 7d4d3725c0b63d6ce75dc8fe3966d52e5fd79e8c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 1 Sep 2025 20:48:34 +0100 Subject: [PATCH 026/108] include readme in package --- src/RESPite/RESPite.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/RESPite/RESPite.csproj b/src/RESPite/RESPite.csproj index c784a295c..c474dcfb3 100644 --- a/src/RESPite/RESPite.csproj +++ b/src/RESPite/RESPite.csproj @@ -7,6 +7,7 @@ enable $(NoWarn);CS1591 2025 - $([System.DateTime]::Now.Year) Marc Gravell + readme.md @@ -17,6 +18,7 @@ + From 86032ab0b30c5c899967ff3422793c4e1dde2bde Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 2 Sep 2025 09:17:37 +0100 Subject: [PATCH 027/108] custom memory manager --- src/RESPite.Benchmark/BenchmarkBase.cs | 4 +- src/RESPite/Internal/BlockBuffer.cs | 112 ++++++++++++------ src/RESPite/Internal/BlockBufferSerializer.cs | 13 +- src/RESPite/Internal/DebugCounters.cs | 48 +++++++- src/RESPite/Internal/RespMessageBase.cs | 40 +------ .../Internal/RespOperationExtensions.cs | 7 +- .../SynchronizedBlockBufferSerializer.cs | 5 +- src/RESPite/RespContextExtensions.cs | 13 +- tests/RESP.Core.Tests/BlockBufferTests.cs | 46 +++---- 9 files changed, 176 insertions(+), 112 deletions(-) diff --git a/src/RESPite.Benchmark/BenchmarkBase.cs b/src/RESPite.Benchmark/BenchmarkBase.cs index bd367b0e9..7833b6828 100644 --- a/src/RESPite.Benchmark/BenchmarkBase.cs +++ b/src/RESPite.Benchmark/BenchmarkBase.cs @@ -519,7 +519,7 @@ protected async Task RunAsync( Console.Write( $"; created: {counters.BufferCreatedCount:#,###,##0}, {FormatBytes(counters.BufferTotalBytes)}"); // always write recycled count - it being zero is important - Console.Write($"; recycled: {counters.BufferRecycledCount:#,###,##0}"); + Console.Write($"; recycled: {counters.BufferRecycledCount:#,###,##0}, {FormatBytes(counters.BufferRecycledBytes)}"); } if (counters.BufferMessageCount != 0) @@ -527,6 +527,8 @@ protected async Task RunAsync( Console.Write( $"; {counters.BufferMessageCount:#,###,##0} messages, {FormatBytes(counters.BufferMessageBytes)}"); } + Console.Write( + $"; max working {FormatBytes(counters.BufferMaxOutstandingBytes)}; {counters.BufferPinCount:#,###,##0} pins; {counters.BufferLeakCount:#,###,##0} leaks"); Console.WriteLine(); } diff --git a/src/RESPite/Internal/BlockBuffer.cs b/src/RESPite/Internal/BlockBuffer.cs index f1a90d4e0..16d1e6969 100644 --- a/src/RESPite/Internal/BlockBuffer.cs +++ b/src/RESPite/Internal/BlockBuffer.cs @@ -2,18 +2,19 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace RESPite.Internal; internal abstract partial class BlockBufferSerializer { - private protected sealed class BlockBuffer : IDisposable + internal sealed class BlockBuffer : MemoryManager { private BlockBuffer(BlockBufferSerializer parent, int minCapacity) { _arrayPool = parent._arrayPool; - _buffer = _arrayPool.Rent(minCapacity); - DebugCounters.OnBufferCapacity(_buffer.Length); + _array = _arrayPool.Rent(minCapacity); + DebugCounters.OnBufferCapacity(_array.Length); #if DEBUG _parent = parent; parent.DebugBufferCreated(); @@ -23,7 +24,7 @@ private BlockBuffer(BlockBufferSerializer parent, int minCapacity) private int _refCount = 1; private int _finalizedOffset, _writeOffset; private readonly ArrayPool _arrayPool; - private byte[] _buffer; + private byte[] _array; #if DEBUG private int _finalizedCount; private BlockBufferSerializer _parent; @@ -35,39 +36,59 @@ public override string ToString() => #endif $"{_finalizedOffset} finalized bytes; writing: {NonFinalizedData.Length} bytes, {Available} available; observers: {_refCount}"; - private int Available => _buffer.Length - _writeOffset; - public Memory UncommittedMemory => _buffer.AsMemory(_writeOffset); - public Span UncommittedSpan => _buffer.AsSpan(_writeOffset); + // only used when filling; _buffer should be non-null + private int Available => _array.Length - _writeOffset; + public Memory UncommittedMemory => _array.AsMemory(_writeOffset); + public Span UncommittedSpan => _array.AsSpan(_writeOffset); // decrease ref-count; dispose if necessary [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Dispose() + public void Release() { if (Interlocked.Decrement(ref _refCount) <= 0) Recycle(); } + public bool TryAddRef() + { + int count; + do + { + count = Volatile.Read(ref _refCount); + if (count <= 0) return false; + } + // repeat until we can successfully swap/incr + while (Interlocked.CompareExchange(ref _refCount, count + 1, count) != count); + + return true; + } + [MethodImpl(MethodImplOptions.NoInlining)] // called rarely vs Dispose private void Recycle() { var count = Volatile.Read(ref _refCount); if (count == 0) { - _buffer.DebugScramble(); - _arrayPool.Return(_buffer); + _array.DebugScramble(); #if DEBUG - GC.SuppressFinalize(this); - _parent.DebugBufferRecycled(); + GC.SuppressFinalize(this); // only have a finalizer in debug + _parent.DebugBufferRecycled(_array.Length); #endif + _arrayPool.Return(_array); + _array = []; } Debug.Assert(count == 0, $"over-disposal? count={count}"); } #if DEBUG +#pragma warning disable CA2015 // Adding a finalizer to a type derived from MemoryManager may permit memory to be freed while it is still in use by a Span + // (the above is fine because we don't actually release anything - just a counter) ~BlockBuffer() { _parent.DebugBufferLeaked(); + DebugCounters.OnBufferLeaked(); } +#pragma warning restore CA2015 #endif public static BlockBuffer GetBuffer(BlockBufferSerializer parent, int sizeHint) @@ -93,7 +114,7 @@ private int AvailableWithResetIfUseful() _writeOffset = _finalizedOffset = 0; // swipe left } - return _buffer.Length - _writeOffset; + return _array.Length - _writeOffset; } private static BlockBuffer GetBufferSlow(BlockBufferSerializer parent, int minBytes) @@ -146,10 +167,10 @@ private void MarkComplete() // record that the old buffer no longer logically has any non-committed bytes (mostly just for ToString()) _writeOffset = _finalizedOffset; Debug.Assert(IsNonCommittedEmpty); - Dispose(); // decrement the observer - #if DEBUG + Release(); // decrement the observer +#if DEBUG DebugCounters.OnBufferCompleted(_finalizedCount, _finalizedOffset); - #endif +#endif } private void CopyFrom(Span source) @@ -158,7 +179,7 @@ private void CopyFrom(Span source) _writeOffset += source.Length; } - private Span NonFinalizedData => _buffer.AsSpan( + private Span NonFinalizedData => _array.AsSpan( _finalizedOffset, _writeOffset - _finalizedOffset); private bool TryResizeFor(int extraBytes) @@ -167,15 +188,15 @@ private bool TryResizeFor(int extraBytes) Volatile.Read(ref _refCount) == 1) // and no-one else is looking (we already tried reset) { // we're already on the boundary - don't scrimp; just do the math from the end of the buffer - byte[] newArray = _arrayPool.Rent(_buffer.Length + extraBytes); - DebugCounters.OnBufferCapacity(newArray.Length - _buffer.Length); // account for extra only + byte[] newArray = _arrayPool.Rent(_array.Length + extraBytes); + DebugCounters.OnBufferCapacity(newArray.Length - _array.Length); // account for extra only // copy the existing data (we always expect some, since we've clamped extraBytes to be // much smaller than the default buffer size) NonFinalizedData.CopyTo(newArray); - _buffer.DebugScramble(); - _arrayPool.Return(_buffer); - _buffer = newArray; + _array.DebugScramble(); + _arrayPool.Return(_array); + _array = newArray; return true; } @@ -200,24 +221,23 @@ public void RevertUnfinalized(BlockBufferSerializer parent) _finalizedOffset = _writeOffset; } - private ReadOnlyMemory Finalize(out IDisposable? block) + private ReadOnlyMemory FinalizeBlock() { var length = _writeOffset - _finalizedOffset; Debug.Assert(length > 0, "already checked this in FinalizeMessage!"); - ReadOnlyMemory chunk = new(_buffer, _finalizedOffset, length); + var chunk = CreateMemory(_finalizedOffset, length); _finalizedOffset = _writeOffset; // move the write head #if DEBUG _finalizedCount++; _parent.DebugMessageFinalized(length); #endif Interlocked.Increment(ref _refCount); // add an observer - block = this; return chunk; } private bool IsNonCommittedEmpty => _finalizedOffset == _writeOffset; - public static ReadOnlyMemory FinalizeMessage(BlockBufferSerializer parent, out IDisposable? block) + public static ReadOnlyMemory FinalizeMessage(BlockBufferSerializer parent) { var buffer = parent.Buffer; if (buffer is null || buffer.IsNonCommittedEmpty) @@ -226,18 +246,44 @@ public static ReadOnlyMemory FinalizeMessage(BlockBufferSerializer parent, if (buffer is not null) buffer._finalizedCount++; parent.DebugMessageFinalized(0); #endif - return DefaultFinalize(out block); + return default; } - return buffer.Finalize(out block); + return buffer.FinalizeBlock(); } - // very rare: means either no buffer *ever*, or we're finalizing an empty message (which isn't valid RESP!) - [MethodImpl(MethodImplOptions.NoInlining)] - private static ReadOnlyMemory DefaultFinalize(out IDisposable? block) + // MemoryManager pieces + protected override void Dispose(bool disposing) + { + if (disposing) Release(); + } + + public override Span GetSpan() => _array; + public int Length => _array.Length; + + // base version is CreateMemory(GetSpan().Length); avoid that GetSpan() + public override Memory Memory => CreateMemory(_array.Length); + + public override unsafe MemoryHandle Pin(int elementIndex = 0) + { + // We *could* be cute and use a shared pin - but that's a *lot* + // of work (synchronization), requires extra storage, and for an + // API that is very unlikely; hence: we'll use per-call GC pins. + GCHandle handle = GCHandle.Alloc(_array, GCHandleType.Pinned); + DebugCounters.OnBufferPinned(); // prove how unlikely this is + byte* ptr = (byte*)handle.AddrOfPinnedObject(); + // note no IPinnable in the MemoryHandle; + return new MemoryHandle(ptr + elementIndex, handle); + } + + // This would only be called if we passed out a MemoryHandle with ourselves + // as IPinnable (in Pin), which: we don't. + public override void Unpin() => throw new NotSupportedException(); + + protected override bool TryGetArray(out ArraySegment segment) { - block = null; - return default; + segment = new ArraySegment(_array); + return true; } } } diff --git a/src/RESPite/Internal/BlockBufferSerializer.cs b/src/RESPite/Internal/BlockBufferSerializer.cs index ba757982c..7216919d1 100644 --- a/src/RESPite/Internal/BlockBufferSerializer.cs +++ b/src/RESPite/Internal/BlockBufferSerializer.cs @@ -33,8 +33,7 @@ public virtual ReadOnlyMemory Serialize( RespCommandMap? commandMap, ReadOnlySpan command, in TRequest request, - IRespFormatter formatter, - out IDisposable? block) + IRespFormatter formatter) #if NET9_0_OR_GREATER where TRequest : allows ref struct #endif @@ -45,7 +44,7 @@ public virtual ReadOnlyMemory Serialize( writer.CommandMap = commandMap; formatter.Format(command, ref writer, request); writer.Flush(); - return BlockBuffer.FinalizeMessage(this, out block); + return BlockBuffer.FinalizeMessage(this); } catch { @@ -63,20 +62,24 @@ public virtual ReadOnlyMemory Serialize( public int CountMessages => Volatile.Read(ref _countMessages); public long CountMessageBytes => Volatile.Read(ref _countMessageBytes); + [Conditional("DEBUG")] private void DebugBufferLeaked() => Interlocked.Increment(ref _countLeaked); - private void DebugBufferRecycled() + [Conditional("DEBUG")] + private void DebugBufferRecycled(int length) { Interlocked.Increment(ref _countRecycled); - DebugCounters.OnBufferRecycled(); + DebugCounters.OnBufferRecycled(length); } + [Conditional("DEBUG")] private void DebugBufferCreated() { Interlocked.Increment(ref _countAdded); DebugCounters.OnBufferCreated(); } + [Conditional("DEBUG")] private void DebugMessageFinalized(int bytes) { Interlocked.Increment(ref _countMessages); diff --git a/src/RESPite/Internal/DebugCounters.cs b/src/RESPite/Internal/DebugCounters.cs index 82829a996..f95b595a8 100644 --- a/src/RESPite/Internal/DebugCounters.cs +++ b/src/RESPite/Internal/DebugCounters.cs @@ -26,13 +26,17 @@ internal partial class DebugCounters _tallyBatchWriteMessageCount, _tallyBufferCreatedCount, _tallyBufferRecycledCount, - _tallyBufferMessageCount; + _tallyBufferMessageCount, + _tallyBufferPinCount, + _tallyBufferLeakCount; private static long _tallyWriteBytes, _tallyReadBytes, _tallyCopyOutBytes, _tallyDiscardAverage, _tallyBufferMessageBytes, + _tallyBufferRecycledBytes, + _tallyBufferMaxOutstandingBytes, _tallyBufferTotalBytes; #endif @@ -160,10 +164,24 @@ public static void OnBufferCreated() } [Conditional("DEBUG")] - public static void OnBufferRecycled() + public static void OnBufferRecycled(int messageBytes) { #if DEBUG Interlocked.Increment(ref _tallyBufferRecycledCount); + var now = Interlocked.Add(ref _tallyBufferRecycledBytes, messageBytes); + var outstanding = Volatile.Read(ref _tallyBufferMessageBytes) - now; + + while (true) + { + var oldOutstanding = Volatile.Read(ref _tallyBufferMaxOutstandingBytes); + // loop until either it isn't an increase, or we successfully perform + // the swap + if (outstanding <= oldOutstanding + || Interlocked.CompareExchange( + ref _tallyBufferMaxOutstandingBytes, + outstanding, + oldOutstanding) == oldOutstanding) break; + } #endif } @@ -183,11 +201,31 @@ public static void OnBufferCapacity(int bytes) #endif } + public static void OnBufferPinned() + { +#if DEBUG + Interlocked.Increment(ref _tallyBufferPinCount); +#endif + } + + public static void OnBufferLeaked() + { +#if DEBUG + Interlocked.Increment(ref _tallyBufferLeakCount); +#endif + } + private DebugCounters() { } - public static DebugCounters Flush() => new(); + public static DebugCounters Flush() + { + #if DEBUG + BlockBufferSerializer.Shared.Clear(); // release any outstanding buffers + #endif + return new(); + } #if DEBUG private static void EstimatedMovingRangeAverage(ref long field, long value) @@ -223,8 +261,12 @@ private static void EstimatedMovingRangeAverage(ref long field, long value) public int BufferCreatedCount { get; } = Interlocked.Exchange(ref _tallyBufferCreatedCount, 0); public int BufferRecycledCount { get; } = Interlocked.Exchange(ref _tallyBufferRecycledCount, 0); + public long BufferRecycledBytes { get; } = Interlocked.Exchange(ref _tallyBufferRecycledBytes, 0); + public long BufferMaxOutstandingBytes { get; } = Interlocked.Exchange(ref _tallyBufferMaxOutstandingBytes, 0); public int BufferMessageCount { get; } = Interlocked.Exchange(ref _tallyBufferMessageCount, 0); public long BufferMessageBytes { get; } = Interlocked.Exchange(ref _tallyBufferMessageBytes, 0); public long BufferTotalBytes { get; } = Interlocked.Exchange(ref _tallyBufferTotalBytes, 0); + public int BufferPinCount { get; } = Interlocked.Exchange(ref _tallyBufferPinCount, 0); + public int BufferLeakCount { get; } = Interlocked.Exchange(ref _tallyBufferLeakCount, 0); #endif } diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs index bfa050c37..ae8b860e9 100644 --- a/src/RESPite/Internal/RespMessageBase.cs +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -15,7 +15,6 @@ internal abstract class RespMessageBase : IRespMessage, IValueTaskSou private CancellationTokenRegistration _cancellationTokenRegistration; private ReadOnlyMemory _request; - private object? _requestOwner; private int _requestRefCount; private int _flags; @@ -151,38 +150,12 @@ public RespMessageBase Init(bool sent, CancellationToken cancellation return this; } - public RespMessageBase Init( - byte[]? oversized, - int offset, - int length, - ArrayPool? pool, - CancellationToken cancellationToken) - { - Debug.Assert(_flags == 0, "flags should be zero"); - Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); - if (oversized is not null) - { - _requestOwner = pool; - _request = new ReadOnlyMemory(oversized, offset, length); - _requestRefCount = 1; - } - - if (cancellationToken.CanBeCanceled) - { - _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellationToken); - } - - return this; - } - public RespMessageBase Init( ReadOnlyMemory request, - IDisposable? owner, CancellationToken cancellationToken) { Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); _request = request; - _requestOwner = owner; _requestRefCount = 1; if (cancellationToken.CanBeCanceled) { @@ -207,7 +180,6 @@ public virtual void Reset(bool recycle) // note we only reset on success, and on // success we've already unregistered cancellation _request = default; - _requestOwner = null; _requestRefCount = 0; _flags = 0; _asyncCore.Reset(); @@ -259,19 +231,11 @@ private bool if (oldCount == 1) // we were the last one; recycle { _request.DebugScramble(); - if (_requestOwner is IDisposable owner) - { - owner.Dispose(); - } - else if (_requestOwner is ArrayPool pool) + if (MemoryMarshal.TryGetMemoryManager(_request, out var block)) { - if (MemoryMarshal.TryGetArray(_request, out var segment)) - { - pool.Return(segment.Array!); - } + block.Release(); } _request = default; - _requestOwner = null; } return true; diff --git a/src/RESPite/Internal/RespOperationExtensions.cs b/src/RESPite/Internal/RespOperationExtensions.cs index 1d0d0887c..242f42713 100644 --- a/src/RESPite/Internal/RespOperationExtensions.cs +++ b/src/RESPite/Internal/RespOperationExtensions.cs @@ -33,6 +33,9 @@ internal static void DebugScramble(this ReadOnlyMemory value) => MemoryMarshal.AsMemory(value).Span.Fill(42); [Conditional("DEBUG")] - internal static void DebugScramble(this byte[] value) - => value.AsSpan().Fill(42); + internal static void DebugScramble(this byte[]? value) + { + if (value is not null) + value.AsSpan().Fill(42); + } } diff --git a/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs b/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs index caeeefc26..1ce6982aa 100644 --- a/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs +++ b/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs @@ -18,8 +18,7 @@ public override ReadOnlyMemory Serialize( RespCommandMap? commandMap, ReadOnlySpan command, in TRequest request, - IRespFormatter formatter, - out IDisposable? block) + IRespFormatter formatter) { bool haveLock = false; try // note that "lock" unrolls to something very similar; we're not adding anything unusual here @@ -29,7 +28,7 @@ public override ReadOnlyMemory Serialize( // add a timeout - just to avoid surprises (since people can write their own formatters) Monitor.TryEnter(this, LockTimeout, ref haveLock); if (!haveLock) ThrowTimeout(); - return base.Serialize(commandMap, command, in request, formatter, out block); + return base.Serialize(commandMap, command, in request, formatter); } finally { diff --git a/src/RESPite/RespContextExtensions.cs b/src/RESPite/RespContextExtensions.cs index e977c5f60..37c8a5fab 100644 --- a/src/RESPite/RespContextExtensions.cs +++ b/src/RESPite/RespContextExtensions.cs @@ -196,8 +196,8 @@ public static RespOperation CreateOperation( { var conn = context.Connection; var memory = - conn.Serializer.Serialize(conn.NonDefaultCommandMap, command, request, formatter, out var block); - var msg = RespMessage.Get(parser).Init(memory, block, context.CancellationToken); + conn.Serializer.Serialize(conn.NonDefaultCommandMap, command, request, formatter); + var msg = RespMessage.Get(parser).Init(memory, context.CancellationToken); return new(msg); } @@ -213,8 +213,8 @@ public static RespOperation CreateOperation( { var conn = context.Connection; var memory = - conn.Serializer.Serialize(conn.NonDefaultCommandMap, command, request, formatter, out var block); - var msg = RespMessage.Get(parser).Init(memory, block, context.CancellationToken); + conn.Serializer.Serialize(conn.NonDefaultCommandMap, command, request, formatter); + var msg = RespMessage.Get(parser).Init(memory, context.CancellationToken); return new(msg); } @@ -230,10 +230,9 @@ public static RespOperation CreateOperation.Get(in state, parser) - .Init(memory, block, context.CancellationToken); + .Init(memory, context.CancellationToken); return new(msg); } } diff --git a/tests/RESP.Core.Tests/BlockBufferTests.cs b/tests/RESP.Core.Tests/BlockBufferTests.cs index 1bef30319..6cf09d701 100644 --- a/tests/RESP.Core.Tests/BlockBufferTests.cs +++ b/tests/RESP.Core.Tests/BlockBufferTests.cs @@ -29,9 +29,9 @@ private void Log(ReadOnlySpan span) public void CanCreateAndWriteSimpleBuffer() { var buffer = BlockBufferSerializer.Create(); - var a = buffer.Serialize(null, "get"u8, "abc", RespFormatters.Key.String, out var blockA); - var b = buffer.Serialize(null, "get"u8, "def", RespFormatters.Key.String, out var blockB); - var c = buffer.Serialize(null, "get"u8, "ghi", RespFormatters.Key.String, out var blockC); + var a = buffer.Serialize(null, "get"u8, "abc", RespFormatters.Key.String); + var b = buffer.Serialize(null, "get"u8, "def", RespFormatters.Key.String); + var c = buffer.Serialize(null, "get"u8, "ghi", RespFormatters.Key.String); buffer.Clear(); #if DEBUG Assert.Equal(1, buffer.CountAdded); @@ -47,19 +47,25 @@ public void CanCreateAndWriteSimpleBuffer() Assert.True(b.Span.SequenceEqual("*2\r\n$3\r\nget\r\n$3\r\ndef\r\n"u8)); Log(c.Span); Assert.True(c.Span.SequenceEqual("*2\r\n$3\r\nget\r\n$3\r\nghi\r\n"u8)); - blockA?.Dispose(); - blockB?.Dispose(); + AssertRelease(a); + AssertRelease(b); #if DEBUG Assert.Equal(0, buffer.CountRecycled); Assert.Equal(0, buffer.CountLeaked); #endif - blockC?.Dispose(); + AssertRelease(c); #if DEBUG Assert.Equal(1, buffer.CountRecycled); Assert.Equal(0, buffer.CountLeaked); #endif } + private static void AssertRelease(ReadOnlyMemory buffer) + { + Assert.True(MemoryMarshal.TryGetMemoryManager(buffer, out var manager)); + manager.Release(); + } + [Fact] public void CanWriteLotsOfBuffers_WithCheapReset() // when messages are consumed before more are added { @@ -72,12 +78,9 @@ public void CanWriteLotsOfBuffers_WithCheapReset() // when messages are consumed #endif for (int i = 0; i < 5000; i++) { - var a = buffer.Serialize(null, "get"u8, "abc", RespFormatters.Key.String, out var blockA); - var b = buffer.Serialize(null, "get"u8, "def", RespFormatters.Key.String, out var blockB); - var c = buffer.Serialize(null, "get"u8, "ghi", RespFormatters.Key.String, out var blockC); - blockA?.Dispose(); - blockB?.Dispose(); - blockC?.Dispose(); + var a = buffer.Serialize(null, "get"u8, "abc", RespFormatters.Key.String); + var b = buffer.Serialize(null, "get"u8, "def", RespFormatters.Key.String); + var c = buffer.Serialize(null, "get"u8, "ghi", RespFormatters.Key.String); Assert.True(MemoryMarshal.TryGetArray(a, out var aSegment)); Assert.True(MemoryMarshal.TryGetArray(b, out var bSegment)); Assert.True(MemoryMarshal.TryGetArray(c, out var cSegment)); @@ -89,6 +92,9 @@ public void CanWriteLotsOfBuffers_WithCheapReset() // when messages are consumed Assert.Equal(22, cSegment.Count); Assert.Same(aSegment.Array, bSegment.Array); Assert.Same(aSegment.Array, cSegment.Array); + AssertRelease(a); + AssertRelease(b); + AssertRelease(c); } #if DEBUG Assert.Equal(1, buffer.CountAdded); @@ -109,7 +115,7 @@ public void CanWriteLotsOfBuffers_WithCheapReset() // when messages are consumed public void CanWriteLotsOfBuffers() { var buffer = BlockBufferSerializer.Create(); - List blocks = new(15_000); + List> blocks = new(15_000); #if DEBUG Assert.Equal(0, buffer.CountAdded); Assert.Equal(0, buffer.CountRecycled); @@ -118,12 +124,12 @@ public void CanWriteLotsOfBuffers() #endif for (int i = 0; i < 5000; i++) { - _ = buffer.Serialize(null, "get"u8, "abc", RespFormatters.Key.String, out var block); - if (block is not null) blocks.Add(block); - _ = buffer.Serialize(null, "get"u8, "def", RespFormatters.Key.String, out block); - if (block is not null) blocks.Add(block); - _ = buffer.Serialize(null, "get"u8, "ghi", RespFormatters.Key.String, out block); - if (block is not null) blocks.Add(block); + var block = buffer.Serialize(null, "get"u8, "abc", RespFormatters.Key.String); + blocks.Add(block); + block = buffer.Serialize(null, "get"u8, "def", RespFormatters.Key.String); + blocks.Add(block); + block = buffer.Serialize(null, "get"u8, "ghi", RespFormatters.Key.String); + blocks.Add(block); } // Each buffer is 2048 by default, so: 93 per buffer; at least 162 buffers (looking at CountAdded). @@ -144,7 +150,7 @@ public void CanWriteLotsOfBuffers() Assert.Equal(0, buffer.CountLeaked); #endif - foreach (var block in blocks) block.Dispose(); + foreach (var block in blocks) AssertRelease(block); #if DEBUG Assert.Equal(15_000, buffer.CountMessages); Assert.True(buffer.CountAdded < 200, "too many buffers used"); From e276735c78dbfd36c9d073f24c6fb74fa89995fa Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 2 Sep 2025 11:10:40 +0100 Subject: [PATCH 028/108] enable multi-message payloads (for batching) --- src/RESPite.Benchmark/BenchmarkBase.cs | 2 +- .../Internal/BasicBatchConnection.cs | 2 +- .../Connections/Internal/NullConnection.cs | 2 +- .../Internal/SynchronizedConnection.cs | 2 +- src/RESPite/Internal/ActivationHelper.cs | 15 +- src/RESPite/Internal/DebugCounters.cs | 8 +- src/RESPite/Internal/IRespMessage.cs | 53 +-- src/RESPite/Internal/RespMessageBase.cs | 365 +++++++++++------- .../Internal/RespOperationExtensions.cs | 19 +- src/RESPite/Internal/StreamConnection.cs | 164 +++++--- src/RESPite/RespContextExtensions.cs | 10 +- src/RESPite/RespOperation.cs | 33 +- src/RESPite/RespOperationT.cs | 29 +- 13 files changed, 441 insertions(+), 263 deletions(-) diff --git a/src/RESPite.Benchmark/BenchmarkBase.cs b/src/RESPite.Benchmark/BenchmarkBase.cs index 7833b6828..673ae91b8 100644 --- a/src/RESPite.Benchmark/BenchmarkBase.cs +++ b/src/RESPite.Benchmark/BenchmarkBase.cs @@ -453,7 +453,7 @@ protected async Task RunAsync( if (counters.WriteBytes != 0) { Console.Write($"Write: {FormatBytes(counters.WriteBytes)}"); - if (counters.WriteCount != 0) Console.Write($"; {counters.WriteCount:#,##0} sync"); + if (counters.SyncWriteCount != 0) Console.Write($"; {counters.SyncWriteCount:#,##0} sync"); if (counters.AsyncWriteInlineCount != 0) Console.Write($"; {counters.AsyncWriteInlineCount:#,##0} async-inline"); if (counters.AsyncWriteCount != 0) Console.Write($"; {counters.AsyncWriteCount:#,##0} full-async"); diff --git a/src/RESPite/Connections/Internal/BasicBatchConnection.cs b/src/RESPite/Connections/Internal/BasicBatchConnection.cs index c571e0fd6..6b0411dd0 100644 --- a/src/RESPite/Connections/Internal/BasicBatchConnection.cs +++ b/src/RESPite/Connections/Internal/BasicBatchConnection.cs @@ -43,7 +43,7 @@ items will be added */ #else foreach (var message in _unsent) { - message.Message.TrySetException(message.Token, CreateObjectDisposedException()); + message.TrySetException(CreateObjectDisposedException()); } #endif _unsent.Clear(); diff --git a/src/RESPite/Connections/Internal/NullConnection.cs b/src/RESPite/Connections/Internal/NullConnection.cs index 79cd5954f..139e8cb82 100644 --- a/src/RESPite/Connections/Internal/NullConnection.cs +++ b/src/RESPite/Connections/Internal/NullConnection.cs @@ -18,7 +18,7 @@ private NullConnection(RespConfiguration configuration) : base(configuration) private const string SendErrorMessage = "Null connections do not support sending messages."; public override void Write(in RespOperation message) { - message.Message.TrySetException(message.Token, new NotSupportedException(SendErrorMessage)); + message.TrySetException(new NotSupportedException(SendErrorMessage)); } public override Task WriteAsync(in RespOperation message) diff --git a/src/RESPite/Connections/Internal/SynchronizedConnection.cs b/src/RESPite/Connections/Internal/SynchronizedConnection.cs index 644aeac1c..b7e7cf53b 100644 --- a/src/RESPite/Connections/Internal/SynchronizedConnection.cs +++ b/src/RESPite/Connections/Internal/SynchronizedConnection.cs @@ -31,7 +31,7 @@ public override void Write(in RespOperation message) } catch (Exception ex) { - message.Message.TrySetException(message.Token, ex); + message.TrySetException(ex); throw; } finally diff --git a/src/RESPite/Internal/ActivationHelper.cs b/src/RESPite/Internal/ActivationHelper.cs index 15b07c7b2..8abd9bac4 100644 --- a/src/RESPite/Internal/ActivationHelper.cs +++ b/src/RESPite/Internal/ActivationHelper.cs @@ -1,6 +1,5 @@ using System.Buffers; using System.Diagnostics; -using System.Runtime.CompilerServices; namespace RESPite.Internal; @@ -14,7 +13,7 @@ private sealed class WorkItem private WorkItem() { #if NET5_0_OR_GREATER - Unsafe.SkipInit(out _payload); + System.Runtime.CompilerServices.Unsafe.SkipInit(out _payload); #else _payload = []; #endif @@ -69,16 +68,18 @@ public void Execute() _payload = []; _length = 0; Interlocked.Exchange(ref _spare, this); - message.Message.TrySetResult(message.Token, new ReadOnlySpan(payload, 0, length)); + var msg = message; + msg.Message.TrySetResult(msg.Token, new ReadOnlySpan(payload, 0, length)); ArrayPool.Shared.Return(payload); } } public static void ProcessResponse(in RespOperation pending, ReadOnlySpan payload, ref byte[]? lease) { - if (pending.Message.AllowInlineParsing) + var msg = pending.Message; + if (msg.AllowInlineParsing) { - pending.Message.TrySetResult(pending.Token, payload); + msg.TrySetResult(pending.Token, payload); } else { @@ -87,10 +88,10 @@ public static void ProcessResponse(in RespOperation pending, ReadOnlySpan } private static readonly Action CancellationCallback = static state - => ((IRespMessage)state!).TrySetCanceled(); + => ((RespMessageBase)state!).TrySetCanceledTrustToken(); public static CancellationTokenRegistration RegisterForCancellation( - IRespMessage message, + RespMessageBase message, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/RESPite/Internal/DebugCounters.cs b/src/RESPite/Internal/DebugCounters.cs index f95b595a8..4e6ff652a 100644 --- a/src/RESPite/Internal/DebugCounters.cs +++ b/src/RESPite/Internal/DebugCounters.cs @@ -11,7 +11,7 @@ internal partial class DebugCounters private static int _tallyReadCount, _tallyAsyncReadCount, _tallyAsyncReadInlineCount, - _tallyWriteCount, + _tallySyncWriteCount, _tallyAsyncWriteCount, _tallyAsyncWriteInlineCount, _tallyCopyOutCount, @@ -81,10 +81,10 @@ internal static void OnAsyncRead(int bytes, bool inline) } [Conditional("DEBUG")] - internal static void OnWrite(int bytes) + internal static void OnSyncWrite(int bytes) { #if DEBUG - Interlocked.Increment(ref _tallyWriteCount); + Interlocked.Increment(ref _tallySyncWriteCount); if (bytes > 0) Interlocked.Add(ref _tallyWriteBytes, bytes); #endif } @@ -242,7 +242,7 @@ private static void EstimatedMovingRangeAverage(ref long field, long value) public int AsyncReadInlineCount { get; } = Interlocked.Exchange(ref _tallyAsyncReadInlineCount, 0); public long ReadBytes { get; } = Interlocked.Exchange(ref _tallyReadBytes, 0); - public int WriteCount { get; } = Interlocked.Exchange(ref _tallyWriteCount, 0); + public int SyncWriteCount { get; } = Interlocked.Exchange(ref _tallySyncWriteCount, 0); public int AsyncWriteCount { get; } = Interlocked.Exchange(ref _tallyAsyncWriteCount, 0); public int AsyncWriteInlineCount { get; } = Interlocked.Exchange(ref _tallyAsyncWriteInlineCount, 0); public long WriteBytes { get; } = Interlocked.Exchange(ref _tallyWriteBytes, 0); diff --git a/src/RESPite/Internal/IRespMessage.cs b/src/RESPite/Internal/IRespMessage.cs index da5342358..edc6484ee 100644 --- a/src/RESPite/Internal/IRespMessage.cs +++ b/src/RESPite/Internal/IRespMessage.cs @@ -1,26 +1,27 @@ -using System.Buffers; -using System.Threading.Tasks.Sources; - -namespace RESPite.Internal; - -internal interface IRespMessage : IValueTaskSource -{ - void Wait(short token, TimeSpan timeout); - void TrySetCanceled(); // only intended for use from cancellation callbacks - bool TrySetCanceled(short token, CancellationToken cancellationToken = default); - bool TrySetException(short token, Exception exception); - bool TrySetResult(short token, scoped ReadOnlySpan response); - bool TrySetResult(short token, in ReadOnlySequence response); - bool TryReserveRequest(short token, out ReadOnlyMemory payload, bool recordSent = true); - void ReleaseRequest(); - bool AllowInlineParsing { get; } - short Token { get; } - ref readonly CancellationToken CancellationToken { get; } - bool IsSent(short token); - - void OnCompletedWithNotSentDetection( - Action continuation, - object? state, - short token, - ValueTaskSourceOnCompletedFlags flags); -} +// using System.Buffers; +// using System.Threading.Tasks.Sources; +// +// namespace RESPite.Internal; +// +// internal interface IRespMessage : IValueTaskSource +// { +// void Wait(short token, TimeSpan timeout); +// void TrySetCanceled(); // only intended for use from cancellation callbacks +// bool TrySetCanceled(short token, CancellationToken cancellationToken = default); +// bool TrySetException(short token, Exception exception); +// bool TrySetResult(short token, scoped ReadOnlySpan response); +// bool TrySetResult(short token, in ReadOnlySequence response); +// bool TryReserveRequest(short token, out ReadOnlySequence payload, bool recordSent = true); +// void ReleaseRequest(); +// bool AllowInlineParsing { get; } +// short Token { get; } +// ref readonly CancellationToken CancellationToken { get; } +// int MessageCount { get; } +// bool IsSent(short token); +// +// void OnCompletedWithNotSentDetection( +// Action continuation, +// object? state, +// short token, +// ValueTaskSourceOnCompletedFlags flags); +// } diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs index ae8b860e9..02eadccbb 100644 --- a/src/RESPite/Internal/RespMessageBase.cs +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -7,21 +7,17 @@ namespace RESPite.Internal; -internal abstract class RespMessageBase : IRespMessage, IValueTaskSource +internal abstract class RespMessageBase : IValueTaskSource { protected RespMessageBase() => RespOperation.DebugOnAllocateMessage(); private CancellationToken _cancellationToken; private CancellationTokenRegistration _cancellationTokenRegistration; - - private ReadOnlyMemory _request; - - private int _requestRefCount; - private int _flags; - private ManualResetValueTaskSourceCore _asyncCore; + private int _requestRefCount, _flags, _messageCount; + private ReadOnlySequence _request; public ref readonly CancellationToken CancellationToken => ref _cancellationToken; - private const int + protected const int Flag_Sent = 1 << 0, // the request has been sent Flag_OutcomeKnown = 1 << 1, // controls which code flow gets to set an outcome Flag_Complete = 1 << 2, // indicates whether all follow-up has completed @@ -29,9 +25,10 @@ private const int Flag_Parser = 1 << 5, // we have a parser Flag_MetadataParser = 1 << 6, // the parser wants to consume metadata Flag_InlineParser = 1 << 7, // we can safely use the parser on the IO thread - Flag_Doomed = 1 << 8; // something went wrong, do not recyle + Flag_Doomed = 1 << 8; // something went wrong, do not recycle - protected abstract TResponse Parse(ref RespReader reader); + protected int Flags => Volatile.Read(ref _flags); + public int MessageCount => _messageCount; protected void InitParser(object? parser) { @@ -54,7 +51,7 @@ protected void InitParser(object? parser) public bool TrySetResult(short token, scoped ReadOnlySpan response) { - if (HasFlag(Flag_OutcomeKnown) | _asyncCore.Version != token) return false; + if (HasFlag(Flag_OutcomeKnown) | Token != token) return false; var flags = _flags & (Flag_MetadataParser | Flag_Parser); switch (flags) { @@ -68,28 +65,20 @@ public bool TrySetResult(short token, scoped ReadOnlySpan response) reader.MoveNext(); } - return TrySetResultPrecheckedToken(Parse(ref reader)); + return TrySetResultPrecheckedToken(ref reader); } catch (Exception ex) { return TrySetExceptionPrecheckedToken(ex); } default: - return TrySetResultPrecheckedToken(default(TResponse)!); + return TrySetDefaultResultPrecheckedToken(); } } - public short Token => _asyncCore.Version; - - public bool IsSent(short token) - { - CheckToken(token); - return HasFlag(Flag_Sent); - } - public bool TrySetResult(short token, in ReadOnlySequence response) { - if (HasFlag(Flag_OutcomeKnown) | _asyncCore.Version != token) return false; + if (HasFlag(Flag_OutcomeKnown) | Token != token) return false; var flags = _flags & (Flag_MetadataParser | Flag_Parser); switch (flags) { @@ -103,18 +92,35 @@ public bool TrySetResult(short token, in ReadOnlySequence response) reader.MoveNext(); } - return TrySetResultPrecheckedToken(Parse(ref reader)); + return TrySetResultPrecheckedToken(ref reader); } catch (Exception ex) { return TrySetExceptionPrecheckedToken(ex); } default: - return TrySetResultPrecheckedToken(default(TResponse)!); + return TrySetDefaultResultPrecheckedToken(); } } - private bool SetFlag(int flag) + protected abstract bool TrySetResultPrecheckedToken(ref RespReader reader); + protected abstract bool TrySetDefaultResultPrecheckedToken(); + + public abstract short Token { get; } + + private protected abstract void CheckToken(short token); + + private protected abstract ValueTaskSourceStatus OwnStatus { get; } + + public abstract ValueTaskSourceStatus GetStatus(short token); + + public bool IsSent(short token) + { + CheckToken(token); + return HasFlag(Flag_Sent); + } + + protected bool SetFlag(int flag) { Debug.Assert(flag != 0, "trying to set a zero flag"); #if NET5_0_OR_GREATER @@ -134,9 +140,9 @@ private bool SetFlag(int flag) } // in the "any" sense - private bool HasFlag(int flag) => (Volatile.Read(ref _flags) & flag) != 0; + protected bool HasFlag(int flag) => (Volatile.Read(ref _flags) & flag) != 0; - public RespMessageBase Init(bool sent, CancellationToken cancellationToken) + public void Init(bool sent, CancellationToken cancellationToken) { Debug.Assert(_flags == 0, "flags should be zero"); Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); @@ -146,26 +152,38 @@ public RespMessageBase Init(bool sent, CancellationToken cancellation _cancellationToken = cancellationToken; _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellationToken); } - - return this; } - public RespMessageBase Init( + public void Init( ReadOnlyMemory request, CancellationToken cancellationToken) { Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); - _request = request; + _request = new(request); _requestRefCount = 1; + _messageCount = 1; if (cancellationToken.CanBeCanceled) { _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellationToken); } + } - return this; + public void Init( + ReadOnlySequence request, + int messageCount, + CancellationToken cancellationToken) + { + Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); + _request = request; + _requestRefCount = 1; + _messageCount = messageCount; + if (cancellationToken.CanBeCanceled) + { + _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellationToken); + } } - private void UnregisterCancellation() + protected void UnregisterCancellation() { _cancellationTokenRegistration.Dispose(); _cancellationTokenRegistration = default; @@ -175,27 +193,28 @@ private void UnregisterCancellation() public virtual void Reset(bool recycle) { Debug.Assert( - !recycle || _asyncCore.GetStatus(_asyncCore.Version) == ValueTaskSourceStatus.Succeeded, + !recycle || OwnStatus == ValueTaskSourceStatus.Succeeded, "We should only be recycling completed messages"); // note we only reset on success, and on // success we've already unregistered cancellation _request = default; _requestRefCount = 0; _flags = 0; - _asyncCore.Reset(); + NextToken(); if (recycle) Recycle(); } protected abstract void Recycle(); + protected abstract void NextToken(); - public bool TryReserveRequest(short token, out ReadOnlyMemory payload, bool recordSent = true) + public bool TryReserveRequest(short token, out ReadOnlySequence payload, bool recordSent = true) { while (true) // redo in case of CEX failure { - Debug.Assert(_asyncCore.GetStatus(_asyncCore.Version) == ValueTaskSourceStatus.Pending); + Debug.Assert(OwnStatus == ValueTaskSourceStatus.Pending); var oldCount = Volatile.Read(ref _requestRefCount); - if (oldCount == 0 | token != _asyncCore.Version) + if (oldCount == 0 | token != Token) { payload = default; return false; @@ -219,8 +238,7 @@ static void ThrowReleased() => throw new InvalidOperationException("The request payload has already been released"); } - private bool - TryReleaseRequest() // bool here means "it wasn't already zero"; it doesn't mean "it became zero" + private bool TryReleaseRequest() // bool here means "it wasn't already zero"; it doesn't mean "it became zero" { while (true) { @@ -230,12 +248,7 @@ private bool { if (oldCount == 1) // we were the last one; recycle { - _request.DebugScramble(); - if (MemoryMarshal.TryGetMemoryManager(_request, out var block)) - { - block.Release(); - } - _request = default; + Release(ref _request); } return true; @@ -243,13 +256,39 @@ private bool } } - /* asking about the status too early is usually a very bad sign that they're doing - something like awaiting a message in a transaction that hasn't been sent */ - public ValueTaskSourceStatus GetStatus(short token) - => _asyncCore.GetStatus(token); + private static void Release(ref ReadOnlySequence request) + { + if (request.IsSingleSegment) + { + if (MemoryMarshal.TryGetMemoryManager( + request.First, out var block)) + { + block.Release(); + } + } + else + { + ReleaseMultiBlock(ref request); + } + + request = default; + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ReleaseMultiBlock(ref ReadOnlySequence request) + { + foreach (var segment in request) + { + if (MemoryMarshal.TryGetMemoryManager( + segment, out var block)) + { + block.Release(); + } + } + } + } [MethodImpl(MethodImplOptions.NoInlining)] - private void ThrowNotSent(short token) + protected void ThrowNotSent(short token) { CheckToken(token); // prefer a token explanation throw new InvalidOperationException( @@ -257,14 +296,130 @@ private void ThrowNotSent(short token) } [MethodImpl(MethodImplOptions.NoInlining)] - private void SetNotSentAsync(short token) + protected void SetNotSentAsync(short token) { CheckToken(token); TrySetExceptionPrecheckedToken(new InvalidOperationException( "This command has not yet been sent; awaiting is not possible. If this is a transaction or batch, you must execute that first.")); } - private void CheckToken(short token) + // spoof untyped on top of typed + void IValueTaskSource.GetResult(short token) => GetResultVoid(token); + + private bool TrySetOutcomeKnown(short token, bool withSuccess) + => Token == token && TrySetOutcomeKnownPrecheckedToken(withSuccess); + + protected bool TrySetOutcomeKnownPrecheckedToken(bool withSuccess) + { + if (!SetFlag(Flag_OutcomeKnown)) return false; + UnregisterCancellation(); + TryReleaseRequest(); // we won't be needing this again + + // configure threading model; failure can be triggered from any thread - *always* + // dispatch to pool; in the success case, we're either on the IO thread + // (if inline-parsing is enabled) - in which case, yes: dispatch - or we've + // already jumped to a pool thread for the parse step. So: the only + // time we want to complete inline is success and not inline-parsing. + SetRunContinuationsAsynchronously(!withSuccess | AllowInlineParsing); + + return true; + } + + private protected abstract void SetRunContinuationsAsynchronously(bool value); + public abstract void GetResultVoid(short token); + public abstract void WaitVoid(short token, TimeSpan timeout); + + public bool TrySetCanceled(short token, CancellationToken cancellationToken = default) + { + if (!cancellationToken.IsCancellationRequested) + { + // use our own token if nothing more specific supplied + cancellationToken = _cancellationToken; + } + + return token == Token && TrySetCanceledPrecheckedToken(cancellationToken); + } + + // this is the path used by cancellation registration callbacks; always use our own + // cancellation token, and we must trust the version token + internal void TrySetCanceledTrustToken() => TrySetCanceledPrecheckedToken(_cancellationToken); + + private bool TrySetCanceledPrecheckedToken(CancellationToken cancellationToken) + { + if (!TrySetOutcomeKnownPrecheckedToken(false)) return false; + SetExceptionPreChecked(new OperationCanceledException(cancellationToken)); + SetFullyComplete(success: false); + return true; + } + + public bool TrySetException(short token, Exception exception) + => token == Token && TrySetExceptionPrecheckedToken(exception); + + private protected abstract void SetExceptionPreChecked(Exception exception); + + private bool TrySetExceptionPrecheckedToken(Exception exception) + { + if (!TrySetOutcomeKnownPrecheckedToken(false)) return false; // first winner only + SetExceptionPreChecked(exception); + SetFullyComplete(success: false); + return true; + } + + protected void SetFullyComplete(bool success) + { + var pulse = !HasFlag(Flag_NoPulse); + SetFlag(success + ? (Flag_Complete | Flag_NoPulse) + : (Flag_Complete | Flag_NoPulse | Flag_Doomed)); + + // for safety, always take the lock unless we know they've actively exited + if (pulse) + { + lock (this) + { + Monitor.PulseAll(this); + } + } + } + + protected bool TrySetTimeoutPrecheckedToken() + { + if (!TrySetOutcomeKnownPrecheckedToken(false)) return false; + + SetExceptionPreChecked(new TimeoutException()); + SetFullyComplete(success: false); + return true; + } + + public abstract void OnCompleted( + Action continuation, + object? state, + short token, + ValueTaskSourceOnCompletedFlags flags); + + public abstract void OnCompletedWithNotSentDetection( + Action continuation, + object? state, + short token, + ValueTaskSourceOnCompletedFlags flags); +} + +internal abstract class RespMessageBase : RespMessageBase, IValueTaskSource +{ + private ManualResetValueTaskSourceCore _asyncCore; + + protected abstract TResponse Parse(ref RespReader reader); + + public override short Token => _asyncCore.Version; + + private protected override ValueTaskSourceStatus OwnStatus => _asyncCore.GetStatus(_asyncCore.Version); + + /* asking about the status too early is usually a very bad sign that they're doing + something like awaiting a message in a transaction that hasn't been sent */ + public override ValueTaskSourceStatus GetStatus(short token) + => _asyncCore.GetStatus(token); + + private protected override void CheckToken(short token) { if (token != _asyncCore.Version) // use cheap test { @@ -275,7 +430,7 @@ private void CheckToken(short token) // this is used from Task/ValueTask; we can't avoid that - in theory // we *coiuld* sort of make it work for ValueTask, but if anyone // calls .AsTask() on it, it would fail - public void OnCompleted( + public override void OnCompleted( Action continuation, object? state, short token, @@ -286,7 +441,7 @@ public void OnCompleted( _asyncCore.OnCompleted(continuation, state, token, flags); } - public void OnCompletedWithNotSentDetection( + public override void OnCompletedWithNotSentDetection( Action continuation, object? state, short token, @@ -298,32 +453,15 @@ public void OnCompletedWithNotSentDetection( _asyncCore.OnCompleted(continuation, state, token, flags); } - // spoof untyped on top of typed - void IValueTaskSource.GetResult(short token) => _ = GetResult(token); - void IRespMessage.Wait(short token, TimeSpan timeout) => _ = Wait(token, timeout); + private protected override void SetRunContinuationsAsynchronously(bool value) + => _asyncCore.RunContinuationsAsynchronously = value; - private bool TrySetOutcomeKnown(short token, bool withSuccess) - => _asyncCore.Version == token && TrySetOutcomeKnownPrecheckedToken(withSuccess); - - private bool TrySetOutcomeKnownPrecheckedToken(bool withSuccess) - { - if (!SetFlag(Flag_OutcomeKnown)) return false; - UnregisterCancellation(); - TryReleaseRequest(); // we won't be needing this again - - // configure threading model; failure can be triggered from any thread - *always* - // dispatch to pool; in the success case, we're either on the IO thread - // (if inline-parsing is enabled) - in which case, yes: dispatch - or we've - // already jumped to a pool thread for the parse step. So: the only - // time we want to complete inline is success and not inline-parsing. - _asyncCore.RunContinuationsAsynchronously = !withSuccess | AllowInlineParsing; - - return true; - } + public override void GetResultVoid(short token) => _ = GetResult(token); + public override void WaitVoid(short token, TimeSpan timeout) => _ = Wait(token, timeout); public TResponse Wait(short token, TimeSpan timeout) { - switch (Volatile.Read(ref _flags) & (Flag_Complete | Flag_Sent)) + switch (Flags & (Flag_Complete | Flag_Sent)) { case Flag_Sent: // this is the normal case break; @@ -338,7 +476,7 @@ public TResponse Wait(short token, TimeSpan timeout) CheckToken(token); lock (this) { - switch (Volatile.Read(ref _flags) & (Flag_Complete | Flag_NoPulse)) + switch (Flags & (Flag_Complete | Flag_NoPulse)) { case Flag_NoPulse | Flag_Complete: case Flag_Complete: @@ -380,66 +518,6 @@ private bool TrySetResultPrecheckedToken(TResponse response) return true; } - private bool TrySetTimeoutPrecheckedToken() - { - if (!TrySetOutcomeKnownPrecheckedToken(false)) return false; - - _asyncCore.SetException(new TimeoutException()); - SetFullyComplete(success: false); - return true; - } - - public bool TrySetCanceled(short token, CancellationToken cancellationToken = default) - { - if (!cancellationToken.IsCancellationRequested) - { - // use our own token if nothing more specific supplied - cancellationToken = _cancellationToken; - } - - return token == _asyncCore.Version && TrySetCanceledPrecheckedToken(cancellationToken); - } - - // this is the path used by cancellation registration callbacks; always use our own - // cancellation token, and we must trust the version token - void IRespMessage.TrySetCanceled() => TrySetCanceledPrecheckedToken(_cancellationToken); - - private bool TrySetCanceledPrecheckedToken(CancellationToken cancellationToken) - { - if (!TrySetOutcomeKnownPrecheckedToken(false)) return false; - _asyncCore.SetException(new OperationCanceledException(cancellationToken)); - SetFullyComplete(success: false); - return true; - } - - public bool TrySetException(short token, Exception exception) - => token == _asyncCore.Version && TrySetExceptionPrecheckedToken(exception); - - private bool TrySetExceptionPrecheckedToken(Exception exception) - { - if (!TrySetOutcomeKnownPrecheckedToken(false)) return false; // first winner only - _asyncCore.SetException(exception); - SetFullyComplete(success: false); - return true; - } - - private void SetFullyComplete(bool success) - { - var pulse = !HasFlag(Flag_NoPulse); - SetFlag(success - ? (Flag_Complete | Flag_NoPulse) - : (Flag_Complete | Flag_NoPulse | Flag_Doomed)); - - // for safety, always take the lock unless we know they've actively exited - if (pulse) - { - lock (this) - { - Monitor.PulseAll(this); - } - } - } - private TResponse ThrowFailure(short token) { try @@ -468,4 +546,15 @@ and we'd rather make people know that there's a problem immediately. This also m Reset(true); return result; } + + private protected override void SetExceptionPreChecked(Exception exception) + => _asyncCore.SetException(exception); + + protected override bool TrySetResultPrecheckedToken(ref RespReader reader) => + TrySetResultPrecheckedToken(Parse(ref reader)); + + protected override bool TrySetDefaultResultPrecheckedToken() + => TrySetResultPrecheckedToken(default!); + + protected override void NextToken() => _asyncCore.Reset(); } diff --git a/src/RESPite/Internal/RespOperationExtensions.cs b/src/RESPite/Internal/RespOperationExtensions.cs index 242f42713..0aedccc69 100644 --- a/src/RESPite/Internal/RespOperationExtensions.cs +++ b/src/RESPite/Internal/RespOperationExtensions.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System.Buffers; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -32,6 +33,22 @@ internal static void DebugScramble(this Memory value) internal static void DebugScramble(this ReadOnlyMemory value) => MemoryMarshal.AsMemory(value).Span.Fill(42); + [Conditional("DEBUG")] + internal static void DebugScramble(this ReadOnlySequence value) + { + if (value.IsSingleSegment) + { + value.First.DebugScramble(); + } + else + { + foreach (var segment in value) + { + segment.DebugScramble(); + } + } + } + [Conditional("DEBUG")] internal static void DebugScramble(this byte[]? value) { diff --git a/src/RESPite/Internal/StreamConnection.cs b/src/RESPite/Internal/StreamConnection.cs index 5eed443cc..c176929f6 100644 --- a/src/RESPite/Internal/StreamConnection.cs +++ b/src/RESPite/Internal/StreamConnection.cs @@ -222,7 +222,7 @@ private async Task ReadAllAsync() // another formatter glitch while (CommitAndParseFrames(read)); - Volatile.Write(ref _readStatus, READER_COMPLETED); + Volatile.Write(ref _readStatus, ReaderCompleted); _readBuffer.Release(); // clean exit, we can recycle } catch (Exception ex) @@ -257,7 +257,7 @@ private void ReadAll() // another formatter glitch while (CommitAndParseFrames(read)); - Volatile.Write(ref _readStatus, READER_COMPLETED); + Volatile.Write(ref _readStatus, ReaderCompleted); _readBuffer.Release(); // clean exit, we can recycle tcs.TrySetResult(null); } @@ -283,7 +283,7 @@ internal override void ThrowIfUnhealthy() private void OnReadException(Exception ex, [CallerMemberName] string operation = "") { _fault ??= ex; - Volatile.Write(ref _readStatus, READER_FAILED); + Volatile.Write(ref _readStatus, ReaderFailed); Debug.WriteLine($"Reader failed: {ex.Message}"); ActivationHelper.DebugBreak(); while (_outstanding.TryDequeue(out var pending)) @@ -352,6 +352,31 @@ private static void DebugValidateSingleFrame(ReadOnlySpan payload) var reader = new RespReader(payload); reader.MoveNext(); reader.SkipChildren(); + + if (reader.TryMoveNext()) + { + throw new InvalidOperationException($"Unexpected trailing {reader.Prefix}"); + } + + if (reader.ProtocolBytesRemaining != 0) + { + var copy = reader; // leave reader alone for inspection + var prefix = copy.TryMoveNext() ? copy.Prefix : RespPrefix.None; + throw new InvalidOperationException( + $"Unexpected additional {reader.ProtocolBytesRemaining} bytes remaining, {prefix}"); + } + } + + [Conditional("DEBUG")] + private static void DebugValidateFrameCount(in ReadOnlySequence payload, int count) + { + var reader = new RespReader(payload); + while (count-- > 0) + { + reader.MoveNext(); + reader.SkipChildren(); + } + if (reader.TryMoveNext()) { throw new InvalidOperationException($"Unexpected trailing {reader.Prefix}"); @@ -410,14 +435,14 @@ static bool IsArrayPong(ReadOnlySpan payload) } private int _writeStatus, _readStatus; - private const int WRITER_AVAILABLE = 0, WRITER_TAKEN = 1, WRITER_DOOMED = 2; - private const int READER_ACTIVE = 0, READER_FAILED = 1, READER_COMPLETED = 2; + private const int WriterAvailable = 0, WriterTaken = 1, WriterDoomed = 2; + private const int ReaderActive = 0, ReaderFailed = 1, ReaderCompleted = 2; private void TakeWriter() { - var status = Interlocked.CompareExchange(ref _writeStatus, WRITER_TAKEN, WRITER_AVAILABLE); - if (status != WRITER_AVAILABLE) ThrowWriterNotAvailable(); - Debug.Assert(Volatile.Read(ref _writeStatus) == WRITER_TAKEN, "writer should be taken"); + var status = Interlocked.CompareExchange(ref _writeStatus, WriterTaken, WriterAvailable); + if (status != WriterAvailable) ThrowWriterNotAvailable(); + Debug.Assert(Volatile.Read(ref _writeStatus) == WriterTaken, "writer should be taken"); } private void ThrowWriterNotAvailable() @@ -426,10 +451,10 @@ private void ThrowWriterNotAvailable() var status = Volatile.Read(ref _writeStatus); var msg = status switch { - WRITER_TAKEN => "A write operation is already in progress; concurrent writes are not supported.", - WRITER_DOOMED when fault is not null => "This connection is terminated; no further writes are possible: " + + WriterTaken => "A write operation is already in progress; concurrent writes are not supported.", + WriterDoomed when fault is not null => "This connection is terminated; no further writes are possible: " + fault.Message, - WRITER_DOOMED => "This connection is terminated; no further writes are possible.", + WriterDoomed => "This connection is terminated; no further writes are possible.", _ => $"Unexpected writer status: {status}", }; throw fault is null ? new InvalidOperationException(msg) : new InvalidOperationException(msg, fault); @@ -437,14 +462,14 @@ private void ThrowWriterNotAvailable() private Exception? _fault; - private void ReleaseWriter(int status = WRITER_AVAILABLE) + private void ReleaseWriter(int status = WriterAvailable) { - if (status == WRITER_AVAILABLE && _isDoomed) + if (status == WriterAvailable && _isDoomed) { - status = WRITER_DOOMED; + status = WriterDoomed; } - Interlocked.CompareExchange(ref _writeStatus, status, WRITER_TAKEN); + Interlocked.CompareExchange(ref _writeStatus, status, WriterTaken); } [MethodImpl(MethodImplOptions.NoInlining)] @@ -453,7 +478,7 @@ private void OnRequestUnavailable(in RespOperation message) if (!message.IsCompleted) { // make sure they know something is wrong - message.Message.TrySetException(message.Token, new InvalidOperationException("Request is not available")); + message.TrySetException(new InvalidOperationException("Request is not available")); } } @@ -466,18 +491,26 @@ public override void Write(in RespOperation message) return; } - DebugValidateSingleFrame(bytes.Span); + DebugValidateFrameCount(bytes, message.MessageCount); TakeWriter(); try { _outstanding.Enqueue(message); releaseRequest = false; // once we write, only release on success + if (bytes.IsSingleSegment) + { #if NETCOREAPP || NETSTANDARD2_1_OR_GREATER - tail.Write(bytes.Span); + tail.Write(bytes.FirstSpan); #else - tail.Write(bytes); + tail.Write(bytes.First); #endif - DebugCounters.OnWrite(bytes.Length); + DebugCounters.OnSyncWrite(bytes.First.Length); + } + else + { + WriteMultiSegment(tail, in bytes); + } + ReleaseWriter(); message.Message.ReleaseRequest(); } @@ -485,13 +518,36 @@ public override void Write(in RespOperation message) { Debug.WriteLine($"Writer failed: {ex.Message}"); ActivationHelper.DebugBreak(); - ReleaseWriter(WRITER_DOOMED); + ReleaseWriter(WriterDoomed); if (releaseRequest) message.Message.ReleaseRequest(); OnConnectionError(ConnectionError, ex); throw; } } + private static void WriteMultiSegment(Stream tail, in ReadOnlySequence payload) + { + foreach (var segment in payload) + { +#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER + tail.Write(segment.Span); +#else + tail.Write(segment); +#endif + DebugCounters.OnSyncWrite(segment.Length); + } + } + + private static async ValueTask WriteMultiSegmentAsync(Stream tail, ReadOnlySequence payload) + { + foreach (var segment in payload) + { + var pending = tail.WriteAsync(segment, CancellationToken.None); + DebugCounters.OnAsyncWrite(segment.Length, pending.IsCompleted); + await pending.ConfigureAwait(false); + } + } + internal override void Write(ReadOnlySpan messages) { switch (messages.Length) @@ -504,7 +560,7 @@ internal override void Write(ReadOnlySpan messages) } TakeWriter(); - IRespMessage? toRelease = null; + RespMessageBase? toRelease = null; try { foreach (var message in messages) @@ -519,15 +575,23 @@ internal override void Write(ReadOnlySpan messages) continue; } - DebugValidateSingleFrame(bytes.Span); + DebugValidateFrameCount(bytes, message.MessageCount); _outstanding.Enqueue(message); toRelease = null; // once we write, only release on success + if (bytes.IsSingleSegment) + { #if NETCOREAPP || NETSTANDARD2_1_OR_GREATER - tail.Write(bytes.Span); + tail.Write(bytes.FirstSpan); #else - tail.Write(bytes); + tail.Write(bytes.First); #endif - DebugCounters.OnWrite(bytes.Length); + DebugCounters.OnSyncWrite(bytes.First.Length); + } + else + { + WriteMultiSegment(tail, in bytes); + } + ReleaseWriter(); message.Message.ReleaseRequest(); } @@ -536,7 +600,7 @@ internal override void Write(ReadOnlySpan messages) { Debug.WriteLine($"Writer failed: {ex.Message}"); ActivationHelper.DebugBreak(); - ReleaseWriter(WRITER_DOOMED); + ReleaseWriter(WriterDoomed); toRelease?.ReleaseRequest(); foreach (var message in messages) { @@ -558,26 +622,27 @@ public override Task WriteAsync(in RespOperation message) return Task.CompletedTask; } - DebugValidateSingleFrame(bytes.Span); - TakeWriter(); + DebugValidateFrameCount(bytes, message.MessageCount); try { _outstanding.Enqueue(message); releaseRequest = false; // once we write, only release on success - var pendingWrite = tail.WriteAsync(bytes, CancellationToken.None); - if (!pendingWrite.IsCompleted) + ValueTask pendingWrite; + if (bytes.IsSingleSegment) { - return AwaitedSingleWithToken( - this, - pendingWrite, -#if DEBUG - bytes.Length, -#endif - message.Message); + pendingWrite = tail.WriteAsync(bytes.First, CancellationToken.None); + DebugCounters.OnAsyncWrite(bytes.First.Length, pendingWrite.IsCompleted); + } + else + { + pendingWrite = WriteMultiSegmentAsync(tail, bytes); } + if (!pendingWrite.IsCompleted) + { + return AwaitedSingleWithToken(this, pendingWrite, message.Message); + } pendingWrite.GetAwaiter().GetResult(); - DebugCounters.OnAsyncWrite(bytes.Length, true); ReleaseWriter(); message.Message.ReleaseRequest(); return Task.CompletedTask; @@ -586,7 +651,7 @@ public override Task WriteAsync(in RespOperation message) { Debug.WriteLine($"Writer failed: {ex.Message}"); ActivationHelper.DebugBreak(); - ReleaseWriter(WRITER_DOOMED); + ReleaseWriter(WriterDoomed); if (releaseRequest) message.Message.ReleaseRequest(); OnConnectionError(ConnectionError, ex); throw; @@ -595,23 +660,17 @@ public override Task WriteAsync(in RespOperation message) static async Task AwaitedSingleWithToken( StreamConnection @this, ValueTask pendingWrite, -#if DEBUG - int length, -#endif - IRespMessage message) + RespMessageBase message) { try { await pendingWrite.ConfigureAwait(false); -#if DEBUG - DebugCounters.OnAsyncWrite(length, false); -#endif @this.ReleaseWriter(); message.ReleaseRequest(); } catch (Exception ex) { - @this.ReleaseWriter(WRITER_DOOMED); + @this.ReleaseWriter(WriterDoomed); OnConnectionError(@this.ConnectionError, ex, $"{nameof(WriteAsync)}:{nameof(AwaitedSingleWithToken)}"); throw; } @@ -636,7 +695,7 @@ internal override Task WriteAsync(ReadOnlyMemory messages) private async Task CombineAndSendMultipleAsync(StreamConnection @this, ReadOnlyMemory messages) { TakeWriter(); - IRespMessage? toRelease = null; + RespMessageBase? toRelease = null; int definitelySent = 0; try { @@ -650,9 +709,10 @@ private async Task CombineAndSendMultipleAsync(StreamConnection @this, ReadOnlyM continue; // skip this message } + DebugValidateFrameCount(bytes, message.MessageCount); toRelease = message.Message; // append to the scratch and consider written (even though we haven't actually) - _writeBuffer.Write(bytes.Span); + _writeBuffer.Write(bytes); toRelease = null; message.Message.ReleaseRequest(); @this._outstanding.Enqueue(message); @@ -696,7 +756,7 @@ private async Task CombineAndSendMultipleAsync(StreamConnection @this, ReadOnlyM { Debug.WriteLine($"Writer failed: {ex.Message}"); ActivationHelper.DebugBreak(); - ReleaseWriter(WRITER_DOOMED); + ReleaseWriter(WriterDoomed); toRelease?.ReleaseRequest(); foreach (var message in messages.Span.Slice(start: definitelySent)) { @@ -711,7 +771,7 @@ private async Task CombineAndSendMultipleAsync(StreamConnection @this, ReadOnlyM private void Doom() { _isDoomed = true; // without a reader, there's no point writing - Interlocked.CompareExchange(ref _writeStatus, WRITER_DOOMED, WRITER_AVAILABLE); + Interlocked.CompareExchange(ref _writeStatus, WriterDoomed, WriterAvailable); } protected override void OnDispose(bool disposing) diff --git a/src/RESPite/RespContextExtensions.cs b/src/RESPite/RespContextExtensions.cs index 37c8a5fab..08d923edf 100644 --- a/src/RESPite/RespContextExtensions.cs +++ b/src/RESPite/RespContextExtensions.cs @@ -197,7 +197,8 @@ public static RespOperation CreateOperation( var conn = context.Connection; var memory = conn.Serializer.Serialize(conn.NonDefaultCommandMap, command, request, formatter); - var msg = RespMessage.Get(parser).Init(memory, context.CancellationToken); + var msg = RespMessage.Get(parser); + msg.Init(memory, context.CancellationToken); return new(msg); } @@ -214,7 +215,8 @@ public static RespOperation CreateOperation( var conn = context.Connection; var memory = conn.Serializer.Serialize(conn.NonDefaultCommandMap, command, request, formatter); - var msg = RespMessage.Get(parser).Init(memory, context.CancellationToken); + var msg = RespMessage.Get(parser); + msg.Init(memory, context.CancellationToken); return new(msg); } @@ -231,8 +233,8 @@ public static RespOperation CreateOperation.Get(in state, parser) - .Init(memory, context.CancellationToken); + var msg = RespMessage.Get(in state, parser); + msg.Init(memory, context.CancellationToken); return new(msg); } } diff --git a/src/RESPite/RespOperation.cs b/src/RESPite/RespOperation.cs index ca5b9ce1c..d813767e0 100644 --- a/src/RESPite/RespOperation.cs +++ b/src/RESPite/RespOperation.cs @@ -36,11 +36,11 @@ internal static void DebugOnAllocateMessage() } // it is important that this layout remains identical between RespOperation and RespOperation - private readonly IRespMessage _message; + private readonly RespMessageBase _message; private readonly short _token; private readonly bool _disableCaptureContext; // default is false, so: bypass - internal RespOperation(IRespMessage message, bool disableCaptureContext = false) + internal RespOperation(RespMessageBase message, bool disableCaptureContext = false) { _message = message; _token = message.Token; @@ -48,9 +48,9 @@ internal RespOperation(IRespMessage message, bool disableCaptureContext = false) } public bool IsSent => Message.IsSent(_token); - internal IRespMessage Message => _message ?? ThrowNoMessage(); + internal RespMessageBase Message => _message ?? ThrowNoMessage(); - internal static IRespMessage ThrowNoMessage() + internal static RespMessageBase ThrowNoMessage() => throw new InvalidOperationException($"{nameof(RespOperation)} is not correctly initialized"); /// @@ -66,7 +66,7 @@ public static implicit operator ValueTask(in RespOperation operation) /// public void Wait(TimeSpan timeout = default) - => Message.Wait(_token, timeout); + => Message.WaitVoid(_token, timeout); /// public bool IsCompleted => Message.GetStatus(_token) != ValueTaskSourceStatus.Pending; @@ -80,9 +80,15 @@ public void Wait(TimeSpan timeout = default) /// public bool IsCanceled => Message.GetStatus(_token) == ValueTaskSourceStatus.Canceled; - internal short Token => _token; public ref readonly CancellationToken CancellationToken => ref Message.CancellationToken; + internal short Token => _token; + internal int MessageCount => Message.MessageCount; + internal bool TrySetException(Exception exception) => Message.TrySetException(_token, exception); + internal bool TrySetCancelled(CancellationToken cancellationToken = default) => Message.TrySetCanceled(_token, cancellationToken); + internal bool TryReserveRequest(out ReadOnlySequence payload, bool recordSent = true) => Message.TryReserveRequest(_token, out payload, recordSent); + internal void ReleaseRequest() => Message.ReleaseRequest(); + internal static readonly Action InvokeState = static state => ((Action)state!).Invoke(); /// @@ -108,7 +114,7 @@ public void UnsafeOnCompleted(Action continuation) } /// - public void GetResult() => Message.GetResult(_token); + public void GetResult() => Message.GetResultVoid(_token); /// public RespOperation GetAwaiter() => this; @@ -128,10 +134,10 @@ public RespOperation ConfigureAwait(bool continueOnCapturedContext) [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public readonly struct Remote { - private readonly IRespMessage _message; + private readonly RespMessageBase _message; private readonly short _token; - internal Remote(IRespMessage message) + internal Remote(RespMessageBase message) { _message = message; _token = message.Token; @@ -167,7 +173,8 @@ public static RespOperation Create( bool sent = true, CancellationToken cancellationToken = default) { - var msg = RespMessage.Get(null).Init(sent, cancellationToken); + var msg = RespMessage.Get(null); + msg.Init(sent, cancellationToken); remote = new(msg); return new RespOperation(msg); } @@ -183,7 +190,8 @@ public static RespOperation Create( bool sent = true, CancellationToken cancellationToken = default) { - var msg = RespMessage.Get(parser).Init(sent, cancellationToken); + var msg = RespMessage.Get(parser); + msg.Init(sent, cancellationToken); remote = new(msg); return new RespOperation(msg); } @@ -201,7 +209,8 @@ public static RespOperation Create( bool sent = true, CancellationToken cancellationToken = default) { - var msg = RespMessage.Get(in state, parser).Init(sent, cancellationToken); + var msg = RespMessage.Get(in state, parser); + msg.Init(sent, cancellationToken); remote = new(msg); return new RespOperation(msg); } diff --git a/src/RESPite/RespOperationT.cs b/src/RESPite/RespOperationT.cs index aa4a0c55b..375241a58 100644 --- a/src/RESPite/RespOperationT.cs +++ b/src/RESPite/RespOperationT.cs @@ -26,10 +26,9 @@ internal RespOperation(RespMessageBase message, bool disableCaptureContext = _disableCaptureContext = disableCaptureContext; } - internal IRespMessage Message => _message ?? RespOperation.ThrowNoMessage(); public CancellationToken CancellationToken => Message.CancellationToken; - private RespMessageBase TypedMessage => _message ?? (RespMessageBase)RespOperation.ThrowNoMessage(); + private RespMessageBase Message => _message ?? (RespMessageBase)RespOperation.ThrowNoMessage(); /// /// Treats this operation as an untyped . @@ -44,34 +43,34 @@ public static implicit operator RespOperation(in RespOperation operation) /// Treats this operation as an untyped . /// public static implicit operator ValueTask(in RespOperation operation) - => new(operation.TypedMessage, operation._token); + => new(operation.Message, operation._token); /// /// Treats this operation as a . /// public static implicit operator ValueTask(in RespOperation operation) - => new(operation.TypedMessage, operation._token); + => new(operation.Message, operation._token); /// - public Task AsTask() => new ValueTask(TypedMessage, _token).AsTask(); + public Task AsTask() => new ValueTask(Message, _token).AsTask(); - public ValueTask AsValueTask() => new(TypedMessage, _token); + public ValueTask AsValueTask() => new(Message, _token); /// public T Wait(TimeSpan timeout = default) - => TypedMessage.Wait(_token, timeout); + => Message.Wait(_token, timeout); /// - public bool IsCompleted => TypedMessage.GetStatus(_token) != ValueTaskSourceStatus.Pending; + public bool IsCompleted => Message.GetStatus(_token) != ValueTaskSourceStatus.Pending; /// - public bool IsCompletedSuccessfully => TypedMessage.GetStatus(_token) == ValueTaskSourceStatus.Succeeded; + public bool IsCompletedSuccessfully => Message.GetStatus(_token) == ValueTaskSourceStatus.Succeeded; /// - public bool IsFaulted => TypedMessage.GetStatus(_token) == ValueTaskSourceStatus.Faulted; + public bool IsFaulted => Message.GetStatus(_token) == ValueTaskSourceStatus.Faulted; /// - public bool IsCanceled => TypedMessage.GetStatus(_token) == ValueTaskSourceStatus.Canceled; + public bool IsCanceled => Message.GetStatus(_token) == ValueTaskSourceStatus.Canceled; /// /// @@ -82,10 +81,10 @@ public void OnCompleted(Action continuation) ? ValueTaskSourceOnCompletedFlags.FlowExecutionContext : ValueTaskSourceOnCompletedFlags.FlowExecutionContext | ValueTaskSourceOnCompletedFlags.UseSchedulingContext; - TypedMessage.OnCompletedWithNotSentDetection(RespOperation.InvokeState, continuation, _token, flags); + Message.OnCompletedWithNotSentDetection(RespOperation.InvokeState, continuation, _token, flags); } - public bool IsSent => TypedMessage.IsSent(_token); + public bool IsSent => Message.IsSent(_token); /// public void UnsafeOnCompleted(Action continuation) @@ -94,11 +93,11 @@ public void UnsafeOnCompleted(Action continuation) var flags = _disableCaptureContext ? ValueTaskSourceOnCompletedFlags.None : ValueTaskSourceOnCompletedFlags.UseSchedulingContext; - TypedMessage.OnCompletedWithNotSentDetection(RespOperation.InvokeState, continuation, _token, flags); + Message.OnCompletedWithNotSentDetection(RespOperation.InvokeState, continuation, _token, flags); } /// - public T GetResult() => TypedMessage.GetResult(_token); + public T GetResult() => Message.GetResult(_token); /// public RespOperation GetAwaiter() => this; From a300f06b0fc10a42c85289633fda8a1aa2b1ecf4 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 2 Sep 2025 14:14:26 +0100 Subject: [PATCH 029/108] prevent message copy in batch --- src/RESPite.Benchmark/BenchmarkBase.cs | 165 +++++++++++------- src/RESPite.Benchmark/NewCoreBenchmark.cs | 71 ++++---- src/RESPite.Benchmark/OldCoreBenchmark.cs | 119 +++++++------ .../Internal/BasicBatchConnection.cs | 128 ++++++++------ .../Internal/DecoratorConnection.cs | 5 + src/RESPite/Internal/DebugCounters.cs | 16 +- src/RESPite/RespBatch.cs | 7 + 7 files changed, 304 insertions(+), 207 deletions(-) diff --git a/src/RESPite.Benchmark/BenchmarkBase.cs b/src/RESPite.Benchmark/BenchmarkBase.cs index 673ae91b8..0eecdc816 100644 --- a/src/RESPite.Benchmark/BenchmarkBase.cs +++ b/src/RESPite.Benchmark/BenchmarkBase.cs @@ -6,7 +6,6 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; -using RESPite; // influenced by redis-benchmark, see .md file namespace RESPite.Benchmark; @@ -120,17 +119,40 @@ public BenchmarkBase(string[] args) } public abstract Task RunAll(); +} - protected static readonly Func - NoFlush = () => throw new NotSupportedException("Not a batch; cannot flush"); +public abstract class BenchmarkBase(string[] args) : BenchmarkBase(args) +{ + protected virtual Task OnCleanupAsync(TClient client) => Task.CompletedTask; - protected Task Pipeline(Func operation, Func flush) => - Pipeline(() => new ValueTask(operation()), flush); + protected virtual Task InitAsync(TClient client) => Task.CompletedTask; + + public async Task CleanupAsync() + { + try + { + var client = GetClient(0); + await Delete(client, _getSetKey).ConfigureAwait(false); + await Delete(client, _counterKey).ConfigureAwait(false); + await Delete(client, _listKey).ConfigureAwait(false); + await Delete(client, _setKey).ConfigureAwait(false); + await Delete(client, _hashKey).ConfigureAwait(false); + await Delete(client, _sortedSetKey).ConfigureAwait(false); + await Delete(client, _streamKey).ConfigureAwait(false); + await OnCleanupAsync(client).ConfigureAwait(false); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Cleanup: {ex.Message}"); + } + } - protected Task Pipeline(Func> operation, Func flush) => - Pipeline(() => new ValueTask(operation()), flush); + protected virtual ValueTask Flush(TClient client) => default; + protected virtual void PrepareBatch(TClient client, int count) { } - protected async Task Pipeline(Func operation, Func flush) + private async Task PipelineUntyped( + TClient client, + Func operation) { var opsPerClient = OperationsPerClient; int i = 0; @@ -140,7 +162,7 @@ protected async Task Pipeline(Func operation, Func flush) { for (; i < opsPerClient; i++) { - await operation().ConfigureAwait(false); + await operation(client).ConfigureAwait(false); } } else if (PipelineMode == PipelineStrategy.Queue) @@ -153,7 +175,7 @@ protected async Task Pipeline(Func operation, Func flush) await queue.Dequeue().ConfigureAwait(false); } - queue.Enqueue(operation()); + queue.Enqueue(operation(client)); } while (queue.Count > 0) @@ -164,14 +186,15 @@ protected async Task Pipeline(Func operation, Func flush) else if (PipelineMode == PipelineStrategy.Batch) { int count = 0; - if (flush is null) throw new InvalidOperationException("Flush is required for batch mode"); var oversized = ArrayPool.Shared.Rent(PipelineDepth); + PrepareBatch(client, Math.Min(opsPerClient, PipelineDepth)); for (; i < opsPerClient; i++) { - oversized[count++] = operation(); + oversized[count++] = operation(client); if (count == PipelineDepth) { - await flush().ConfigureAwait(false); + await Flush(client).ConfigureAwait(false); + PrepareBatch(client, Math.Min(opsPerClient - i, PipelineDepth)); for (int j = 0; j < count; j++) { await oversized[j].ConfigureAwait(false); @@ -181,7 +204,7 @@ protected async Task Pipeline(Func operation, Func flush) } } - await flush().ConfigureAwait(false); + await Flush(client).ConfigureAwait(false); for (int j = 0; j < count; j++) { await oversized[j].ConfigureAwait(false); @@ -199,20 +222,22 @@ protected async Task Pipeline(Func operation, Func flush) Console.Error.WriteLine($"{operation.Method.Name} failed after {i} operations"); Program.WriteException(ex); } + + return DBNull.Value; } - protected async Task Pipeline(Func> operation, Func flush) + private async Task PipelineTyped(TClient client, Func> operation) { var opsPerClient = OperationsPerClient; int i = 0; - T? result = default; + T result = default!; try { if (PipelineDepth == 1) { for (; i < opsPerClient; i++) { - result = await operation().ConfigureAwait(false); + result = await operation(client).ConfigureAwait(false); } } else if (PipelineMode == PipelineStrategy.Queue) @@ -225,7 +250,7 @@ protected async Task Pipeline(Func operation, Func flush) _ = await queue.Dequeue().ConfigureAwait(false); } - queue.Enqueue(operation()); + queue.Enqueue(operation(client)); } while (queue.Count > 0) @@ -236,14 +261,15 @@ protected async Task Pipeline(Func operation, Func flush) else if (PipelineMode == PipelineStrategy.Batch) { int count = 0; - if (flush is null) throw new InvalidOperationException("Flush is required for batch mode"); var oversized = ArrayPool>.Shared.Rent(PipelineDepth); + PrepareBatch(client, Math.Min(opsPerClient, PipelineDepth)); for (; i < opsPerClient; i++) { - oversized[count++] = operation(); + oversized[count++] = operation(client); if (count == PipelineDepth) { - await flush().ConfigureAwait(false); + await Flush(client).ConfigureAwait(false); + PrepareBatch(client, Math.Min(opsPerClient - i, PipelineDepth)); for (int j = 0; j < count; j++) { result = await oversized[j].ConfigureAwait(false); @@ -253,7 +279,7 @@ protected async Task Pipeline(Func operation, Func flush) } } - await flush().ConfigureAwait(false); + await Flush(client).ConfigureAwait(false); for (int j = 0; j < count; j++) { result = await oversized[j].ConfigureAwait(false); @@ -274,35 +300,6 @@ protected async Task Pipeline(Func operation, Func flush) return result; } -} - -public abstract class BenchmarkBase(string[] args) : BenchmarkBase(args) -{ - protected virtual Task OnCleanupAsync(TClient client) => Task.CompletedTask; - - protected virtual Task InitAsync(TClient client) => Task.CompletedTask; - - protected virtual Func GetFlush(TClient client) => NoFlush; - - public async Task CleanupAsync() - { - try - { - var client = GetClient(0); - await Delete(client, _getSetKey).ConfigureAwait(false); - await Delete(client, _counterKey).ConfigureAwait(false); - await Delete(client, _listKey).ConfigureAwait(false); - await Delete(client, _setKey).ConfigureAwait(false); - await Delete(client, _hashKey).ConfigureAwait(false); - await Delete(client, _sortedSetKey).ConfigureAwait(false); - await Delete(client, _streamKey).ConfigureAwait(false); - await OnCleanupAsync(client).ConfigureAwait(false); - } - catch (Exception ex) - { - Console.Error.WriteLine($"Cleanup: {ex.Message}"); - } - } public async Task InitAsync() { @@ -318,15 +315,37 @@ public async Task InitAsync() protected abstract TClient CreateBatch(TClient client); - protected async Task RunAsync( + protected Task RunAsync( string? key, - Func, Task> action, - Func? init = null, + Func> action, + Func? init = null, + string format = "") + => RunAsyncCore( + key, + action, + client => action(client).AsUntypedValueTask(), + client => PipelineTyped(client, action), + init, + format); + + protected Task RunAsync( + string? key, + Func action, + Func? init = null, + string format = "") + => RunAsyncCore(key, action, action, client => PipelineUntyped(client, action), init, format); + + protected async Task RunAsyncCore( + string? key, + Delegate underlyingAction, + Func test, + Func> pipeline, + Func? init = null, string format = "") { - string name = action.Method.Name; + string name = underlyingAction.Method.Name; - if (action.Method.GetCustomAttribute(typeof(DisplayNameAttribute)) is DisplayNameAttribute + if (underlyingAction.Method.GetCustomAttribute(typeof(DisplayNameAttribute)) is DisplayNameAttribute { DisplayName: { Length: > 0 } } dna) @@ -339,7 +358,7 @@ protected async Task RunAsync( // include additional test metadata string description = ""; - if (action.Method.GetCustomAttribute(typeof(DescriptionAttribute)) is DescriptionAttribute + if (underlyingAction.Method.GetCustomAttribute(typeof(DescriptionAttribute)) is DescriptionAttribute { Description: { Length: > 0 } } da) @@ -377,6 +396,7 @@ protected async Task RunAsync( Console.WriteLine(")"); } + bool didNotRun = false; try { if (key is not null) @@ -384,6 +404,17 @@ protected async Task RunAsync( await Delete(GetClient(0), key).ConfigureAwait(false); } + try + { + await test(GetClient(0)).ConfigureAwait(false); + } + catch (Exception ex) + { + Console.Error.WriteLine($"\t{ex.Message}"); + didNotRun = true; + return; + } + if (init is not null) { await init(GetClient(0)).ConfigureAwait(false); @@ -408,8 +439,7 @@ protected async Task RunAsync( client = CreateBatch(client); } - var flush = GetFlush(client); - pending[index++] = Task.Run(() => action(WithCancellation(client, cancellationToken), flush)); + pending[index++] = Task.Run(() => pipeline(WithCancellation(client, cancellationToken))); } await Task.WhenAll(pending).ConfigureAwait(false); @@ -428,7 +458,7 @@ protected async Task RunAsync( $"{TotalOperations:###,###,##0} requests completed in {seconds:0.00} seconds, {rate:###,###,##0} ops/sec"); } - if (!Quiet) + if (!Quiet & typeof(T) != typeof(DBNull)) { if (string.IsNullOrWhiteSpace(format)) { @@ -446,9 +476,10 @@ protected async Task RunAsync( } finally { -#if DEBUG && !TEST_BASELINE + _ = didNotRun; +#if DEBUG var counters = Internal.DebugCounters.Flush(); // flush even if not showing - if (!Quiet) + if (!Quiet & !didNotRun) { if (counters.WriteBytes != 0) { @@ -510,6 +541,12 @@ protected async Task RunAsync( Console.WriteLine(); } + if (counters.BatchGrowCount != 0) + { + Console.WriteLine( + $"Batch growth; {counters.BatchGrowCount:#,##0} events, {counters.BatchGrowCopyCount:#,###,##0} elements copied"); + } + if (counters.BufferCreatedCount != 0 || counters.BufferRecycledCount != 0 | counters.BufferMessageCount != 0) { @@ -519,7 +556,8 @@ protected async Task RunAsync( Console.Write( $"; created: {counters.BufferCreatedCount:#,###,##0}, {FormatBytes(counters.BufferTotalBytes)}"); // always write recycled count - it being zero is important - Console.Write($"; recycled: {counters.BufferRecycledCount:#,###,##0}, {FormatBytes(counters.BufferRecycledBytes)}"); + Console.Write( + $"; recycled: {counters.BufferRecycledCount:#,###,##0}, {FormatBytes(counters.BufferRecycledBytes)}"); } if (counters.BufferMessageCount != 0) @@ -527,6 +565,7 @@ protected async Task RunAsync( Console.Write( $"; {counters.BufferMessageCount:#,###,##0} messages, {FormatBytes(counters.BufferMessageBytes)}"); } + Console.Write( $"; max working {FormatBytes(counters.BufferMaxOutstandingBytes)}; {counters.BufferPinCount:#,###,##0} pins; {counters.BufferLeakCount:#,###,##0} leaks"); Console.WriteLine(); diff --git a/src/RESPite.Benchmark/NewCoreBenchmark.cs b/src/RESPite.Benchmark/NewCoreBenchmark.cs index f330e001e..5d415d8d6 100644 --- a/src/RESPite.Benchmark/NewCoreBenchmark.cs +++ b/src/RESPite.Benchmark/NewCoreBenchmark.cs @@ -109,78 +109,82 @@ public override async Task RunAll() await CleanupAsync().ConfigureAwait(false); } - protected override RespContext CreateBatch(RespContext client) => client.CreateBatch().Context; + protected override RespContext CreateBatch(RespContext client) => client.CreateBatch(PipelineDepth).Context; - protected override Func GetFlush(RespContext client) + protected override ValueTask Flush(RespContext client) { if (client.Connection is RespBatch batch) { - return () => - { - return new(batch.FlushAsync()); - }; + return new(batch.FlushAsync()); } - return base.GetFlush(client); + return default; + } + + protected override void PrepareBatch(RespContext client, int count) + { + if (client.Connection is RespBatch batch) + { + batch.EnsureCapacity(count); + } } [DisplayName("PING_INLINE")] - private Task PingInline(RespContext ctx, Func flush) => Pipeline(() => ctx.PingInlineAsync(_payload), flush); + private ValueTask PingInline(RespContext ctx) => ctx.PingInlineAsync(_payload); [DisplayName("PING_BULK")] - private Task PingBulk(RespContext ctx, Func flush) => Pipeline(() => ctx.PingAsync(_payload), flush); + private ValueTask PingBulk(RespContext ctx) => ctx.PingAsync(_payload); [DisplayName("INCR")] - private Task Incr(RespContext ctx, Func flush) => Pipeline(() => ctx.IncrAsync(_counterKey), flush); + private ValueTask Incr(RespContext ctx) => ctx.IncrAsync(_counterKey); [DisplayName("GET")] - private Task Get(RespContext ctx, Func flush) => Pipeline(() => ctx.GetAsync(_getSetKey), flush); + private ValueTask Get(RespContext ctx) => ctx.GetAsync(_getSetKey); - private Task GetInit(RespContext ctx) => ctx.SetAsync(_getSetKey, _payload).AsTask(); + private ValueTask GetInit(RespContext ctx) => ctx.SetAsync(_getSetKey, _payload).AsUntypedValueTask(); [DisplayName("SET")] - private Task Set(RespContext ctx, Func flush) => Pipeline(() => ctx.SetAsync(_getSetKey, _payload), flush); + private ValueTask Set(RespContext ctx) => ctx.SetAsync(_getSetKey, _payload); [DisplayName("LPUSH")] - private Task LPush(RespContext ctx, Func flush) => Pipeline(() => ctx.LPushAsync(_listKey, _payload), flush); + private ValueTask LPush(RespContext ctx) => ctx.LPushAsync(_listKey, _payload); [DisplayName("RPUSH")] - private Task RPush(RespContext ctx, Func flush) => Pipeline(() => ctx.RPushAsync(_listKey, _payload), flush); + private ValueTask RPush(RespContext ctx) => ctx.RPushAsync(_listKey, _payload); [DisplayName("LRANGE_100")] - private Task LRange100(RespContext ctx, Func flush) => Pipeline(() => ctx.LRangeAsync(_listKey, 0, 99), flush); + private ValueTask LRange100(RespContext ctx) => ctx.LRangeAsync(_listKey, 0, 99); [DisplayName("LRANGE_300")] - private Task LRange300(RespContext ctx, Func flush) => Pipeline(() => ctx.LRangeAsync(_listKey, 0, 299), flush); + private ValueTask LRange300(RespContext ctx) => ctx.LRangeAsync(_listKey, 0, 299); [DisplayName("LRANGE_500")] - private Task LRange500(RespContext ctx, Func flush) => Pipeline(() => ctx.LRangeAsync(_listKey, 0, 499), flush); + private ValueTask LRange500(RespContext ctx) => ctx.LRangeAsync(_listKey, 0, 499); [DisplayName("LRANGE_600")] - private Task LRange600(RespContext ctx, Func flush) => Pipeline(() => ctx.LRangeAsync(_listKey, 0, 599), flush); + private ValueTask LRange600(RespContext ctx) => ctx.LRangeAsync(_listKey, 0, 599); [DisplayName("LPOP")] - private Task LPop(RespContext ctx, Func flush) => Pipeline(() => ctx.LPopAsync(_listKey), flush); + private ValueTask LPop(RespContext ctx) => ctx.LPopAsync(_listKey); [DisplayName("RPOP")] - private Task RPop(RespContext ctx, Func flush) => Pipeline(() => ctx.RPopAsync(_listKey), flush); + private ValueTask RPop(RespContext ctx) => ctx.RPopAsync(_listKey); - private Task LPopInit(RespContext ctx) => ctx.LPushAsync(_listKey, _payload, TotalOperations).AsTask(); + private ValueTask LPopInit(RespContext ctx) => ctx.LPushAsync(_listKey, _payload, TotalOperations).AsUntypedValueTask(); [DisplayName("SADD")] - private Task SAdd(RespContext ctx, Func flush) => Pipeline(() => ctx.SAddAsync(_setKey, "element:__rand_int__"), flush); + private ValueTask SAdd(RespContext ctx) => ctx.SAddAsync(_setKey, "element:__rand_int__"); [DisplayName("HSET")] - private Task HSet(RespContext ctx, Func flush) => - Pipeline(() => ctx.HSetAsync(_hashKey, "element:__rand_int__", _payload), flush); + private ValueTask HSet(RespContext ctx) => ctx.HSetAsync(_hashKey, "element:__rand_int__", _payload); [DisplayName("ZADD")] - private Task ZAdd(RespContext ctx, Func flush) => Pipeline(() => ctx.ZAddAsync(_sortedSetKey, 0, "element:__rand_int__"), flush); + private ValueTask ZAdd(RespContext ctx) => ctx.ZAddAsync(_sortedSetKey, 0, "element:__rand_int__"); [DisplayName("ZPOPMIN")] - private Task ZPopMin(RespContext ctx, Func flush) => Pipeline(() => ctx.ZPopMinAsync(_sortedSetKey), flush); + private ValueTask ZPopMin(RespContext ctx) => ctx.ZPopMinAsync(_sortedSetKey); - private async Task ZPopMinInit(RespContext ctx) + private async ValueTask ZPopMinInit(RespContext ctx) { int ops = TotalOperations; var rand = new Random(); @@ -192,9 +196,9 @@ await ctx.ZAddAsync(_sortedSetKey, (rand.NextDouble() * 2000) - 1000, "element:_ } [DisplayName("SPOP")] - private Task SPop(RespContext ctx, Func flush) => Pipeline(() => ctx.SPopAsync(_setKey), flush); + private ValueTask SPop(RespContext ctx) => ctx.SPopAsync(_setKey); - private async Task SPopInit(RespContext ctx) + private async ValueTask SPopInit(RespContext ctx) { int ops = TotalOperations; for (int i = 0; i < ops; i++) @@ -204,13 +208,12 @@ private async Task SPopInit(RespContext ctx) } [DisplayName("MSET"), Description("10 keys")] - private Task MSet(RespContext ctx, Func flush) => Pipeline(() => ctx.MSetAsync(_pairs), flush); + private ValueTask MSet(RespContext ctx) => ctx.MSetAsync(_pairs); - private Task LRangeInit(RespContext ctx) => ctx.LPushAsync(_listKey, _payload, TotalOperations).AsTask(); + private ValueTask LRangeInit(RespContext ctx) => ctx.LPushAsync(_listKey, _payload, TotalOperations).AsUntypedValueTask(); [DisplayName("XADD")] - private Task XAdd(RespContext ctx, Func flush) => - Pipeline(() => ctx.XAddAsync(_streamKey, "*", "myfield", _payload), flush); + private ValueTask XAdd(RespContext ctx) => ctx.XAddAsync(_streamKey, "*", "myfield", _payload); } internal static partial class RedisCommands diff --git a/src/RESPite.Benchmark/OldCoreBenchmark.cs b/src/RESPite.Benchmark/OldCoreBenchmark.cs index 6cea82fad..e758ea5ab 100644 --- a/src/RESPite.Benchmark/OldCoreBenchmark.cs +++ b/src/RESPite.Benchmark/OldCoreBenchmark.cs @@ -76,110 +76,110 @@ public override async Task RunAll() protected override IDatabaseAsync CreateBatch(IDatabaseAsync client) => ((IDatabase)client).CreateBatch(); - protected override Func GetFlush(IDatabaseAsync client) + protected override ValueTask Flush(IDatabaseAsync client) { if (client is IBatch batch) { - return () => - { - batch.Execute(); - return default; - }; + batch.Execute(); } - return GetFlush(client); + + return default; } [DisplayName("GET")] - private Task Get(IDatabaseAsync client, Func flush) => Pipeline(() => GetAndMeasureString(client), flush); + private ValueTask Get(IDatabaseAsync client) => GetAndMeasureString(client); - private async Task GetAndMeasureString(IDatabaseAsync client) + private async ValueTask GetAndMeasureString(IDatabaseAsync client) { using var lease = await client.StringGetLeaseAsync(_getSetKey).ConfigureAwait(false); return lease?.Length ?? -1; } [DisplayName("SET")] - private Task Set(IDatabaseAsync client, Func flush) => - Pipeline(() => client.StringSetAsync(_getSetKey, _payload), flush); + private ValueTask Set(IDatabaseAsync client) => client.StringSetAsync(_getSetKey, _payload).AsValueTask(); - private Task GetInit(IDatabaseAsync client) => client.StringSetAsync(_getSetKey, _payload); + private ValueTask GetInit(IDatabaseAsync client) => + client.StringSetAsync(_getSetKey, _payload).AsUntypedValueTask(); - private Task PingInline(IDatabaseAsync client, Func flush) => Pipeline(() => client.PingAsync(), flush); + private ValueTask PingInline(IDatabaseAsync client) => client.PingAsync().AsValueTask(); [DisplayName("PING_BULK")] - private Task PingBulk(IDatabaseAsync client, Func flush) => Pipeline(() => client.PingAsync(), flush); + private ValueTask PingBulk(IDatabaseAsync client) => client.PingAsync().AsValueTask(); [DisplayName("INCR")] - private Task Incr(IDatabaseAsync client, Func flush) => - Pipeline(() => client.StringIncrementAsync(_counterKey), flush); + private ValueTask Incr(IDatabaseAsync client) => client.StringIncrementAsync(_counterKey).AsValueTask(); [DisplayName("HSET")] - private Task HSet(IDatabaseAsync client, Func flush) => - Pipeline(() => client.HashSetAsync(_hashKey, "element:__rand_int__", _payload), flush); + private ValueTask HSet(IDatabaseAsync client) => + client.HashSetAsync(_hashKey, "element:__rand_int__", _payload).AsValueTask(); [DisplayName("SADD")] - private Task SAdd(IDatabaseAsync client, Func flush) => - Pipeline(() => client.SetAddAsync(_setKey, "element:__rand_int__"), flush); + private ValueTask SAdd(IDatabaseAsync client) => + client.SetAddAsync(_setKey, "element:__rand_int__").AsValueTask(); [DisplayName("LPUSH")] - private Task LPush(IDatabaseAsync client, Func flush) => - Pipeline(() => client.ListLeftPushAsync(_listKey, _payload), flush); + private ValueTask LPush(IDatabaseAsync client) => client.ListLeftPushAsync(_listKey, _payload).AsValueTask(); [DisplayName("RPUSH")] - private Task RPush(IDatabaseAsync client, Func flush) => - Pipeline(() => client.ListRightPushAsync(_listKey, _payload), flush); + private ValueTask RPush(IDatabaseAsync client) => client.ListRightPushAsync(_listKey, _payload).AsValueTask(); [DisplayName("LPOP")] - private Task LPop(IDatabaseAsync client, Func flush) => - Pipeline(() => client.ListLeftPopAsync(_listKey), flush); + private ValueTask LPop(IDatabaseAsync client) => client.ListLeftPopAsync(_listKey).AsValueTask(); [DisplayName("RPOP")] - private Task RPop(IDatabaseAsync client, Func flush) => - Pipeline(() => client.ListRightPopAsync(_listKey), flush); + private ValueTask RPop(IDatabaseAsync client) => client.ListRightPopAsync(_listKey).AsValueTask(); - private Task LPopInit(IDatabaseAsync client) => client.ListLeftPushAsync(_listKey, _payload); + private ValueTask LPopInit(IDatabaseAsync client) => + client.ListLeftPushAsync(_listKey, _payload).AsUntypedValueTask(); [DisplayName("SPOP")] - private Task SPop(IDatabaseAsync client, Func flush) => Pipeline(() => client.SetPopAsync(_setKey), flush); - private Task SPopInit(IDatabaseAsync client) => client.SetAddAsync(_setKey, "element:__rand_int__"); + private ValueTask SPop(IDatabaseAsync client) => client.SetPopAsync(_setKey).AsValueTask(); + + private ValueTask SPopInit(IDatabaseAsync client) => + client.SetAddAsync(_setKey, "element:__rand_int__").AsUntypedValueTask(); [DisplayName("ZADD")] - private Task ZAdd(IDatabaseAsync client, Func flush) => - Pipeline(() => client.SortedSetAddAsync(_sortedSetKey, "element:__rand_int__", 0), flush); + private ValueTask ZAdd(IDatabaseAsync client) => + client.SortedSetAddAsync(_sortedSetKey, "element:__rand_int__", 0).AsValueTask(); [DisplayName("ZPOPMIN")] - private Task ZPopMin(IDatabaseAsync client, Func flush) => - Pipeline(() => CountAsync(client.SortedSetPopAsync(_sortedSetKey, 1)), flush); + private ValueTask ZPopMin(IDatabaseAsync client) => CountAsync(client.SortedSetPopAsync(_sortedSetKey, 1)); - private Task ZPopMinInit(IDatabaseAsync client) => client.SortedSetAddAsync(_sortedSetKey, "element:__rand_int__", 0); + private async ValueTask ZPopMinInit(IDatabaseAsync client) + { + int ops = TotalOperations; + var rand = new Random(); + for (int i = 0; i < ops; i++) + { + await client.SortedSetAddAsync(_sortedSetKey, "element:__rand_int__", (rand.NextDouble() * 2000) - 1000) + .ConfigureAwait(false); + } + } [DisplayName("MSET")] - private Task MSet(IDatabaseAsync client, Func flush) => Pipeline(() => client.StringSetAsync(_pairs), flush); + private ValueTask MSet(IDatabaseAsync client) => client.StringSetAsync(_pairs).AsValueTask(); [DisplayName("XADD")] - private Task XAdd(IDatabaseAsync client, Func flush) => - Pipeline(() => client.StreamAddAsync(_streamKey, "myfield", _payload), flush); + private ValueTask XAdd(IDatabaseAsync client) => + client.StreamAddAsync(_streamKey, "myfield", _payload).AsValueTask(); [DisplayName("LRANGE_100")] - private Task LRange100(IDatabaseAsync client, Func flush) => - Pipeline(() => CountAsync(client.ListRangeAsync(_listKey, 0, 99)), flush); + private ValueTask LRange100(IDatabaseAsync client) => CountAsync(client.ListRangeAsync(_listKey, 0, 99)); [DisplayName("LRANGE_300")] - private Task LRange300(IDatabaseAsync client, Func flush) => - Pipeline(() => CountAsync(client.ListRangeAsync(_listKey, 0, 299)), flush); + private ValueTask LRange300(IDatabaseAsync client) => CountAsync(client.ListRangeAsync(_listKey, 0, 299)); [DisplayName("LRANGE_500")] - private Task LRange500(IDatabaseAsync client, Func flush) => - Pipeline(() => CountAsync(client.ListRangeAsync(_listKey, 0, 499)), flush); + private ValueTask LRange500(IDatabaseAsync client) => CountAsync(client.ListRangeAsync(_listKey, 0, 499)); [DisplayName("LRANGE_600")] - private Task LRange600(IDatabaseAsync client, Func flush) => - Pipeline(() => CountAsync(client.ListRangeAsync(_listKey, 0, 599)), flush); + private ValueTask LRange600(IDatabaseAsync client) => + CountAsync(client.ListRangeAsync(_listKey, 0, 599)); - private static Task CountAsync(Task task) => - task.ContinueWith(t => t.Result.Length, TaskContinuationOptions.ExecuteSynchronously); + private static ValueTask CountAsync(Task task) => task.ContinueWith( + t => t.Result.Length, TaskContinuationOptions.ExecuteSynchronously).AsValueTask(); - private async Task LRangeInit(IDatabaseAsync client) + private async ValueTask LRangeInit(IDatabaseAsync client) { var ops = TotalOperations; for (int i = 0; i < ops; i++) @@ -188,3 +188,22 @@ private async Task LRangeInit(IDatabaseAsync client) } } } + +internal static class TaskExtensions +{ + public static ValueTask AsValueTask(this Task task) => new(task); + public static ValueTask AsUntypedValueTask(this Task task) => new(task); + public static ValueTask AsValueTask(this Task task) => new(task); + + public static ValueTask AsUntypedValueTask(this ValueTask task) + { + if (!task.IsCompleted) return Awaited(task); + task.GetAwaiter().GetResult(); + return default; + + static async ValueTask Awaited(ValueTask task) + { + await task.ConfigureAwait(false); + } + } +} diff --git a/src/RESPite/Connections/Internal/BasicBatchConnection.cs b/src/RESPite/Connections/Internal/BasicBatchConnection.cs index 6b0411dd0..39f29f16a 100644 --- a/src/RESPite/Connections/Internal/BasicBatchConnection.cs +++ b/src/RESPite/Connections/Internal/BasicBatchConnection.cs @@ -1,6 +1,8 @@ using System.Buffers; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using RESPite.Internal; namespace RESPite.Connections.Internal; @@ -10,23 +12,27 @@ namespace RESPite.Connections.Internal; /// internal sealed class BasicBatchConnection : RespBatch { - private readonly List _unsent; + private RespOperation[] _buffer; + private int _count = 0; + + private object SyncLock => this; public BasicBatchConnection(in RespContext context, int sizeHint) : base(context) { // ack: yes, I know we won't spot every recursive+decorated scenario if (Tail is BasicBatchConnection) ThrowNestedBatch(); - _unsent = sizeHint <= 0 ? [] : new List(sizeHint); + _buffer = sizeHint <= 0 ? [] : ArrayPool.Shared.Rent(sizeHint); - static void ThrowNestedBatch() => throw new ArgumentException("Nested batches are not supported", nameof(context)); + static void ThrowNestedBatch() => + throw new ArgumentException("Nested batches are not supported", nameof(context)); } protected override void OnDispose(bool disposing) { if (disposing) { - lock (_unsent) + lock (SyncLock) { /* everyone else checks disposal inside the lock; the base type already marked as disposed, so: @@ -34,101 +40,107 @@ protected override void OnDispose(bool disposing) items will be added */ Debug.Assert(IsDisposed); } -#if NET5_0_OR_GREATER - var span = CollectionsMarshal.AsSpan(_unsent); + + var buffer = _buffer; + _buffer = []; + var span = buffer.AsSpan(0, _count); foreach (var message in span) { message.Message.TrySetException(message.Token, CreateObjectDisposedException()); } -#else - foreach (var message in _unsent) - { - message.TrySetException(CreateObjectDisposedException()); - } -#endif - _unsent.Clear(); + + ArrayPool.Shared.Return(buffer); + ConnectionError = null; } base.OnDispose(disposing); } - internal override int OutstandingOperations + internal override int OutstandingOperations => _count; // always a thread-race, no point locking + + public override void Write(in RespOperation message) { - get + lock (SyncLock) { - lock (_unsent) - { - return _unsent.Count; - } + ThrowIfDisposed(); + EnsureSpaceForLocked(1); + _buffer[_count++] = message; } } - public override void Write(in RespOperation message) + public override void EnsureCapacity(int additionalCount) { - lock (_unsent) + if (additionalCount > _buffer.Length - _count) { - ThrowIfDisposed(); - _unsent.Add(message); + lock (SyncLock) + { + EnsureSpaceForLocked(additionalCount); + } } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureSpaceForLocked(int add) + { + var required = _count + add; + if (_buffer.Length < required) GrowLocked(required); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowLocked(int required) + { + const int maxLength = 0X7FFFFFC7; // not directly available on down-level runtimes :( + var newCapacity = _buffer.Length * 2; // try doubling + if ((uint)newCapacity > maxLength) newCapacity = maxLength; // account for max + if (newCapacity < required) newCapacity = required; // in case doubling wasn't enough + + var newBuffer = ArrayPool.Shared.Rent(newCapacity); + DebugCounters.OnBatchGrow(_count); + _buffer.AsSpan(0, _count).CopyTo(newBuffer); + ArrayPool.Shared.Return(_buffer); + _buffer = newBuffer; + } + internal override void Write(ReadOnlySpan messages) { if (messages.Length != 0) { - lock (_unsent) + lock (SyncLock) { ThrowIfDisposed(); -#if NET8_0_OR_GREATER - _unsent.AddRange(messages); // internally optimized -#else - // two-step; first ensure capacity, then add in loop -#if NET6_0_OR_GREATER - _unsent.EnsureCapacity(_unsent.Count + messages.Length); -#else - var required = _unsent.Count + messages.Length; - if (_unsent.Capacity < required) - { - const int maxLength = 0X7FFFFFC7; // not directly available on down-level runtimes :( - var newCapacity = _unsent.Capacity * 2; // try doubling - if ((uint)newCapacity > maxLength) newCapacity = maxLength; // account for max - if (newCapacity < required) newCapacity = required; // in case doubling wasn't enough - _unsent.Capacity = newCapacity; - } -#endif - foreach (var message in messages) - { - _unsent.Add(message); - } -#endif + EnsureSpaceForLocked(messages.Length); + messages.CopyTo(_buffer.AsSpan(_count)); + _count += messages.Length; } } } private int Flush(out RespOperation[] oversized, out RespOperation single) { - lock (_unsent) + lock (SyncLock) { - var count = _unsent.Count; - switch (count) + var count = _count; + switch (_count) { case 0: + // nothing to do, keep our local buffer oversized = []; single = default; - break; + return 0; case 1: + // but keep our local buffer, just reset the count oversized = []; - single = _unsent[0]; - break; + single = _buffer[0]; + _count = 0; + return 1; default: - oversized = ArrayPool.Shared.Rent(count); + // hand the caller our buffer, and reset + oversized = _buffer; single = default; - _unsent.CopyTo(oversized); - break; + _buffer = []; // we *expect* people to only flush once, so: don't rent a new one + _count = 0; + return count; } - - _unsent.Clear(); - return count; } } diff --git a/src/RESPite/Connections/Internal/DecoratorConnection.cs b/src/RESPite/Connections/Internal/DecoratorConnection.cs index af74f9fad..83ac600b6 100644 --- a/src/RESPite/Connections/Internal/DecoratorConnection.cs +++ b/src/RESPite/Connections/Internal/DecoratorConnection.cs @@ -19,6 +19,11 @@ public DecoratorConnection(in RespContext tail, RespConfiguration? configuration protected override void OnDispose(bool disposing) { + if (PrivateConnectionError is not null) + { + PrivateConnectionError = null; // force unsubscribe + Tail.ConnectionError -= _onConnectionError; + } if (disposing & OwnsConnection) Tail.Dispose(); } diff --git a/src/RESPite/Internal/DebugCounters.cs b/src/RESPite/Internal/DebugCounters.cs index 4e6ff652a..ddb011945 100644 --- a/src/RESPite/Internal/DebugCounters.cs +++ b/src/RESPite/Internal/DebugCounters.cs @@ -28,7 +28,8 @@ internal partial class DebugCounters _tallyBufferRecycledCount, _tallyBufferMessageCount, _tallyBufferPinCount, - _tallyBufferLeakCount; + _tallyBufferLeakCount, + _tallyBatchGrowCount; private static long _tallyWriteBytes, _tallyReadBytes, @@ -37,7 +38,8 @@ internal partial class DebugCounters _tallyBufferMessageBytes, _tallyBufferRecycledBytes, _tallyBufferMaxOutstandingBytes, - _tallyBufferTotalBytes; + _tallyBufferTotalBytes, + _tallyBatchGrowCopyCount; #endif [Conditional("DEBUG")] @@ -49,6 +51,14 @@ internal static void OnRead(int bytes) #endif } + public static void OnBatchGrow(int count) + { +#if DEBUG + Interlocked.Increment(ref _tallyBatchGrowCount); + if (count > 0) Interlocked.Add(ref _tallyBatchGrowCopyCount, count); +#endif + } + public static void OnBatchWrite(int messageCount) { #if DEBUG @@ -258,6 +268,8 @@ private static void EstimatedMovingRangeAverage(ref long field, long value) public int BatchWriteFullPageCount { get; } = Interlocked.Exchange(ref _tallyBatchWriteFullPageCount, 0); public int BatchWritePartialPageCount { get; } = Interlocked.Exchange(ref _tallyBatchWritePartialPageCount, 0); public int BatchWriteMessageCount { get; } = Interlocked.Exchange(ref _tallyBatchWriteMessageCount, 0); + public int BatchGrowCount { get; } = Interlocked.Exchange(ref _tallyBatchGrowCount, 0); + public long BatchGrowCopyCount { get; } = Interlocked.Exchange(ref _tallyBatchGrowCopyCount, 0); public int BufferCreatedCount { get; } = Interlocked.Exchange(ref _tallyBufferCreatedCount, 0); public int BufferRecycledCount { get; } = Interlocked.Exchange(ref _tallyBufferRecycledCount, 0); diff --git a/src/RESPite/RespBatch.cs b/src/RESPite/RespBatch.cs index bdeb0a037..a26b96ef7 100644 --- a/src/RESPite/RespBatch.cs +++ b/src/RESPite/RespBatch.cs @@ -19,4 +19,11 @@ internal override void ThrowIfUnhealthy() } internal override bool IsHealthy => base.IsHealthy & Tail.IsHealthy; + + /// + /// Suggests that the batch should ensure it has enough capacity for the given number of additional operations. + /// Note that this contrasts with , where the number provided + /// is the total number of elements. + /// + public virtual void EnsureCapacity(int additionalCount) { } } From a35e3642cbf6f220be97102dbf999646ef1d77a1 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 2 Sep 2025 16:53:21 +0100 Subject: [PATCH 030/108] machinery for block buffer --- .../Internal/BasicBatchConnection.cs | 152 +----------- .../Internal/BufferingBatchConnection.cs | 146 +++++++++++ .../Internal/MergingBatchConnection.cs | 80 ++++++ src/RESPite/Internal/BlockBuffer.cs | 60 ++++- src/RESPite/Internal/BlockBufferSerializer.cs | 6 +- src/RESPite/Internal/IRespMessage.cs | 27 -- src/RESPite/Internal/RespMessageBase.cs | 230 +----------------- src/RESPite/Internal/RespMessageBaseT.cs | 159 ++++++++++++ src/RESPite/Internal/RespMultiMessage.cs | 76 ++++++ ...ped_Stateful.cs => RespStatefulMessage.cs} | 10 +- ...eBase_Typed.cs => RespStatelessMessage.cs} | 10 +- .../SynchronizedBlockBufferSerializer.cs | 81 +++++- src/RESPite/RESPite.csproj | 33 ++- src/RESPite/RespBatch.cs | 5 + src/RESPite/RespContextExtensions.cs | 6 +- src/RESPite/RespOperation.cs | 6 +- 16 files changed, 661 insertions(+), 426 deletions(-) create mode 100644 src/RESPite/Connections/Internal/BufferingBatchConnection.cs create mode 100644 src/RESPite/Connections/Internal/MergingBatchConnection.cs delete mode 100644 src/RESPite/Internal/IRespMessage.cs create mode 100644 src/RESPite/Internal/RespMessageBaseT.cs create mode 100644 src/RESPite/Internal/RespMultiMessage.cs rename src/RESPite/Internal/{RespMessageBase_Typed_Stateful.cs => RespStatefulMessage.cs} (62%) rename src/RESPite/Internal/{RespMessageBase_Typed.cs => RespStatelessMessage.cs} (62%) diff --git a/src/RESPite/Connections/Internal/BasicBatchConnection.cs b/src/RESPite/Connections/Internal/BasicBatchConnection.cs index 39f29f16a..6490acf51 100644 --- a/src/RESPite/Connections/Internal/BasicBatchConnection.cs +++ b/src/RESPite/Connections/Internal/BasicBatchConnection.cs @@ -1,8 +1,4 @@ using System.Buffers; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using RESPite.Internal; namespace RESPite.Connections.Internal; @@ -10,142 +6,8 @@ namespace RESPite.Connections.Internal; /// Holds basic RespOperation, queue and release - turns /// multiple send/send-many calls into a single send-many call. /// -internal sealed class BasicBatchConnection : RespBatch +internal sealed class BasicBatchConnection(in RespContext context, int sizeHint) : BufferingBatchConnection(context, sizeHint) { - private RespOperation[] _buffer; - private int _count = 0; - - private object SyncLock => this; - - public BasicBatchConnection(in RespContext context, int sizeHint) : base(context) - { - // ack: yes, I know we won't spot every recursive+decorated scenario - if (Tail is BasicBatchConnection) ThrowNestedBatch(); - - _buffer = sizeHint <= 0 ? [] : ArrayPool.Shared.Rent(sizeHint); - - static void ThrowNestedBatch() => - throw new ArgumentException("Nested batches are not supported", nameof(context)); - } - - protected override void OnDispose(bool disposing) - { - if (disposing) - { - lock (SyncLock) - { - /* everyone else checks disposal inside the lock; - the base type already marked as disposed, so: - once we're past this point, we can be sure that no more - items will be added */ - Debug.Assert(IsDisposed); - } - - var buffer = _buffer; - _buffer = []; - var span = buffer.AsSpan(0, _count); - foreach (var message in span) - { - message.Message.TrySetException(message.Token, CreateObjectDisposedException()); - } - - ArrayPool.Shared.Return(buffer); - ConnectionError = null; - } - - base.OnDispose(disposing); - } - - internal override int OutstandingOperations => _count; // always a thread-race, no point locking - - public override void Write(in RespOperation message) - { - lock (SyncLock) - { - ThrowIfDisposed(); - EnsureSpaceForLocked(1); - _buffer[_count++] = message; - } - } - - public override void EnsureCapacity(int additionalCount) - { - if (additionalCount > _buffer.Length - _count) - { - lock (SyncLock) - { - EnsureSpaceForLocked(additionalCount); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void EnsureSpaceForLocked(int add) - { - var required = _count + add; - if (_buffer.Length < required) GrowLocked(required); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private void GrowLocked(int required) - { - const int maxLength = 0X7FFFFFC7; // not directly available on down-level runtimes :( - var newCapacity = _buffer.Length * 2; // try doubling - if ((uint)newCapacity > maxLength) newCapacity = maxLength; // account for max - if (newCapacity < required) newCapacity = required; // in case doubling wasn't enough - - var newBuffer = ArrayPool.Shared.Rent(newCapacity); - DebugCounters.OnBatchGrow(_count); - _buffer.AsSpan(0, _count).CopyTo(newBuffer); - ArrayPool.Shared.Return(_buffer); - _buffer = newBuffer; - } - - internal override void Write(ReadOnlySpan messages) - { - if (messages.Length != 0) - { - lock (SyncLock) - { - ThrowIfDisposed(); - EnsureSpaceForLocked(messages.Length); - messages.CopyTo(_buffer.AsSpan(_count)); - _count += messages.Length; - } - } - } - - private int Flush(out RespOperation[] oversized, out RespOperation single) - { - lock (SyncLock) - { - var count = _count; - switch (_count) - { - case 0: - // nothing to do, keep our local buffer - oversized = []; - single = default; - return 0; - case 1: - // but keep our local buffer, just reset the count - oversized = []; - single = _buffer[0]; - _count = 0; - return 1; - default: - // hand the caller our buffer, and reset - oversized = _buffer; - single = default; - _buffer = []; // we *expect* people to only flush once, so: don't rent a new one - _count = 0; - return count; - } - } - } - - public override event EventHandler? ConnectionError; - public override Task FlushAsync() { try @@ -160,7 +22,7 @@ public override Task FlushAsync() } catch (Exception ex) { - OnConnectionError(ConnectionError, ex); + OnConnectionError(ex); throw; } @@ -179,14 +41,6 @@ static async Task SendAndRecycleAsync(RespConnection tail, RespOperation[] overs } } - private static void TrySetException(ReadOnlySpan messages, Exception ex) - { - foreach (var message in messages) - { - message.Message.TrySetException(message.Token, ex); - } - } - public override void Flush() { string operation = nameof(Flush); @@ -208,7 +62,7 @@ public override void Flush() } catch (Exception ex) { - OnConnectionError(ConnectionError, ex, operation); + OnConnectionError(ex, operation); throw; } diff --git a/src/RESPite/Connections/Internal/BufferingBatchConnection.cs b/src/RESPite/Connections/Internal/BufferingBatchConnection.cs new file mode 100644 index 000000000..a176da2c1 --- /dev/null +++ b/src/RESPite/Connections/Internal/BufferingBatchConnection.cs @@ -0,0 +1,146 @@ +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using RESPite.Internal; + +namespace RESPite.Connections.Internal; + +/// +/// Collects messages into a buffer, and then flushes them all at once. Subclass defines how to flush. +/// +internal abstract class BufferingBatchConnection(in RespContext context, int sizeHint) : RespBatch(context) +{ + private RespOperation[] _buffer = sizeHint <= 0 ? [] : ArrayPool.Shared.Rent(sizeHint); + private int _count = 0; + + protected object SyncLock => this; + + protected override void OnDispose(bool disposing) + { + if (disposing) + { + lock (SyncLock) + { + /* everyone else checks disposal inside the lock; + the base type already marked as disposed, so: + once we're past this point, we can be sure that no more + items will be added */ + Debug.Assert(IsDisposed); + } + + var buffer = _buffer; + _buffer = []; + var span = buffer.AsSpan(0, _count); + foreach (var message in span) + { + message.Message.TrySetException(message.Token, CreateObjectDisposedException()); + } + + ArrayPool.Shared.Return(buffer); + ConnectionError = null; + } + + base.OnDispose(disposing); + } + + internal override int OutstandingOperations => _count; // always a thread-race, no point locking + + public override void Write(in RespOperation message) + { + lock (SyncLock) + { + ThrowIfDisposed(); + EnsureSpaceForLocked(1); + _buffer[_count++] = message; + } + } + + public override void EnsureCapacity(int additionalCount) + { + if (additionalCount > _buffer.Length - _count) + { + lock (SyncLock) + { + EnsureSpaceForLocked(additionalCount); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureSpaceForLocked(int add) + { + var required = _count + add; + if (_buffer.Length < required) GrowLocked(required); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowLocked(int required) + { + const int maxLength = 0X7FFFFFC7; // not directly available on down-level runtimes :( + var newCapacity = _buffer.Length * 2; // try doubling + if ((uint)newCapacity > maxLength) newCapacity = maxLength; // account for max + if (newCapacity < required) newCapacity = required; // in case doubling wasn't enough + + var newBuffer = ArrayPool.Shared.Rent(newCapacity); + DebugCounters.OnBatchGrow(_count); + _buffer.AsSpan(0, _count).CopyTo(newBuffer); + ArrayPool.Shared.Return(_buffer); + _buffer = newBuffer; + } + + internal override void Write(ReadOnlySpan messages) + { + if (messages.Length != 0) + { + lock (SyncLock) + { + ThrowIfDisposed(); + EnsureSpaceForLocked(messages.Length); + messages.CopyTo(_buffer.AsSpan(_count)); + _count += messages.Length; + } + } + } + + protected int Flush(out RespOperation[] oversized, out RespOperation single) + { + lock (SyncLock) + { + var count = _count; + switch (_count) + { + case 0: + // nothing to do, keep our local buffer + oversized = []; + single = default; + return 0; + case 1: + // but keep our local buffer, just reset the count + oversized = []; + single = _buffer[0]; + _count = 0; + return 1; + default: + // hand the caller our buffer, and reset + oversized = _buffer; + single = default; + _buffer = []; // we *expect* people to only flush once, so: don't rent a new one + _count = 0; + return count; + } + } + } + + protected void OnConnectionError(Exception exception, [CallerMemberName] string operation = "") + => OnConnectionError(ConnectionError, exception, operation); + + public override event EventHandler? ConnectionError; + + protected static void TrySetException(ReadOnlySpan messages, Exception ex) + { + foreach (var message in messages) + { + message.Message.TrySetException(message.Token, ex); + } + } +} diff --git a/src/RESPite/Connections/Internal/MergingBatchConnection.cs b/src/RESPite/Connections/Internal/MergingBatchConnection.cs new file mode 100644 index 000000000..fe8e91914 --- /dev/null +++ b/src/RESPite/Connections/Internal/MergingBatchConnection.cs @@ -0,0 +1,80 @@ +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using RESPite.Internal; +using RESPite.Messages; + +namespace RESPite.Connections.Internal; + +/// +/// Holds basic RespOperation, queue and release - turns +/// multiple send calls into a single multi-message send. +/// +internal sealed class MergingBatchConnection(in RespContext context, int sizeHint) : BufferingBatchConnection(context, sizeHint) +{ + // Collate new messages in a batch-specific buffer, rather than the usual thread-local one; this means + // that all the messages will be in contiguous memory. + private readonly BlockBufferSerializer _serializer = BlockBufferSerializer.Create(retainChain: true); + + protected override void OnDispose(bool disposing) + { + if (disposing) + { + _serializer.Clear(); + } + + base.OnDispose(disposing); + } + + internal override BlockBufferSerializer Serializer + { + get + { + ThrowIfDisposed(); + return _serializer; + } + } + + private bool Flush(out RespOperation single) + { + lock (SyncLock) + { + var payload = _serializer.Flush(); + var count = Flush(out var oversized, out single); + switch (count) + { + case 0: + Debug.Assert(payload.IsEmpty); + return false; + case 1: + Debug.Assert(!payload.IsEmpty); + // send as a single-message we don't need the extra add-ref on the entire payload + BlockBufferSerializer.BlockBuffer.Release(in payload); + + return true; + default: + Debug.Assert(!payload.IsEmpty); + var msg = RespMultiMessage.Get(oversized, count); + msg.Init(payload, Context.CancellationToken); + single = new(msg); + return true; + } + } + } + + public override Task FlushAsync() + { + return Flush(out var single) + ? Tail.WriteAsync(single) + : Task.CompletedTask; + } + + public override void Flush() + { + if (Flush(out var single)) + { + Tail.Write(single); + } + } +} diff --git a/src/RESPite/Internal/BlockBuffer.cs b/src/RESPite/Internal/BlockBuffer.cs index 16d1e6969..752d74c8d 100644 --- a/src/RESPite/Internal/BlockBuffer.cs +++ b/src/RESPite/Internal/BlockBuffer.cs @@ -48,6 +48,12 @@ public void Release() if (Interlocked.Decrement(ref _refCount) <= 0) Recycle(); } + public void AddRef() + { + if (!TryAddRef()) Throw(); + static void Throw() => throw new ObjectDisposedException(nameof(BlockBuffer)); + } + public bool TryAddRef() { int count; @@ -148,7 +154,7 @@ private static BlockBuffer GetBufferSlow(BlockBufferSerializer parent, int minBy // the ~emperor~ buffer is dead; long live the ~emperor~ buffer parent.Buffer = newBuffer; - buffer.MarkComplete(); + buffer.MarkComplete(parent); return newBuffer; } @@ -158,16 +164,33 @@ public static void Clear(BlockBufferSerializer parent) if (parent.Buffer is { } buffer) { parent.Buffer = null; - buffer.MarkComplete(); + buffer.MarkComplete(parent); } } - private void MarkComplete() + public static ReadOnlyMemory RetainCurrent(BlockBufferSerializer parent) + { + if (parent.Buffer is { } buffer && buffer._finalizedOffset != 0) + { + parent.Buffer = null; + buffer.AddRef(); + return buffer.CreateMemory(0, buffer._finalizedOffset); + } + // nothing useful to detach! + return default; + } + + private void MarkComplete(BlockBufferSerializer parent) { // record that the old buffer no longer logically has any non-committed bytes (mostly just for ToString()) _writeOffset = _finalizedOffset; Debug.Assert(IsNonCommittedEmpty); - Release(); // decrement the observer + + // see if the caller wants to take ownership of the segment + if (_finalizedOffset != 0 && !parent.ClaimSegment(CreateMemory(0, _finalizedOffset))) + { + Release(); // decrement the observer + } #if DEBUG DebugCounters.OnBufferCompleted(_finalizedCount, _finalizedOffset); #endif @@ -285,5 +308,34 @@ protected override bool TryGetArray(out ArraySegment segment) segment = new ArraySegment(_array); return true; } + + internal static void Release(in ReadOnlySequence request) + { + if (request.IsSingleSegment) + { + if (MemoryMarshal.TryGetMemoryManager( + request.First, out var block)) + { + block.Release(); + } + } + else + { + ReleaseMultiBlock(in request); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ReleaseMultiBlock(in ReadOnlySequence request) + { + foreach (var segment in request) + { + if (MemoryMarshal.TryGetMemoryManager( + segment, out var block)) + { + block.Release(); + } + } + } + } } } diff --git a/src/RESPite/Internal/BlockBufferSerializer.cs b/src/RESPite/Internal/BlockBufferSerializer.cs index 7216919d1..22ff5361f 100644 --- a/src/RESPite/Internal/BlockBufferSerializer.cs +++ b/src/RESPite/Internal/BlockBufferSerializer.cs @@ -27,7 +27,9 @@ internal abstract partial class BlockBufferSerializer(ArrayPool? arrayPool void IBufferWriter.Advance(int count) => BlockBuffer.Advance(this, count); - public void Clear() => BlockBuffer.Clear(this); + public virtual void Clear() => BlockBuffer.Clear(this); + + internal virtual ReadOnlySequence Flush() => throw new NotSupportedException(); public virtual ReadOnlyMemory Serialize( RespCommandMap? commandMap, @@ -53,6 +55,8 @@ public virtual ReadOnlyMemory Serialize( } } + protected virtual bool ClaimSegment(ReadOnlyMemory segment) => false; + #if DEBUG private int _countAdded, _countRecycled, _countLeaked, _countMessages; private long _countMessageBytes; diff --git a/src/RESPite/Internal/IRespMessage.cs b/src/RESPite/Internal/IRespMessage.cs deleted file mode 100644 index edc6484ee..000000000 --- a/src/RESPite/Internal/IRespMessage.cs +++ /dev/null @@ -1,27 +0,0 @@ -// using System.Buffers; -// using System.Threading.Tasks.Sources; -// -// namespace RESPite.Internal; -// -// internal interface IRespMessage : IValueTaskSource -// { -// void Wait(short token, TimeSpan timeout); -// void TrySetCanceled(); // only intended for use from cancellation callbacks -// bool TrySetCanceled(short token, CancellationToken cancellationToken = default); -// bool TrySetException(short token, Exception exception); -// bool TrySetResult(short token, scoped ReadOnlySpan response); -// bool TrySetResult(short token, in ReadOnlySequence response); -// bool TryReserveRequest(short token, out ReadOnlySequence payload, bool recordSent = true); -// void ReleaseRequest(); -// bool AllowInlineParsing { get; } -// short Token { get; } -// ref readonly CancellationToken CancellationToken { get; } -// int MessageCount { get; } -// bool IsSent(short token); -// -// void OnCompletedWithNotSentDetection( -// Action continuation, -// object? state, -// short token, -// ValueTaskSourceOnCompletedFlags flags); -// } diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs index 02eadccbb..50f824210 100644 --- a/src/RESPite/Internal/RespMessageBase.cs +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -13,7 +13,7 @@ internal abstract class RespMessageBase : IValueTaskSource private CancellationToken _cancellationToken; private CancellationTokenRegistration _cancellationTokenRegistration; - private int _requestRefCount, _flags, _messageCount; + private int _requestRefCount, _flags; private ReadOnlySequence _request; public ref readonly CancellationToken CancellationToken => ref _cancellationToken; @@ -28,7 +28,7 @@ protected const int Flag_Doomed = 1 << 8; // something went wrong, do not recycle protected int Flags => Volatile.Read(ref _flags); - public int MessageCount => _messageCount; + public virtual int MessageCount => 1; protected void InitParser(object? parser) { @@ -49,7 +49,7 @@ protected void InitParser(object? parser) public bool AllowInlineParsing => HasFlag(Flag_InlineParser); - public bool TrySetResult(short token, scoped ReadOnlySpan response) + public bool TrySetResult(short token, ref RespReader reader) { if (HasFlag(Flag_OutcomeKnown) | Token != token) return false; var flags = _flags & (Flag_MetadataParser | Flag_Parser); @@ -59,7 +59,6 @@ public bool TrySetResult(short token, scoped ReadOnlySpan response) case Flag_Parser | Flag_MetadataParser: try { - RespReader reader = new(response); if ((flags & Flag_MetadataParser) == 0) { reader.MoveNext(); @@ -76,31 +75,16 @@ public bool TrySetResult(short token, scoped ReadOnlySpan response) } } - public bool TrySetResult(short token, in ReadOnlySequence response) + public bool TrySetResult(short token, scoped ReadOnlySpan response) { - if (HasFlag(Flag_OutcomeKnown) | Token != token) return false; - var flags = _flags & (Flag_MetadataParser | Flag_Parser); - switch (flags) - { - case Flag_Parser: - case Flag_Parser | Flag_MetadataParser: - try - { - RespReader reader = new(response); - if ((flags & Flag_MetadataParser) == 0) - { - reader.MoveNext(); - } + RespReader reader = new(response); + return TrySetResult(token, ref reader); + } - return TrySetResultPrecheckedToken(ref reader); - } - catch (Exception ex) - { - return TrySetExceptionPrecheckedToken(ex); - } - default: - return TrySetDefaultResultPrecheckedToken(); - } + public bool TrySetResult(short token, in ReadOnlySequence response) + { + RespReader reader = new(response); + return TrySetResult(token, ref reader); } protected abstract bool TrySetResultPrecheckedToken(ref RespReader reader); @@ -161,7 +145,6 @@ public void Init( Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); _request = new(request); _requestRefCount = 1; - _messageCount = 1; if (cancellationToken.CanBeCanceled) { _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellationToken); @@ -170,13 +153,11 @@ public void Init( public void Init( ReadOnlySequence request, - int messageCount, CancellationToken cancellationToken) { Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); _request = request; _requestRefCount = 1; - _messageCount = messageCount; if (cancellationToken.CanBeCanceled) { _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellationToken); @@ -248,7 +229,8 @@ static void ThrowReleased() => { if (oldCount == 1) // we were the last one; recycle { - Release(ref _request); + BlockBufferSerializer.BlockBuffer.Release(in _request); + _request = default; } return true; @@ -256,37 +238,6 @@ static void ThrowReleased() => } } - private static void Release(ref ReadOnlySequence request) - { - if (request.IsSingleSegment) - { - if (MemoryMarshal.TryGetMemoryManager( - request.First, out var block)) - { - block.Release(); - } - } - else - { - ReleaseMultiBlock(ref request); - } - - request = default; - - [MethodImpl(MethodImplOptions.NoInlining)] - static void ReleaseMultiBlock(ref ReadOnlySequence request) - { - foreach (var segment in request) - { - if (MemoryMarshal.TryGetMemoryManager( - segment, out var block)) - { - block.Release(); - } - } - } - } - [MethodImpl(MethodImplOptions.NoInlining)] protected void ThrowNotSent(short token) { @@ -403,158 +354,3 @@ public abstract void OnCompletedWithNotSentDetection( short token, ValueTaskSourceOnCompletedFlags flags); } - -internal abstract class RespMessageBase : RespMessageBase, IValueTaskSource -{ - private ManualResetValueTaskSourceCore _asyncCore; - - protected abstract TResponse Parse(ref RespReader reader); - - public override short Token => _asyncCore.Version; - - private protected override ValueTaskSourceStatus OwnStatus => _asyncCore.GetStatus(_asyncCore.Version); - - /* asking about the status too early is usually a very bad sign that they're doing - something like awaiting a message in a transaction that hasn't been sent */ - public override ValueTaskSourceStatus GetStatus(short token) - => _asyncCore.GetStatus(token); - - private protected override void CheckToken(short token) - { - if (token != _asyncCore.Version) // use cheap test - { - _ = _asyncCore.GetStatus(token); // get consistent exception message - } - } - - // this is used from Task/ValueTask; we can't avoid that - in theory - // we *coiuld* sort of make it work for ValueTask, but if anyone - // calls .AsTask() on it, it would fail - public override void OnCompleted( - Action continuation, - object? state, - short token, - ValueTaskSourceOnCompletedFlags flags) - { - CheckToken(token); - SetFlag(Flag_NoPulse); // async doesn't need to be pulsed - _asyncCore.OnCompleted(continuation, state, token, flags); - } - - public override void OnCompletedWithNotSentDetection( - Action continuation, - object? state, - short token, - ValueTaskSourceOnCompletedFlags flags) - { - CheckToken(token); - if (!HasFlag(Flag_Sent)) SetNotSentAsync(token); - SetFlag(Flag_NoPulse); // async doesn't need to be pulsed - _asyncCore.OnCompleted(continuation, state, token, flags); - } - - private protected override void SetRunContinuationsAsynchronously(bool value) - => _asyncCore.RunContinuationsAsynchronously = value; - - public override void GetResultVoid(short token) => _ = GetResult(token); - public override void WaitVoid(short token, TimeSpan timeout) => _ = Wait(token, timeout); - - public TResponse Wait(short token, TimeSpan timeout) - { - switch (Flags & (Flag_Complete | Flag_Sent)) - { - case Flag_Sent: // this is the normal case - break; - case Flag_Complete | Flag_Sent: // already complete - return GetResult(token); - default: - ThrowNotSent(token); // always throws - break; - } - - bool isTimeout = false; - CheckToken(token); - lock (this) - { - switch (Flags & (Flag_Complete | Flag_NoPulse)) - { - case Flag_NoPulse | Flag_Complete: - case Flag_Complete: - break; // fine, we're complete - case 0: - // THIS IS OUR EXPECTED BRANCH; not complete, and will pulse - if (timeout == TimeSpan.Zero) - { - Monitor.Wait(this); - } - else if (!Monitor.Wait(this, timeout)) - { - isTimeout = true; - SetFlag(Flag_NoPulse); // no point in being woken, we're exiting - } - - break; - case Flag_NoPulse: - ThrowWillNotPulse(); - break; - } - } - - UnregisterCancellation(); - if (isTimeout) TrySetTimeoutPrecheckedToken(); - - return GetResult(token); - - static void ThrowWillNotPulse() => throw new InvalidOperationException( - "This operation cannot be waited because it entered async/await mode - most likely by calling AsTask()"); - } - - private bool TrySetResultPrecheckedToken(TResponse response) - { - if (!TrySetOutcomeKnownPrecheckedToken(true)) return false; - - _asyncCore.SetResult(response); - SetFullyComplete(success: true); - return true; - } - - private TResponse ThrowFailure(short token) - { - try - { - return _asyncCore.GetResult(token); - } - finally - { - // we're not recycling; this is for GC reasons only - Reset(false); - } - } - - public TResponse GetResult(short token) - { - // failure uses some try/catch logic, let's put that to one side - if (HasFlag(Flag_Doomed)) return ThrowFailure(token); - var result = _asyncCore.GetResult(token); - /* - If we get here, we're successful; increment "version"/"token" *immediately*. Technically - we could defer to when it is reused (after recycling), but then repeated calls will appear - to work for a while, which might lead to undetected problems in local builds (without much concurrency), - and we'd rather make people know that there's a problem immediately. This also means that any - continuation primitives (callback/state) are available for GC. - */ - Reset(true); - return result; - } - - private protected override void SetExceptionPreChecked(Exception exception) - => _asyncCore.SetException(exception); - - protected override bool TrySetResultPrecheckedToken(ref RespReader reader) => - TrySetResultPrecheckedToken(Parse(ref reader)); - - protected override bool TrySetDefaultResultPrecheckedToken() - => TrySetResultPrecheckedToken(default!); - - protected override void NextToken() => _asyncCore.Reset(); -} diff --git a/src/RESPite/Internal/RespMessageBaseT.cs b/src/RESPite/Internal/RespMessageBaseT.cs new file mode 100644 index 000000000..f4e90e6f5 --- /dev/null +++ b/src/RESPite/Internal/RespMessageBaseT.cs @@ -0,0 +1,159 @@ +using System.Threading.Tasks.Sources; +using RESPite.Messages; + +namespace RESPite.Internal; + +internal abstract class RespMessageBase : RespMessageBase, IValueTaskSource +{ + private ManualResetValueTaskSourceCore _asyncCore; + + protected abstract TResponse Parse(ref RespReader reader); + + public override short Token => _asyncCore.Version; + + private protected override ValueTaskSourceStatus OwnStatus => _asyncCore.GetStatus(_asyncCore.Version); + + /* asking about the status too early is usually a very bad sign that they're doing + something like awaiting a message in a transaction that hasn't been sent */ + public override ValueTaskSourceStatus GetStatus(short token) + => _asyncCore.GetStatus(token); + + private protected override void CheckToken(short token) + { + if (token != _asyncCore.Version) // use cheap test + { + _ = _asyncCore.GetStatus(token); // get consistent exception message + } + } + + // this is used from Task/ValueTask; we can't avoid that - in theory + // we *coiuld* sort of make it work for ValueTask, but if anyone + // calls .AsTask() on it, it would fail + public override void OnCompleted( + Action continuation, + object? state, + short token, + ValueTaskSourceOnCompletedFlags flags) + { + CheckToken(token); + SetFlag(Flag_NoPulse); // async doesn't need to be pulsed + _asyncCore.OnCompleted(continuation, state, token, flags); + } + + public override void OnCompletedWithNotSentDetection( + Action continuation, + object? state, + short token, + ValueTaskSourceOnCompletedFlags flags) + { + CheckToken(token); + if (!HasFlag(Flag_Sent)) SetNotSentAsync(token); + SetFlag(Flag_NoPulse); // async doesn't need to be pulsed + _asyncCore.OnCompleted(continuation, state, token, flags); + } + + private protected override void SetRunContinuationsAsynchronously(bool value) + => _asyncCore.RunContinuationsAsynchronously = value; + + public override void GetResultVoid(short token) => _ = GetResult(token); + public override void WaitVoid(short token, TimeSpan timeout) => _ = Wait(token, timeout); + + public TResponse Wait(short token, TimeSpan timeout) + { + switch (Flags & (Flag_Complete | Flag_Sent)) + { + case Flag_Sent: // this is the normal case + break; + case Flag_Complete | Flag_Sent: // already complete + return GetResult(token); + default: + ThrowNotSent(token); // always throws + break; + } + + bool isTimeout = false; + CheckToken(token); + lock (this) + { + switch (Flags & (Flag_Complete | Flag_NoPulse)) + { + case Flag_NoPulse | Flag_Complete: + case Flag_Complete: + break; // fine, we're complete + case 0: + // THIS IS OUR EXPECTED BRANCH; not complete, and will pulse + if (timeout == TimeSpan.Zero) + { + Monitor.Wait(this); + } + else if (!Monitor.Wait(this, timeout)) + { + isTimeout = true; + SetFlag(Flag_NoPulse); // no point in being woken, we're exiting + } + + break; + case Flag_NoPulse: + ThrowWillNotPulse(); + break; + } + } + + UnregisterCancellation(); + if (isTimeout) TrySetTimeoutPrecheckedToken(); + + return GetResult(token); + + static void ThrowWillNotPulse() => throw new InvalidOperationException( + "This operation cannot be waited because it entered async/await mode - most likely by calling AsTask()"); + } + + private bool TrySetResultPrecheckedToken(TResponse response) + { + if (!TrySetOutcomeKnownPrecheckedToken(true)) return false; + + _asyncCore.SetResult(response); + SetFullyComplete(success: true); + return true; + } + + private TResponse ThrowFailure(short token) + { + try + { + return _asyncCore.GetResult(token); + } + finally + { + // we're not recycling; this is for GC reasons only + Reset(false); + } + } + + public TResponse GetResult(short token) + { + // failure uses some try/catch logic, let's put that to one side + if (HasFlag(Flag_Doomed)) return ThrowFailure(token); + var result = _asyncCore.GetResult(token); + /* + If we get here, we're successful; increment "version"/"token" *immediately*. Technically + we could defer to when it is reused (after recycling), but then repeated calls will appear + to work for a while, which might lead to undetected problems in local builds (without much concurrency), + and we'd rather make people know that there's a problem immediately. This also means that any + continuation primitives (callback/state) are available for GC. + */ + Reset(true); + return result; + } + + private protected override void SetExceptionPreChecked(Exception exception) + => _asyncCore.SetException(exception); + + protected override bool TrySetResultPrecheckedToken(ref RespReader reader) => + TrySetResultPrecheckedToken(Parse(ref reader)); + + protected override bool TrySetDefaultResultPrecheckedToken() + => TrySetResultPrecheckedToken(default!); + + protected override void NextToken() => _asyncCore.Reset(); +} diff --git a/src/RESPite/Internal/RespMultiMessage.cs b/src/RESPite/Internal/RespMultiMessage.cs new file mode 100644 index 000000000..58f41fb08 --- /dev/null +++ b/src/RESPite/Internal/RespMultiMessage.cs @@ -0,0 +1,76 @@ +using System.Runtime.CompilerServices; +using RESPite.Messages; + +namespace RESPite.Internal; + +internal sealed class RespMultiMessage : RespMessageBase +{ + private RespOperation[] _oversized; + private int _count = 0; + + [ThreadStatic] + // used for object recycling of the async machinery + private static RespMultiMessage? _threadStaticSpare; + + internal static RespMultiMessage Get(RespOperation[] oversized, int count) + { + RespMultiMessage obj = _threadStaticSpare ?? new(); + _threadStaticSpare = null; + obj._oversized = oversized; + obj._count = count; + obj.SetFlag(Flag_Parser | Flag_MetadataParser); + return obj; + } + + protected override void Recycle() => _threadStaticSpare = this; + + private RespMultiMessage() => Unsafe.SkipInit(out _oversized); + + protected override int Parse(ref RespReader reader) + => MultiMessageParser.Default.Parse(new ReadOnlySpan(_oversized, 0, _count), ref reader); + + public override void Reset(bool recycle) + { + _oversized = []; + _count = 0; + base.Reset(recycle); + } + + public override int MessageCount => _count; + + private sealed class MultiMessageParser + { + private MultiMessageParser() { } + public static readonly MultiMessageParser Default = new(); + + public int Parse(ReadOnlySpan operations, ref RespReader reader) + { + int count = 0; + foreach (var op in operations) + { + // we need to give each sub-operation an isolated reader - no bleeding + // data between misbehaving readers (for example, that don't consume + // all of their data) + var clone = reader; // track the start position + if (!reader.TryMoveNext(checkError: false)) ThrowEOF(); // we definitely expected enough for all + + reader.SkipChildren(); // track the end position (for scalar, this is "move past current") + + // now clamp this sub-reader, passing *that* to the operation + clone.TrimToTotal(reader.BytesConsumed); + if (op.Message.TrySetResult(op.Token, ref clone)) + { + // track how many we successfully processed, ignoring things + // that, for example, failed due to cancellation before we got here + count++; + } + } + + if (reader.TryMoveNext()) ThrowTrailing(); + return count; + + static void ThrowTrailing() => throw new FormatException("Unexpected trailing data"); + static void ThrowEOF() => throw new EndOfStreamException(); + } + } +} diff --git a/src/RESPite/Internal/RespMessageBase_Typed_Stateful.cs b/src/RESPite/Internal/RespStatefulMessage.cs similarity index 62% rename from src/RESPite/Internal/RespMessageBase_Typed_Stateful.cs rename to src/RESPite/Internal/RespStatefulMessage.cs index 12d26c143..457b84e43 100644 --- a/src/RESPite/Internal/RespMessageBase_Typed_Stateful.cs +++ b/src/RESPite/Internal/RespStatefulMessage.cs @@ -3,16 +3,16 @@ namespace RESPite.Internal; -internal sealed class RespMessage : RespMessageBase +internal sealed class RespStatefulMessage : RespMessageBase { private TState _state; private IRespParser? _parser; [ThreadStatic] // used for object recycling of the async machinery - private static RespMessage? _threadStaticSpare; - internal static RespMessage Get(in TState state, IRespParser? parser) + private static RespStatefulMessage? _threadStaticSpare; + internal static RespStatefulMessage Get(in TState state, IRespParser? parser) { - RespMessage obj = _threadStaticSpare ?? new(); + RespStatefulMessage obj = _threadStaticSpare ?? new(); _threadStaticSpare = null; obj._state = state; obj._parser = parser; @@ -22,7 +22,7 @@ internal static RespMessage Get(in TState state, IRespParser< protected override void Recycle() => _threadStaticSpare = this; - private RespMessage() => Unsafe.SkipInit(out _state); + private RespStatefulMessage() => Unsafe.SkipInit(out _state); protected override TResponse Parse(ref RespReader reader) => _parser!.Parse(in _state, ref reader); diff --git a/src/RESPite/Internal/RespMessageBase_Typed.cs b/src/RESPite/Internal/RespStatelessMessage.cs similarity index 62% rename from src/RESPite/Internal/RespMessageBase_Typed.cs rename to src/RESPite/Internal/RespStatelessMessage.cs index 392c59f55..e46721df5 100644 --- a/src/RESPite/Internal/RespMessageBase_Typed.cs +++ b/src/RESPite/Internal/RespStatelessMessage.cs @@ -2,16 +2,16 @@ namespace RESPite.Internal; -internal sealed class RespMessage : RespMessageBase +internal sealed class RespStatelessMessage : RespMessageBase { private IRespParser? _parser; [ThreadStatic] // used for object recycling of the async machinery - private static RespMessage? _threadStaticSpare; + private static RespStatelessMessage? _threadStaticSpare; - internal static RespMessage Get(IRespParser? parser) + internal static RespStatelessMessage Get(IRespParser? parser) { - RespMessage obj = _threadStaticSpare ?? new(); + RespStatelessMessage obj = _threadStaticSpare ?? new(); _threadStaticSpare = null; obj._parser = parser; obj.InitParser(parser); @@ -20,7 +20,7 @@ internal static RespMessage Get(IRespParser? parser) protected override void Recycle() => _threadStaticSpare = this; - private RespMessage() { } + private RespStatelessMessage() { } protected override TResponse Parse(ref RespReader reader) => _parser!.Parse(ref reader); diff --git a/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs b/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs index 1ce6982aa..cd5aabafb 100644 --- a/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs +++ b/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs @@ -1,16 +1,20 @@ -using RESPite.Messages; +using System.Buffers; +using RESPite.Messages; namespace RESPite.Internal; internal partial class BlockBufferSerializer { - internal static BlockBufferSerializer Create() => new SynchronizedBlockBufferSerializer(); + internal static BlockBufferSerializer Create(bool retainChain = false) => + new SynchronizedBlockBufferSerializer(retainChain); /// /// Used for things like . /// - private sealed class SynchronizedBlockBufferSerializer : BlockBufferSerializer + private sealed class SynchronizedBlockBufferSerializer(bool retainChain) : BlockBufferSerializer { + private bool _discardDuringClear; + private protected override BlockBuffer? Buffer { get; set; } // simple per-instance auto-prop // use lock-based synchronization @@ -42,5 +46,76 @@ public override ReadOnlyMemory Serialize( } private static readonly TimeSpan LockTimeout = TimeSpan.FromSeconds(5); + + private Segment? _head, _tail; + + protected override bool ClaimSegment(ReadOnlyMemory segment) + { + if (retainChain & !_discardDuringClear) + { + if (_head is null) + { + _head = _tail = new Segment(segment); + } + else + { + _tail = new Segment(segment, _tail); + } + + // note we don't need to increment the ref-count; because of this "true" + return true; + } + + return false; + } + + internal override ReadOnlySequence Flush() + { + if (_head is null) + { + // at worst, single-segment - we can skip the alloc + return new(BlockBuffer.RetainCurrent(this)); + } + + // otherwise, flush everything *keeping the chain* + ClearWithDiscard(discard: false); + ReadOnlySequence seq = new(_head, 0, _tail!, _tail!.Length); + _head = _tail = null; + return seq; + } + + public override void Clear() + { + ClearWithDiscard(discard: true); + _head = _tail = null; + } + + private void ClearWithDiscard(bool discard) + { + try + { + _discardDuringClear = discard; + base.Clear(); + } + finally + { + _discardDuringClear = false; + } + } + + private sealed class Segment : ReadOnlySequenceSegment + { + public Segment(ReadOnlyMemory memory, Segment? previous = null) + { + Memory = memory; + if (previous is not null) + { + previous.Next = this; + RunningIndex = previous.RunningIndex + previous.Length; + } + } + + public int Length => Memory.Length; + } } } diff --git a/src/RESPite/RESPite.csproj b/src/RESPite/RESPite.csproj index c474dcfb3..f3c5ec8a4 100644 --- a/src/RESPite/RESPite.csproj +++ b/src/RESPite/RESPite.csproj @@ -25,15 +25,6 @@ RespOperation.cs - - RespMessage.cs - - - RespMessageBase.cs - - - RespMessageBase.cs - RespReader.cs @@ -58,6 +49,30 @@ BlockBufferSerializer.cs + + RespMessageBase.cs + + + RespMessageBase.cs + + + RespMessageBase.cs + + + BufferingBatchConnection.cs + + + BufferingBatchConnection.cs + + + RespMessageBase.cs + + + DecoratorConnection.cs + + + DecoratorConnection.cs + diff --git a/src/RESPite/RespBatch.cs b/src/RESPite/RespBatch.cs index a26b96ef7..1ca8c8183 100644 --- a/src/RESPite/RespBatch.cs +++ b/src/RESPite/RespBatch.cs @@ -7,6 +7,11 @@ public abstract class RespBatch : RespConnection private protected RespBatch(in RespContext tail) : base(tail) { Tail = tail.Connection; + // ack: yes, I know we won't spot every recursive+decorated scenario + if (Tail is RespBatch) ThrowNestedBatch(); + + static void ThrowNestedBatch() => + throw new ArgumentException("Nested batches are not supported", nameof(tail)); } public abstract Task FlushAsync(); diff --git a/src/RESPite/RespContextExtensions.cs b/src/RESPite/RespContextExtensions.cs index 08d923edf..7ad30bd9d 100644 --- a/src/RESPite/RespContextExtensions.cs +++ b/src/RESPite/RespContextExtensions.cs @@ -197,7 +197,7 @@ public static RespOperation CreateOperation( var conn = context.Connection; var memory = conn.Serializer.Serialize(conn.NonDefaultCommandMap, command, request, formatter); - var msg = RespMessage.Get(parser); + var msg = RespStatelessMessage.Get(parser); msg.Init(memory, context.CancellationToken); return new(msg); } @@ -215,7 +215,7 @@ public static RespOperation CreateOperation( var conn = context.Connection; var memory = conn.Serializer.Serialize(conn.NonDefaultCommandMap, command, request, formatter); - var msg = RespMessage.Get(parser); + var msg = RespStatelessMessage.Get(parser); msg.Init(memory, context.CancellationToken); return new(msg); } @@ -233,7 +233,7 @@ public static RespOperation CreateOperation.Get(in state, parser); + var msg = RespStatefulMessage.Get(in state, parser); msg.Init(memory, context.CancellationToken); return new(msg); } diff --git a/src/RESPite/RespOperation.cs b/src/RESPite/RespOperation.cs index d813767e0..7d1e9c2c8 100644 --- a/src/RESPite/RespOperation.cs +++ b/src/RESPite/RespOperation.cs @@ -173,7 +173,7 @@ public static RespOperation Create( bool sent = true, CancellationToken cancellationToken = default) { - var msg = RespMessage.Get(null); + var msg = RespStatelessMessage.Get(null); msg.Init(sent, cancellationToken); remote = new(msg); return new RespOperation(msg); @@ -190,7 +190,7 @@ public static RespOperation Create( bool sent = true, CancellationToken cancellationToken = default) { - var msg = RespMessage.Get(parser); + var msg = RespStatelessMessage.Get(parser); msg.Init(sent, cancellationToken); remote = new(msg); return new RespOperation(msg); @@ -209,7 +209,7 @@ public static RespOperation Create( bool sent = true, CancellationToken cancellationToken = default) { - var msg = RespMessage.Get(in state, parser); + var msg = RespStatefulMessage.Get(in state, parser); msg.Init(sent, cancellationToken); remote = new(msg); return new RespOperation(msg); From 236da8c039c230ec29ce8d8011972a50f042aafd Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 2 Sep 2025 17:07:58 +0100 Subject: [PATCH 031/108] batch test infrastructure --- .../RespCommandGenerator.cs | 5 +-- tests/RESP.Core.Tests/BatchTests.cs | 32 +++++++++++++++++++ tests/RESP.Core.Tests/RESP.Core.Tests.csproj | 1 + 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tests/RESP.Core.Tests/BatchTests.cs diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index 9639f1c3d..cfbde41ce 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -328,8 +328,9 @@ private void Generate( } StringBuilder NewLine() => sb.AppendLine().Append(' ', Math.Max(indent * 4, 0)); - NewLine().Append("using System;"); - NewLine().Append("using System.Threading.Tasks;"); + NewLine().Append("using global::RESPite;"); + NewLine().Append("using global::System;"); + NewLine().Append("using global::System.Threading.Tasks;"); foreach (var grp in methods.GroupBy(l => (l.Namespace, l.TypeName, l.TypeModifiers))) { NewLine(); diff --git a/tests/RESP.Core.Tests/BatchTests.cs b/tests/RESP.Core.Tests/BatchTests.cs new file mode 100644 index 000000000..50c2043a6 --- /dev/null +++ b/tests/RESP.Core.Tests/BatchTests.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using RESPite; +using Xunit; + +namespace RESP.Core.Tests; + +public partial class BatchTests +{ + [Fact] + public async Task SimpleBatching() + { + // todo: create a manually controlled data pipe + var parentContext = RespContext.Null.WithCancellationToken(TestContext.Current.CancellationToken); + using (var batch = parentContext.CreateBatch()) + { + var ctx = batch.Context; + var a = FooAsync(ctx); + var b = FooAsync(ctx); + var c = FooAsync(ctx); + + await batch.FlushAsync(); + + // todo: supply :1\r\n:2\r\n:3\r\n + Assert.Equal(1, await a); + Assert.Equal(2, await b); + Assert.Equal(3, await c); + } + } + + [RespCommand] + private static partial int Foo(in RespContext ctx); +} diff --git a/tests/RESP.Core.Tests/RESP.Core.Tests.csproj b/tests/RESP.Core.Tests/RESP.Core.Tests.csproj index 977a0f3a6..935187312 100644 --- a/tests/RESP.Core.Tests/RESP.Core.Tests.csproj +++ b/tests/RESP.Core.Tests/RESP.Core.Tests.csproj @@ -19,5 +19,6 @@ + From da3683a018473ccb1868cf0ac1951886fba28bb1 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 3 Sep 2025 00:11:52 +0100 Subject: [PATCH 032/108] test server FTW --- tests/RESP.Core.Tests/BatchTests.cs | 10 ++ tests/RESP.Core.Tests/TestConnection.cs | 194 ++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 tests/RESP.Core.Tests/TestConnection.cs diff --git a/tests/RESP.Core.Tests/BatchTests.cs b/tests/RESP.Core.Tests/BatchTests.cs index 50c2043a6..2c981f6d9 100644 --- a/tests/RESP.Core.Tests/BatchTests.cs +++ b/tests/RESP.Core.Tests/BatchTests.cs @@ -6,6 +6,16 @@ namespace RESP.Core.Tests; public partial class BatchTests { + [Fact] + public async Task TestInfrastructure() + { + using var server = new TestServer(); + var pending = FooAsync(server.Context); + server.AssertSent("*1\r\n$3\r\nfoo\r\n"u8); + Assert.False(pending.IsCompleted); + server.Respond(":42\r\n"u8); + Assert.Equal(42, await pending); + } [Fact] public async Task SimpleBatching() { diff --git a/tests/RESP.Core.Tests/TestConnection.cs b/tests/RESP.Core.Tests/TestConnection.cs new file mode 100644 index 000000000..40aa73ef0 --- /dev/null +++ b/tests/RESP.Core.Tests/TestConnection.cs @@ -0,0 +1,194 @@ +using System; +using System.Buffers; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using RESPite; +using RESPite.Internal; +using Xunit; + +namespace RESP.Core.Tests; + +internal sealed class TestServer : IDisposable +{ + public void Dispose() + { + _stream?.Dispose(); + Connection?.Dispose(); + } + private readonly TestRespServerStream _stream = new(); + public RespConnection Connection { get; } + public ref readonly RespContext Context => ref Connection.Context; + + public TestServer(RespConfiguration? configuration = null) + { + Connection = new StreamConnection( + RespContext.Null.WithCancellationToken(TestContext.Current.CancellationToken), + configuration ?? RespConfiguration.Default, + _stream); + } + public void Respond(ReadOnlySpan serverToClient) => _stream.Respond(serverToClient); + public void AssertSent(ReadOnlySpan clientToServer) => _stream.AssertSent(clientToServer); + private sealed class TestRespServerStream : Stream + { + private bool _disposed, _closed; + + public override void Close() + { + _closed = true; + lock (InboundLock) + { + Monitor.PulseAll(InboundLock); + } + } + + protected override void Dispose(bool disposing) + { + _disposed = true; + if (disposing) + { + lock (InboundLock) + { + Monitor.PulseAll(InboundLock); + } + } + } + + public override void Flush() { } + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + + private void ThrowIfDisposed() + { + if (_disposed) throw new ObjectDisposedException(GetType().Name); + } + + public override int Read(byte[] buffer, int offset, int count) + => ReadCore(buffer.AsSpan(offset, count)); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var read = ReadCore(buffer.AsSpan(offset, count)); + return Task.FromResult(read); + } + + public void Respond(ReadOnlySpan serverToClient) + { + lock (InboundLock) + { + if (!(_disposed | _disposed)) + { + _inbound.Write(serverToClient); + } + + Monitor.PulseAll(InboundLock); + } + } + + private int ReadCore(Span destination) + { + ThrowIfDisposed(); + lock (InboundLock) + { + while (_inbound.CommittedIsEmpty) + { + if (_closed) return 0; + Monitor.Wait(InboundLock); + ThrowIfDisposed(); + } + + if (destination.IsEmpty) return 0; // zero-length read + Assert.True(_inbound.TryGetFirstCommittedSpan(1, out var span)); + Assert.False(span.IsEmpty); + if (span.Length > destination.Length) span = span.Slice(0, destination.Length); + span.CopyTo(destination); + return span.Length; + } + } + +#if NET + public override int Read(Span buffer) => ReadCore(buffer); + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + var read = ReadCore(buffer.Span); + return new(read); + } +#endif + + private readonly object OutboundLock = new object(), InboundLock = new object(); + + private CycleBuffer _outbound = CycleBuffer.Create(MemoryPool.Shared), _inbound = CycleBuffer.Create(MemoryPool.Shared); + + private void WriteCore(ReadOnlySpan source) + { + lock (OutboundLock) + { + _outbound.Write(source); + } + } + + public override void Write(byte[] buffer, int offset, int count) + => WriteCore(buffer.AsSpan(offset, count)); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + WriteCore(buffer.AsSpan(offset, count)); + return Task.CompletedTask; + } + +#if NET + public override void Write(ReadOnlySpan buffer) => WriteCore(buffer); + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + WriteCore(buffer.Span); + return default; + } +#endif + + /// + /// Verifies and discards outbound data. + /// + public void AssertSent(ReadOnlySpan clientToServer) + { + lock (OutboundLock) + { + var available = _outbound.GetCommittedLength(); + Assert.True(available >= clientToServer.Length, $"expected {clientToServer.Length} bytes, {available} available"); + while (!clientToServer.IsEmpty) + { + Assert.True(_outbound.TryGetFirstCommittedSpan(1, out var received), "should have data available"); + var take = Math.Min(received.Length, clientToServer.Length); + Assert.True(take > 0, "should have some data to compare"); + var xBytes = clientToServer.Slice(0, take); + var yBytes = received.Slice(0, take); + if (!xBytes.SequenceEqual(yBytes)) + { + var xText = Encoding.UTF8.GetString(xBytes); + var yText = Encoding.UTF8.GetString(yBytes); + Assert.Fail($"Data mismatch; expected '{xText}', got '{yText}'"); + } + _outbound.DiscardCommitted(take); + clientToServer = clientToServer.Slice(take); + } + } + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + } +} From 3b31c7ebecdf70fd353afe8b4316587b882b97c5 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 3 Sep 2025 07:27:33 +0100 Subject: [PATCH 033/108] more test infrastructure --- src/RESPite/Internal/RespConstants.cs | 4 +- src/RESPite/Messages/RespReader.cs | 12 +- tests/RESP.Core.Tests/BatchTests.cs | 12 +- tests/RESP.Core.Tests/TestConnection.cs | 164 ++++++++++++++++++++++-- 4 files changed, 171 insertions(+), 21 deletions(-) diff --git a/src/RESPite/Internal/RespConstants.cs b/src/RESPite/Internal/RespConstants.cs index 2c5a88786..accb8400b 100644 --- a/src/RESPite/Internal/RespConstants.cs +++ b/src/RESPite/Internal/RespConstants.cs @@ -2,7 +2,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; - +// ReSharper disable InconsistentNaming namespace RESPite.Internal; internal static class RespConstants @@ -13,8 +13,10 @@ internal static class RespConstants public static readonly ushort CrLfUInt16 = UnsafeCpuUInt16(CrlfBytes); + public static ReadOnlySpan OKBytes_LC => "ok"u8; public static ReadOnlySpan OKBytes => "OK"u8; public static readonly ushort OKUInt16 = UnsafeCpuUInt16(OKBytes); + public static readonly ushort OKUInt16_LC = UnsafeCpuUInt16(OKBytes_LC); public static readonly uint BulkStringStreaming = UnsafeCpuUInt32("$?\r\n"u8); public static readonly uint BulkStringNull = UnsafeCpuUInt32("$-1\r"u8); diff --git a/src/RESPite/Messages/RespReader.cs b/src/RESPite/Messages/RespReader.cs index a22713892..ee13aaf8a 100644 --- a/src/RESPite/Messages/RespReader.cs +++ b/src/RESPite/Messages/RespReader.cs @@ -1231,9 +1231,12 @@ private bool TryMoveToNextSegment() [MethodImpl(MethodImplOptions.AggressiveInlining)] internal readonly bool IsOK() // go mad with this, because it is used so often { - return TryGetSpan(out var span) && span.Length == 2 - ? Unsafe.ReadUnaligned(ref UnsafeCurrent) == RespConstants.OKUInt16 - : IsSlow(RespConstants.OKBytes); + if (TryGetSpan(out var span) && span.Length == 2) + { + var u16 = Unsafe.ReadUnaligned(ref UnsafeCurrent); + return u16 == RespConstants.OKUInt16 | u16 == RespConstants.OKUInt16_LC; + } + return IsSlow(RespConstants.OKBytes, RespConstants.OKBytes_LC); } /// @@ -1279,6 +1282,9 @@ public readonly bool Is(byte value) return IsSlow(span); } + private readonly bool IsSlow(ReadOnlySpan testValue0, ReadOnlySpan testValue2) + => IsSlow(testValue0) || IsSlow(testValue2); + private readonly bool IsSlow(ReadOnlySpan testValue) { DemandScalar(); diff --git a/tests/RESP.Core.Tests/BatchTests.cs b/tests/RESP.Core.Tests/BatchTests.cs index 2c981f6d9..81f1ec834 100644 --- a/tests/RESP.Core.Tests/BatchTests.cs +++ b/tests/RESP.Core.Tests/BatchTests.cs @@ -9,12 +9,10 @@ public partial class BatchTests [Fact] public async Task TestInfrastructure() { - using var server = new TestServer(); - var pending = FooAsync(server.Context); - server.AssertSent("*1\r\n$3\r\nfoo\r\n"u8); - Assert.False(pending.IsCompleted); - server.Respond(":42\r\n"u8); - Assert.Equal(42, await pending); + await TestServer.Execute(ctx => FooAsync(ctx), "*1\r\n$3\r\nfoo\r\n"u8, ":42\r\n"u8, 42); + await TestServer.Execute(ctx => FooAsync(ctx), "*1\r\n$3\r\nfoo\r\n", ":42\r\n", 42); + await TestServer.Execute(ctx => BarAsync(ctx), "*1\r\n$3\r\nbar\r\n"u8, "+ok\r\n"u8); + await TestServer.Execute(ctx => BarAsync(ctx), "*1\r\n$3\r\nbar\r\n", "+OK\r\n"); } [Fact] public async Task SimpleBatching() @@ -39,4 +37,6 @@ public async Task SimpleBatching() [RespCommand] private static partial int Foo(in RespContext ctx); + [RespCommand] + private static partial void Bar(in RespContext ctx); } diff --git a/tests/RESP.Core.Tests/TestConnection.cs b/tests/RESP.Core.Tests/TestConnection.cs index 40aa73ef0..ed72de0b6 100644 --- a/tests/RESP.Core.Tests/TestConnection.cs +++ b/tests/RESP.Core.Tests/TestConnection.cs @@ -12,14 +12,8 @@ namespace RESP.Core.Tests; internal sealed class TestServer : IDisposable { - public void Dispose() - { - _stream?.Dispose(); - Connection?.Dispose(); - } private readonly TestRespServerStream _stream = new(); public RespConnection Connection { get; } - public ref readonly RespContext Context => ref Connection.Context; public TestServer(RespConfiguration? configuration = null) { @@ -28,8 +22,152 @@ public TestServer(RespConfiguration? configuration = null) configuration ?? RespConfiguration.Default, _stream); } + + public void Dispose() + { + _stream?.Dispose(); + Connection?.Dispose(); + } + + public static ValueTask Execute( + Func> operation, + ReadOnlySpan request, + ReadOnlySpan response) + => ExecuteCore(operation, request, response); + + // intended for use with [InlineData("...")] scenarios + public static ValueTask Execute( + Func> operation, + string request, + string response) + { + var lease = Encode(request, response, out var reqSpan, out var respSpan); + return ExecuteCore(operation, reqSpan, respSpan, lease); + } + + private static byte[] Encode( + string request, + string response, + out ReadOnlySpan requestSpan, + out ReadOnlySpan responseSpan) + { + var byteCount = Encoding.UTF8.GetByteCount(request) + Encoding.UTF8.GetByteCount(response); + var lease = ArrayPool.Shared.Rent(byteCount); + var reqLen = Encoding.UTF8.GetBytes(request.AsSpan(), lease.AsSpan()); + var respLen = Encoding.UTF8.GetBytes(response.AsSpan(), lease.AsSpan(reqLen)); + requestSpan = lease.AsSpan(0, reqLen); + responseSpan = lease.AsSpan(reqLen, respLen); + return lease; + } + + private static ValueTask ExecuteCore( + Func> operation, + ReadOnlySpan request, + ReadOnlySpan response, + byte[]? lease = null) + { + bool disposeServer = true; + TestServer? server = null; + try + { + server = new TestServer(); + var pending = operation(server.Context); + server.AssertSent(request); + Assert.False(pending.IsCompleted); + server.Respond(response); + disposeServer = false; + return AwaitAndDispose(server, pending); + } + finally + { + if (disposeServer) server?.Dispose(); + if (lease is not null) ArrayPool.Shared.Return(lease); + } + + static async ValueTask AwaitAndDispose(TestServer server, ValueTask pending) + { + using (server) + { + return await pending.ConfigureAwait(false); + } + } + } + + public static ValueTask Execute( + Func> operation, + ReadOnlySpan request, + ReadOnlySpan response, + T expected) + => AwaitAndValidate(Execute(operation, request, response), expected); + + // intended for use with [InlineData("...")] scenarios + public static ValueTask Execute( + Func> operation, + string request, + string response, + T expected) + => AwaitAndValidate(Execute(operation, request, response), expected); + + public static ValueTask Execute( + Func operation, + ReadOnlySpan request, + ReadOnlySpan response) + => ExecuteCore(operation, request, response); + + // intended for use with [InlineData("...")] scenarios + public static ValueTask Execute( + Func operation, + string request, + string response) + { + var lease = Encode(request, response, out var reqSpan, out var respSpan); + return ExecuteCore(operation, reqSpan, respSpan, lease); + } + + private static ValueTask ExecuteCore( + Func operation, + ReadOnlySpan request, + ReadOnlySpan response, + byte[]? lease = null) + { + bool disposeServer = true; + TestServer? server = null; + try + { + server = new TestServer(); + var pending = operation(server.Context); + server.AssertSent(request); + Assert.False(pending.IsCompleted); + server.Respond(response); + disposeServer = false; + return AwaitAndDispose(server, pending); + } + finally + { + if (disposeServer) server?.Dispose(); + if (lease is not null) ArrayPool.Shared.Return(lease); + } + + static async ValueTask AwaitAndDispose(TestServer server, ValueTask pending) + { + using (server) + { + await pending.ConfigureAwait(false); + } + } + } + + private static async ValueTask AwaitAndValidate(ValueTask pending, T expected) + { + var actual = await pending.ConfigureAwait(false); + Assert.Equal(expected, actual); + } + + public ref readonly RespContext Context => ref Connection.Context; + public void Respond(ReadOnlySpan serverToClient) => _stream.Respond(serverToClient); public void AssertSent(ReadOnlySpan clientToServer) => _stream.AssertSent(clientToServer); + private sealed class TestRespServerStream : Stream { private bool _disposed, _closed; @@ -122,7 +260,8 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken private readonly object OutboundLock = new object(), InboundLock = new object(); - private CycleBuffer _outbound = CycleBuffer.Create(MemoryPool.Shared), _inbound = CycleBuffer.Create(MemoryPool.Shared); + private CycleBuffer _outbound = CycleBuffer.Create(MemoryPool.Shared), + _inbound = CycleBuffer.Create(MemoryPool.Shared); private void WriteCore(ReadOnlySpan source) { @@ -160,7 +299,9 @@ public void AssertSent(ReadOnlySpan clientToServer) lock (OutboundLock) { var available = _outbound.GetCommittedLength(); - Assert.True(available >= clientToServer.Length, $"expected {clientToServer.Length} bytes, {available} available"); + Assert.True( + available >= clientToServer.Length, + $"expected {clientToServer.Length} bytes, {available} available"); while (!clientToServer.IsEmpty) { Assert.True(_outbound.TryGetFirstCommittedSpan(1, out var received), "should have data available"); @@ -170,10 +311,11 @@ public void AssertSent(ReadOnlySpan clientToServer) var yBytes = received.Slice(0, take); if (!xBytes.SequenceEqual(yBytes)) { - var xText = Encoding.UTF8.GetString(xBytes); - var yText = Encoding.UTF8.GetString(yBytes); - Assert.Fail($"Data mismatch; expected '{xText}', got '{yText}'"); + var xText = Encoding.UTF8.GetString(xBytes).Replace("\r\n", "\\r\\n"); + var yText = Encoding.UTF8.GetString(yBytes).Replace("\r\n", "\\r\\n"); + Assert.Equal(xText, yText); } + _outbound.DiscardCommitted(take); clientToServer = clientToServer.Slice(take); } From 57cc9fdba9ff44f658444c9e477d69498a8a95e6 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 3 Sep 2025 08:35:09 +0100 Subject: [PATCH 034/108] more test infrastructure --- .../RespCommandGenerator.cs | 1 + src/RESPite/RespFormatters.cs | 45 +++++++++- src/RESPite/RespOperation.cs | 6 ++ src/RESPite/RespOperationT.cs | 6 ++ tests/RESP.Core.Tests/BatchTests.cs | 51 +++++++---- .../{TestConnection.cs => TestServer.cs} | 25 +++++- tests/RESP.Core.Tests/ValueTaskExtensions.cs | 89 +++++++++++++++++++ 7 files changed, 203 insertions(+), 20 deletions(-) rename tests/RESP.Core.Tests/{TestConnection.cs => TestServer.cs} (94%) create mode 100644 tests/RESP.Core.Tests/ValueTaskExtensions.cs diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index cfbde41ce..3bde746dc 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -673,6 +673,7 @@ private static int DataParameterCount( "long" => RespFormattersPrefix + "Int64", "float" => RespFormattersPrefix + "Single", "double" => RespFormattersPrefix + "Double", + "" => RespFormattersPrefix + "Empty", _ => null, }; diff --git a/src/RESPite/RespFormatters.cs b/src/RESPite/RespFormatters.cs index 5fbe1681c..3ac332fda 100644 --- a/src/RESPite/RespFormatters.cs +++ b/src/RESPite/RespFormatters.cs @@ -9,6 +9,10 @@ public static class RespFormatters public static IRespFormatter ByteArray(bool isKey) => isKey ? Key.ByteArray : Value.ByteArray; public static IRespFormatter> Bytes(bool isKey) => isKey ? Key.Bytes : Value.Bytes; public static IRespFormatter Empty => EmptyFormatter.Instance; + public static IRespFormatter Int32 => Value.Formatter.Default; + public static IRespFormatter Int64 => Value.Formatter.Default; + public static IRespFormatter Single => Value.Formatter.Default; + public static IRespFormatter Double => Value.Formatter.Default; public static class Key { @@ -16,8 +20,11 @@ public static class Key public static IRespFormatter String => Formatter.Default; public static IRespFormatter> Chars => Formatter.Default; public static IRespFormatter ByteArray => Formatter.Default; + public static IRespFormatter> Bytes => Formatter.Default; // ReSharper restore MemberHidesStaticFromOuterClass + + // (just to fix an auto-format glitch) internal sealed class Formatter : IRespFormatter, IRespFormatter, IRespFormatter>, IRespFormatter> { @@ -29,16 +36,19 @@ public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in writer.WriteCommand(command, 1); writer.WriteKey(value); } + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in byte[] value) { writer.WriteCommand(command, 1); writer.WriteKey(value); } + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in ReadOnlyMemory value) { writer.WriteCommand(command, 1); writer.WriteKey(value); } + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in ReadOnlyMemory value) { writer.WriteCommand(command, 1); @@ -53,10 +63,15 @@ public static class Value public static IRespFormatter String => Formatter.Default; public static IRespFormatter> Chars => Formatter.Default; public static IRespFormatter ByteArray => Formatter.Default; + public static IRespFormatter> Bytes => Formatter.Default; // ReSharper restore MemberHidesStaticFromOuterClass + + // (just to fix an auto-format glitch) internal sealed class Formatter : IRespFormatter, IRespFormatter, - IRespFormatter>, IRespFormatter> + IRespFormatter>, IRespFormatter>, + IRespFormatter, IRespFormatter, + IRespFormatter, IRespFormatter { private Formatter() { } public static readonly Formatter Default = new(); @@ -66,21 +81,48 @@ public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in writer.WriteCommand(command, 1); writer.WriteBulkString(value); } + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in byte[] value) { writer.WriteCommand(command, 1); writer.WriteBulkString(value); } + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in ReadOnlyMemory value) { writer.WriteCommand(command, 1); writer.WriteBulkString(value); } + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in ReadOnlyMemory value) { writer.WriteCommand(command, 1); writer.WriteBulkString(value); } + + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in int value) + { + writer.WriteCommand(command, 1); + writer.WriteBulkString(value); + } + + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in long value) + { + writer.WriteCommand(command, 1); + writer.WriteBulkString(value); + } + + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in float value) + { + writer.WriteCommand(command, 1); + writer.WriteBulkString(value); + } + + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in double value) + { + writer.WriteCommand(command, 1); + writer.WriteBulkString(value); + } } } @@ -88,6 +130,7 @@ private sealed class EmptyFormatter : IRespFormatter { private EmptyFormatter() { } public static readonly EmptyFormatter Instance = new(); + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in bool value) { writer.WriteCommand(command, 0); diff --git a/src/RESPite/RespOperation.cs b/src/RESPite/RespOperation.cs index 7d1e9c2c8..49b81fa83 100644 --- a/src/RESPite/RespOperation.cs +++ b/src/RESPite/RespOperation.cs @@ -40,6 +40,12 @@ internal static void DebugOnAllocateMessage() private readonly short _token; private readonly bool _disableCaptureContext; // default is false, so: bypass + internal RespOperation(RespMessageBase message, short token, bool disableCaptureContext) + { + _message = message; + _token = token; + _disableCaptureContext = disableCaptureContext; + } internal RespOperation(RespMessageBase message, bool disableCaptureContext = false) { _message = message; diff --git a/src/RESPite/RespOperationT.cs b/src/RESPite/RespOperationT.cs index 375241a58..3e39ec79a 100644 --- a/src/RESPite/RespOperationT.cs +++ b/src/RESPite/RespOperationT.cs @@ -19,6 +19,12 @@ public readonly struct RespOperation private readonly short _token; private readonly bool _disableCaptureContext; + internal RespOperation(RespMessageBase message, short token, bool disableCaptureContext) + { + _message = message; + _token = token; + _disableCaptureContext = disableCaptureContext; + } internal RespOperation(RespMessageBase message, bool disableCaptureContext = false) { _message = message; diff --git a/tests/RESP.Core.Tests/BatchTests.cs b/tests/RESP.Core.Tests/BatchTests.cs index 81f1ec834..75dac7eeb 100644 --- a/tests/RESP.Core.Tests/BatchTests.cs +++ b/tests/RESP.Core.Tests/BatchTests.cs @@ -14,29 +14,48 @@ public async Task TestInfrastructure() await TestServer.Execute(ctx => BarAsync(ctx), "*1\r\n$3\r\nbar\r\n"u8, "+ok\r\n"u8); await TestServer.Execute(ctx => BarAsync(ctx), "*1\r\n$3\r\nbar\r\n", "+OK\r\n"); } + [Fact] public async Task SimpleBatching() { - // todo: create a manually controlled data pipe - var parentContext = RespContext.Null.WithCancellationToken(TestContext.Current.CancellationToken); - using (var batch = parentContext.CreateBatch()) - { - var ctx = batch.Context; - var a = FooAsync(ctx); - var b = FooAsync(ctx); - var c = FooAsync(ctx); - - await batch.FlushAsync(); - - // todo: supply :1\r\n:2\r\n:3\r\n - Assert.Equal(1, await a); - Assert.Equal(2, await b); - Assert.Equal(3, await c); - } + using var server = new TestServer(); + // prepare a batch + var batch = server.Context.CreateBatch(); + var b = TestAsync(batch.Context, 1); + var c = TestAsync(batch.Context, 2); + var d = TestAsync(batch.Context, 3); + + // we want to sandwich the batch between two regular operations + var a = TestAsync(server.Context, 0); // uses SERVER + Assert.True(a.Unwrap().IsSent); + Assert.False(d.Unwrap().IsSent); + await batch.FlushAsync(); // uses BATCH + Assert.True(d.Unwrap().IsSent); + var e = TestAsync(server.Context, 4); // uses SERVER again + + // check what was sent + server.AssertSent("*2\r\n$4\r\ntest\r\n$1\r\n0\r\n"u8); + server.AssertSent("*2\r\n$4\r\ntest\r\n$1\r\n1\r\n"u8); + server.AssertSent("*2\r\n$4\r\ntest\r\n$1\r\n2\r\n"u8); + server.AssertSent("*2\r\n$4\r\ntest\r\n$1\r\n3\r\n"u8); + server.AssertSent("*2\r\n$4\r\ntest\r\n$1\r\n4\r\n"u8); + server.AssertAllSent(); // that's everything + + // check what is received (all in one chunk) + server.Respond(":5\r\n:6\r\n:7\r\n:8\r\n:9\r\n"u8); + Assert.Equal(5, await a); + Assert.Equal(6, await b); + Assert.Equal(7, await c); + Assert.Equal(8, await d); + Assert.Equal(9, await e); } + [RespCommand] + private static partial int Test(in RespContext ctx, int value); + [RespCommand] private static partial int Foo(in RespContext ctx); + [RespCommand] private static partial void Bar(in RespContext ctx); } diff --git a/tests/RESP.Core.Tests/TestConnection.cs b/tests/RESP.Core.Tests/TestServer.cs similarity index 94% rename from tests/RESP.Core.Tests/TestConnection.cs rename to tests/RESP.Core.Tests/TestServer.cs index ed72de0b6..2889e24ea 100644 --- a/tests/RESP.Core.Tests/TestConnection.cs +++ b/tests/RESP.Core.Tests/TestServer.cs @@ -72,7 +72,7 @@ private static ValueTask ExecuteCore( { server = new TestServer(); var pending = operation(server.Context); - server.AssertSent(request); + server.AssertSent(request, final: true); Assert.False(pending.IsCompleted); server.Respond(response); disposeServer = false; @@ -136,7 +136,7 @@ private static ValueTask ExecuteCore( { server = new TestServer(); var pending = operation(server.Context); - server.AssertSent(request); + server.AssertSent(request, final: true); Assert.False(pending.IsCompleted); server.Respond(response); disposeServer = false; @@ -166,7 +166,14 @@ private static async ValueTask AwaitAndValidate(ValueTask pending, T expec public ref readonly RespContext Context => ref Connection.Context; public void Respond(ReadOnlySpan serverToClient) => _stream.Respond(serverToClient); - public void AssertSent(ReadOnlySpan clientToServer) => _stream.AssertSent(clientToServer); + + public void AssertSent(ReadOnlySpan clientToServer, bool final = false) + { + _stream.AssertSent(clientToServer); + if (final) _stream.AssertAllSent(); + } + + public void AssertAllSent() => _stream.AssertAllSent(); private sealed class TestRespServerStream : Stream { @@ -291,6 +298,18 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo } #endif + // verifies that there is no more request data unaccounted for + public void AssertAllSent() + { + bool empty; + lock (OutboundLock) + { + empty = _outbound.CommittedIsEmpty; + } + + Assert.True(empty); + } + /// /// Verifies and discards outbound data. /// diff --git a/tests/RESP.Core.Tests/ValueTaskExtensions.cs b/tests/RESP.Core.Tests/ValueTaskExtensions.cs new file mode 100644 index 000000000..7e37a44e8 --- /dev/null +++ b/tests/RESP.Core.Tests/ValueTaskExtensions.cs @@ -0,0 +1,89 @@ +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using RESPite; +using RESPite.Internal; + +namespace RESP.Core.Tests; + +public static class ValueTaskExtensions +{ + private static class FieldCache + { +#if NET8_0_OR_GREATER + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_obj")] + public static extern ref readonly object? Object(in ValueTask task); + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_token")] + public static extern ref readonly short Token(in ValueTask task); + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_continueOnCapturedContext")] + public static extern ref readonly bool ContinueOnCapturedContext(in ValueTask task); +#else + private static readonly FieldInfo _obj = + typeof(ValueTask).GetField(nameof(_obj), BindingFlags.NonPublic | BindingFlags.Instance)!; + + private static readonly FieldInfo _token = + typeof(ValueTask).GetField(nameof(_token), BindingFlags.NonPublic | BindingFlags.Instance)!; + + private static readonly FieldInfo? _continueOnCapturedContext = typeof(ValueTask).GetField( + nameof(_continueOnCapturedContext), + BindingFlags.NonPublic | BindingFlags.Instance); + + public static object? Object(ValueTask task) => _obj.GetValue(task); + + public static short Token(ValueTask task) => (short)_token.GetValue(task)!; + + public static bool ContinueOnCapturedContext(ValueTask task) + => _continueOnCapturedContext is not null + && (bool)_continueOnCapturedContext.GetValue(task)!; +#endif + } + + private static class FieldCache + { +#if NET8_0_OR_GREATER + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_obj")] + public static extern ref readonly object? Object(in ValueTask task); + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_token")] + public static extern ref readonly short Token(in ValueTask task); + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_continueOnCapturedContext")] + public static extern ref readonly bool ContinueOnCapturedContext(in ValueTask task); +#else + private static readonly FieldInfo _obj = + typeof(ValueTask).GetField(nameof(_obj), BindingFlags.NonPublic | BindingFlags.Instance)!; + + private static readonly FieldInfo _token = + typeof(ValueTask).GetField(nameof(_token), BindingFlags.NonPublic | BindingFlags.Instance)!; + + private static readonly FieldInfo? _continueOnCapturedContext = typeof(ValueTask).GetField( + nameof(_continueOnCapturedContext), + BindingFlags.NonPublic | BindingFlags.Instance); + + public static object? Object(ValueTask task) => _obj.GetValue(task); + + public static short Token(ValueTask task) => (short)_token.GetValue(task)!; + + public static bool ContinueOnCapturedContext(ValueTask task) + => _continueOnCapturedContext is not null + && (bool)_continueOnCapturedContext.GetValue(task)!; +#endif + } + + public static RespOperation Unwrap(this ValueTask value) + { + if (FieldCache.Object(value) is not RespMessageBase msg) + throw new ArgumentException($"ValueTask does not wrap a {nameof(RespMessageBase)}<{typeof(T).Name}>"); + short token = FieldCache.Token(value); + bool continueOnCapturedContext = FieldCache.ContinueOnCapturedContext(value); + return new RespOperation(msg, token, continueOnCapturedContext); + } + + public static RespOperation Unwrap(this ValueTask value) + { + if (FieldCache.Object(value) is not RespMessageBase msg) + throw new ArgumentException($"ValueTask does not wrap a {nameof(RespMessageBase)}"); + short token = FieldCache.Token(value); + bool continueOnCapturedContext = FieldCache.ContinueOnCapturedContext(value); + return new RespOperation(msg, token, continueOnCapturedContext); + } +} From 3dd9b13c5ed24afb5c2a20e18d69605cc4c091dc Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 3 Sep 2025 08:56:52 +0100 Subject: [PATCH 035/108] batch lifetime --- tests/RESP.Core.Tests/BatchTests.cs | 37 +++++++++++++++++++---------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/tests/RESP.Core.Tests/BatchTests.cs b/tests/RESP.Core.Tests/BatchTests.cs index 75dac7eeb..8218b9552 100644 --- a/tests/RESP.Core.Tests/BatchTests.cs +++ b/tests/RESP.Core.Tests/BatchTests.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using RESPite; using Xunit; @@ -20,18 +21,30 @@ public async Task SimpleBatching() { using var server = new TestServer(); // prepare a batch - var batch = server.Context.CreateBatch(); - var b = TestAsync(batch.Context, 1); - var c = TestAsync(batch.Context, 2); - var d = TestAsync(batch.Context, 3); - - // we want to sandwich the batch between two regular operations - var a = TestAsync(server.Context, 0); // uses SERVER - Assert.True(a.Unwrap().IsSent); - Assert.False(d.Unwrap().IsSent); - await batch.FlushAsync(); // uses BATCH + ValueTask a, b, c, d, e, f; + using (var batch = server.Context.CreateBatch()) + { + b = TestAsync(batch.Context, 1); + c = TestAsync(batch.Context, 2); + d = TestAsync(batch.Context, 3); + + // we want to sandwich the batch between two regular operations + a = TestAsync(server.Context, 0); // uses SERVER + Assert.True(a.Unwrap().IsSent); + Assert.False(d.Unwrap().IsSent); + await batch.FlushAsync(); // uses BATCH + + // await something not flushed, inside the scope of the batch + f = TestAsync(batch.Context, 10); + await Assert.ThrowsAsync(async () => await f); + + // and try one that escapes the batch (should be disposed) + f = TestAsync(batch.Context, 10); // never flushed, intentionally + } + await Assert.ThrowsAsync(async () => await f); + Assert.True(d.Unwrap().IsSent); - var e = TestAsync(server.Context, 4); // uses SERVER again + e = TestAsync(server.Context, 4); // uses SERVER again // check what was sent server.AssertSent("*2\r\n$4\r\ntest\r\n$1\r\n0\r\n"u8); From e8e1cdc6c1e1f641cee4810d481a85022560c7a5 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 3 Sep 2025 09:39:42 +0100 Subject: [PATCH 036/108] Make ValueTaskExtensions part of the RESPite API --- src/RESPite/Internal/RespMessageBase.cs | 12 +- src/RESPite/RespOperationT.cs | 2 +- src/RESPite/ValueTaskExtensions.cs | 137 +++++++++++++++++++ tests/RESP.Core.Tests/BatchTests.cs | 27 +++- tests/RESP.Core.Tests/ValueTaskExtensions.cs | 89 ------------ 5 files changed, 161 insertions(+), 106 deletions(-) create mode 100644 src/RESPite/ValueTaskExtensions.cs delete mode 100644 tests/RESP.Core.Tests/ValueTaskExtensions.cs diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs index 50f824210..5005efed2 100644 --- a/src/RESPite/Internal/RespMessageBase.cs +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -140,16 +140,7 @@ public void Init(bool sent, CancellationToken cancellationToken) public void Init( ReadOnlyMemory request, - CancellationToken cancellationToken) - { - Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); - _request = new(request); - _requestRefCount = 1; - if (cancellationToken.CanBeCanceled) - { - _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellationToken); - } - } + CancellationToken cancellationToken) => Init(new ReadOnlySequence(request), cancellationToken); public void Init( ReadOnlySequence request, @@ -160,6 +151,7 @@ public void Init( _requestRefCount = 1; if (cancellationToken.CanBeCanceled) { + _cancellationToken = cancellationToken; _cancellationTokenRegistration = ActivationHelper.RegisterForCancellation(this, cancellationToken); } } diff --git a/src/RESPite/RespOperationT.cs b/src/RESPite/RespOperationT.cs index 3e39ec79a..8de2c9542 100644 --- a/src/RESPite/RespOperationT.cs +++ b/src/RESPite/RespOperationT.cs @@ -12,7 +12,7 @@ namespace RESPite; /// value can be awaited synchronously if required. /// /// The type of value returned by the operation. -public readonly struct RespOperation +public readonly struct RespOperation : ICriticalNotifyCompletion { // it is important that this layout remains identical between RespOperation and RespOperation private readonly RespMessageBase _message; diff --git a/src/RESPite/ValueTaskExtensions.cs b/src/RESPite/ValueTaskExtensions.cs new file mode 100644 index 000000000..56ec0cfde --- /dev/null +++ b/src/RESPite/ValueTaskExtensions.cs @@ -0,0 +1,137 @@ +#if NET8_0_OR_GREATER +using System.Runtime.CompilerServices; +#else +using System.Reflection; +#endif +using RESPite.Internal; + +namespace RESPite; + +/// +/// The results of asynchronous RESPite operations can be treated interchangeably as either or +/// (or their generic twins: and ). +/// is a more familiar, and is convenient in pre-existing code; +/// is more context-aware, and adds a few additional capabilities, such as: +/// - most notably: automatic detection if attempting to wait/await before a message has been sent. +/// - to check whether the message has been sent to a server. +/// - to access cancellation information about this message. +/// - to wait synchronously for the operation to complete. +/// - a can be implicitly converted to a (unlike to ). +/// +/// Neither representation is more efficient, and the semantics are identical - the result can only be waited/awaited once +/// (unless hoisted into a ). +/// +public static class ValueTaskExtensions +{ + public static bool TryGetRespOperation(this ValueTask value, out RespOperation operation) + { + if (FieldAccessor.Object(value) is not RespMessageBase msg) + { + operation = default; + return false; + } + + short token = FieldAccessor.Token(value); + bool continueOnCapturedContext = FieldAccessor.ContinueOnCapturedContext(value); + operation = new RespOperation(msg, token, continueOnCapturedContext); + return true; + } + + public static RespOperation AsRespOperation(this ValueTask value) + { + if (!TryGetRespOperation(value, out var operation)) Throw(typeof(T)); + return operation; + } + + public static bool TryGetRespOperation(this ValueTask value, out RespOperation operation) + { + if (FieldAccessor.Object(value) is not RespMessageBase msg) + { + operation = default; + return false; + } + + short token = FieldAccessor.Token(value); + bool continueOnCapturedContext = FieldAccessor.ContinueOnCapturedContext(value); + operation = new RespOperation(msg, token, continueOnCapturedContext); + return true; + } + + public static RespOperation AsRespOperation(this ValueTask value) + { + if (!TryGetRespOperation(value, out var operation)) Throw(); + return operation; + } + + private static void Throw(Type type) + => throw new ArgumentException( + $"The {nameof(ValueTask)}<{type.Name}> does not wrap does not wrap a {nameof(RespMessageBase)}<{type.Name}>"); + + private static void Throw() => + throw new ArgumentException($"The {nameof(ValueTask)} does not wrap a {nameof(RespMessageBase)}"); + + // from here on: evil reflection to peek inside ValueTask[] and extract the fields we need + private static class FieldAccessor + { +#if NET8_0_OR_GREATER + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_obj")] + public static extern ref readonly object? Object(in ValueTask task); + + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_token")] + public static extern ref readonly short Token(in ValueTask task); + + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_continueOnCapturedContext")] + public static extern ref readonly bool ContinueOnCapturedContext(in ValueTask task); +#else + private static readonly FieldInfo _obj = + typeof(ValueTask).GetField(nameof(_obj), BindingFlags.NonPublic | BindingFlags.Instance)!; + + private static readonly FieldInfo _token = + typeof(ValueTask).GetField(nameof(_token), BindingFlags.NonPublic | BindingFlags.Instance)!; + + private static readonly FieldInfo? _continueOnCapturedContext = typeof(ValueTask).GetField( + nameof(_continueOnCapturedContext), + BindingFlags.NonPublic | BindingFlags.Instance); + + public static object? Object(ValueTask task) => _obj.GetValue(task); + + public static short Token(ValueTask task) => (short)_token.GetValue(task)!; + + public static bool ContinueOnCapturedContext(ValueTask task) + => _continueOnCapturedContext is not null + && (bool)_continueOnCapturedContext.GetValue(task)!; +#endif + } + + private static class FieldAccessor + { +#if NET8_0_OR_GREATER + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_obj")] + public static extern ref readonly object? Object(in ValueTask task); + + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_token")] + public static extern ref readonly short Token(in ValueTask task); + + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_continueOnCapturedContext")] + public static extern ref readonly bool ContinueOnCapturedContext(in ValueTask task); +#else + private static readonly FieldInfo _obj = + typeof(ValueTask).GetField(nameof(_obj), BindingFlags.NonPublic | BindingFlags.Instance)!; + + private static readonly FieldInfo _token = + typeof(ValueTask).GetField(nameof(_token), BindingFlags.NonPublic | BindingFlags.Instance)!; + + private static readonly FieldInfo? _continueOnCapturedContext = typeof(ValueTask).GetField( + nameof(_continueOnCapturedContext), + BindingFlags.NonPublic | BindingFlags.Instance); + + public static object? Object(ValueTask task) => _obj.GetValue(task); + + public static short Token(ValueTask task) => (short)_token.GetValue(task)!; + + public static bool ContinueOnCapturedContext(ValueTask task) + => _continueOnCapturedContext is not null + && (bool)_continueOnCapturedContext.GetValue(task)!; +#endif + } +} diff --git a/tests/RESP.Core.Tests/BatchTests.cs b/tests/RESP.Core.Tests/BatchTests.cs index 8218b9552..bab447c7a 100644 --- a/tests/RESP.Core.Tests/BatchTests.cs +++ b/tests/RESP.Core.Tests/BatchTests.cs @@ -16,34 +16,49 @@ public async Task TestInfrastructure() await TestServer.Execute(ctx => BarAsync(ctx), "*1\r\n$3\r\nbar\r\n", "+OK\r\n"); } - [Fact] + [Fact(Timeout = 1000)] public async Task SimpleBatching() { + // server setup using var server = new TestServer(); + var cancellationToken = server.Context.CancellationToken; + Assert.Equal(TestContext.Current.CancellationToken, cancellationToken); // check server has CT + Assert.True(cancellationToken.CanBeCanceled); + // prepare a batch ValueTask a, b, c, d, e, f; using (var batch = server.Context.CreateBatch()) { + Assert.Equal(cancellationToken, batch.Context.CancellationToken); // check the batch inherited CT + b = TestAsync(batch.Context, 1); + Assert.Equal(cancellationToken, b.AsRespOperation().CancellationToken); // check batch ops inherit CT c = TestAsync(batch.Context, 2); d = TestAsync(batch.Context, 3); // we want to sandwich the batch between two regular operations a = TestAsync(server.Context, 0); // uses SERVER - Assert.True(a.Unwrap().IsSent); - Assert.False(d.Unwrap().IsSent); + Assert.Equal(cancellationToken, a.AsRespOperation().CancellationToken); // check server ops inherit CT + Assert.True(a.AsRespOperation().IsSent); + Assert.False(d.AsRespOperation().IsSent); await batch.FlushAsync(); // uses BATCH // await something not flushed, inside the scope of the batch f = TestAsync(batch.Context, 10); - await Assert.ThrowsAsync(async () => await f); - // and try one that escapes the batch (should be disposed) + // Because of https://github.com/dotnet/runtime/issues/119232, we can't detect unsent operations + // in ValueTask/Task (technically we could for ValueTask[T], but it would break .AsTask()), but + // we can check the unwrapped handling. + var ex = await Assert.ThrowsAsync(async () => await f.AsRespOperation()); + Assert.StartsWith("This command has not yet been sent", ex.Message); + + // and try one that escapes the batch (should get disposed) f = TestAsync(batch.Context, 10); // never flushed, intentionally } + // we *can* safely await if the batch is disposed await Assert.ThrowsAsync(async () => await f); - Assert.True(d.Unwrap().IsSent); + Assert.True(d.AsRespOperation().IsSent); e = TestAsync(server.Context, 4); // uses SERVER again // check what was sent diff --git a/tests/RESP.Core.Tests/ValueTaskExtensions.cs b/tests/RESP.Core.Tests/ValueTaskExtensions.cs deleted file mode 100644 index 7e37a44e8..000000000 --- a/tests/RESP.Core.Tests/ValueTaskExtensions.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using RESPite; -using RESPite.Internal; - -namespace RESP.Core.Tests; - -public static class ValueTaskExtensions -{ - private static class FieldCache - { -#if NET8_0_OR_GREATER - [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_obj")] - public static extern ref readonly object? Object(in ValueTask task); - [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_token")] - public static extern ref readonly short Token(in ValueTask task); - [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_continueOnCapturedContext")] - public static extern ref readonly bool ContinueOnCapturedContext(in ValueTask task); -#else - private static readonly FieldInfo _obj = - typeof(ValueTask).GetField(nameof(_obj), BindingFlags.NonPublic | BindingFlags.Instance)!; - - private static readonly FieldInfo _token = - typeof(ValueTask).GetField(nameof(_token), BindingFlags.NonPublic | BindingFlags.Instance)!; - - private static readonly FieldInfo? _continueOnCapturedContext = typeof(ValueTask).GetField( - nameof(_continueOnCapturedContext), - BindingFlags.NonPublic | BindingFlags.Instance); - - public static object? Object(ValueTask task) => _obj.GetValue(task); - - public static short Token(ValueTask task) => (short)_token.GetValue(task)!; - - public static bool ContinueOnCapturedContext(ValueTask task) - => _continueOnCapturedContext is not null - && (bool)_continueOnCapturedContext.GetValue(task)!; -#endif - } - - private static class FieldCache - { -#if NET8_0_OR_GREATER - [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_obj")] - public static extern ref readonly object? Object(in ValueTask task); - [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_token")] - public static extern ref readonly short Token(in ValueTask task); - [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_continueOnCapturedContext")] - public static extern ref readonly bool ContinueOnCapturedContext(in ValueTask task); -#else - private static readonly FieldInfo _obj = - typeof(ValueTask).GetField(nameof(_obj), BindingFlags.NonPublic | BindingFlags.Instance)!; - - private static readonly FieldInfo _token = - typeof(ValueTask).GetField(nameof(_token), BindingFlags.NonPublic | BindingFlags.Instance)!; - - private static readonly FieldInfo? _continueOnCapturedContext = typeof(ValueTask).GetField( - nameof(_continueOnCapturedContext), - BindingFlags.NonPublic | BindingFlags.Instance); - - public static object? Object(ValueTask task) => _obj.GetValue(task); - - public static short Token(ValueTask task) => (short)_token.GetValue(task)!; - - public static bool ContinueOnCapturedContext(ValueTask task) - => _continueOnCapturedContext is not null - && (bool)_continueOnCapturedContext.GetValue(task)!; -#endif - } - - public static RespOperation Unwrap(this ValueTask value) - { - if (FieldCache.Object(value) is not RespMessageBase msg) - throw new ArgumentException($"ValueTask does not wrap a {nameof(RespMessageBase)}<{typeof(T).Name}>"); - short token = FieldCache.Token(value); - bool continueOnCapturedContext = FieldCache.ContinueOnCapturedContext(value); - return new RespOperation(msg, token, continueOnCapturedContext); - } - - public static RespOperation Unwrap(this ValueTask value) - { - if (FieldCache.Object(value) is not RespMessageBase msg) - throw new ArgumentException($"ValueTask does not wrap a {nameof(RespMessageBase)}"); - short token = FieldCache.Token(value); - bool continueOnCapturedContext = FieldCache.ContinueOnCapturedContext(value); - return new RespOperation(msg, token, continueOnCapturedContext); - } -} From e14eddf92dceccfb790631bbb9ac29bb96c81636 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 3 Sep 2025 10:11:07 +0100 Subject: [PATCH 037/108] check single-await --- tests/RESP.Core.Tests/BatchTests.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/RESP.Core.Tests/BatchTests.cs b/tests/RESP.Core.Tests/BatchTests.cs index bab447c7a..8477e947f 100644 --- a/tests/RESP.Core.Tests/BatchTests.cs +++ b/tests/RESP.Core.Tests/BatchTests.cs @@ -16,7 +16,7 @@ public async Task TestInfrastructure() await TestServer.Execute(ctx => BarAsync(ctx), "*1\r\n$3\r\nbar\r\n", "+OK\r\n"); } - [Fact(Timeout = 1000)] + [Fact(Timeout = 500)] // this should be very fast unless something is very wrong public async Task SimpleBatching() { // server setup @@ -76,6 +76,13 @@ public async Task SimpleBatching() Assert.Equal(7, await c); Assert.Equal(8, await d); Assert.Equal(9, await e); + + // but can only be awaited once + await Assert.ThrowsAsync(async () => await a); + await Assert.ThrowsAsync(async () => await b); + await Assert.ThrowsAsync(async () => await c); + await Assert.ThrowsAsync(async () => await d); + await Assert.ThrowsAsync(async () => await e); } [RespCommand] From c4a9264489bb6434fe32da046953d1d14d57d01c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 3 Sep 2025 10:11:07 +0100 Subject: [PATCH 038/108] re-integrate reader/writer tests; failing cancellation test in netfx, vexingly --- StackExchange.Redis.sln | 2 +- src/RESP.Core/RESP.Core.csproj | 2 +- src/RESPite/Internal/RespMessageBase.cs | 72 +- src/RESPite/Internal/RespMessageBaseT.cs | 24 +- src/RESPite/Internal/RespMultiMessage.cs | 2 +- .../RespReader.AggregateEnumerator.cs | 4 +- .../Messages/RespReader.ScalarEnumerator.cs | 4 +- src/RESPite/Messages/RespReader.Utils.cs | 6 +- src/RESPite/Messages/RespReader.cs | 33 +- src/RESPite/RESPite.csproj | 2 +- .../BasicIntegrationTests.cs | 2 +- .../BatchTests.cs | 11 +- .../BlockBufferTests.cs | 2 +- .../ConnectionFixture.cs | 6 +- .../CycleBufferTests.cs | 2 +- .../IntegrationTestBase.cs | 2 +- .../OperationUnitTests.cs | 25 +- .../RESPite.Tests.csproj} | 0 .../RedisStringsIntegrationTests.cs | 2 +- tests/RESPite.Tests/RespReaderTests.cs | 863 ++++++++++++++++++ tests/RESPite.Tests/RespWriterTests.cs | 43 + tests/RESPite.Tests/TestBufferWriter.cs | 52 ++ .../TestServer.cs | 2 +- 23 files changed, 1077 insertions(+), 86 deletions(-) rename tests/{RESP.Core.Tests => RESPite.Tests}/BasicIntegrationTests.cs (99%) rename tests/{RESP.Core.Tests => RESPite.Tests}/BatchTests.cs (86%) rename tests/{RESP.Core.Tests => RESPite.Tests}/BlockBufferTests.cs (99%) rename tests/{RESP.Core.Tests => RESPite.Tests}/ConnectionFixture.cs (79%) rename tests/{RESP.Core.Tests => RESPite.Tests}/CycleBufferTests.cs (99%) rename tests/{RESP.Core.Tests => RESPite.Tests}/IntegrationTestBase.cs (96%) rename tests/{RESP.Core.Tests => RESPite.Tests}/OperationUnitTests.cs (92%) rename tests/{RESP.Core.Tests/RESP.Core.Tests.csproj => RESPite.Tests/RESPite.Tests.csproj} (100%) rename tests/{RESP.Core.Tests => RESPite.Tests}/RedisStringsIntegrationTests.cs (97%) create mode 100644 tests/RESPite.Tests/RespReaderTests.cs create mode 100644 tests/RESPite.Tests/RespWriterTests.cs create mode 100644 tests/RESPite.Tests/TestBufferWriter.cs rename tests/{RESP.Core.Tests => RESPite.Tests}/TestServer.cs (99%) diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index 736c56699..86f751e25 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -124,7 +124,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackExchange.Redis.Benchma EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RESP.Core", "src\RESP.Core\RESP.Core.csproj", "{E50EEB8B-6B3F-4C8C-A5C6-C37FB87C01E2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RESP.Core.Tests", "tests\RESP.Core.Tests\RESP.Core.Tests.csproj", "{7063E2D3-C591-4604-A5DD-32D4A1678A58}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RESPite.Tests", "tests\RESPite.Tests\RESPite.Tests.csproj", "{7063E2D3-C591-4604-A5DD-32D4A1678A58}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{C0132984-68D1-4A97-8F8C-AD4E2EECC583}" EndProject diff --git a/src/RESP.Core/RESP.Core.csproj b/src/RESP.Core/RESP.Core.csproj index 5bdcb3e28..ee0188560 100644 --- a/src/RESP.Core/RESP.Core.csproj +++ b/src/RESP.Core/RESP.Core.csproj @@ -47,7 +47,7 @@ - + diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs index 5005efed2..6e4f37e40 100644 --- a/src/RESPite/Internal/RespMessageBase.cs +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -17,49 +17,53 @@ internal abstract class RespMessageBase : IValueTaskSource private ReadOnlySequence _request; public ref readonly CancellationToken CancellationToken => ref _cancellationToken; - protected const int - Flag_Sent = 1 << 0, // the request has been sent - Flag_OutcomeKnown = 1 << 1, // controls which code flow gets to set an outcome - Flag_Complete = 1 << 2, // indicates whether all follow-up has completed - Flag_NoPulse = 1 << 4, // don't pulse when completing - either async, or timeout - Flag_Parser = 1 << 5, // we have a parser - Flag_MetadataParser = 1 << 6, // the parser wants to consume metadata - Flag_InlineParser = 1 << 7, // we can safely use the parser on the IO thread - Flag_Doomed = 1 << 8; // something went wrong, do not recycle - - protected int Flags => Volatile.Read(ref _flags); + [Flags] + protected enum StateFlags : int + { + None = 0, + IsSent = 1 << 0, // the request has been sent + OutcomeKnown = 1 << 1, // controls which code flow gets to set an outcome + Complete = 1 << 2, // indicates whether all follow-up has completed + NoPulse = 1 << 4, // don't pulse when completing - either async, or timeout + Doomed = 1 << 5, // something went wrong, do not recycle + HasParser = 1 << 6, // we have a parser + MetadataParser = 1 << 7, // the parser wants to consume metadata + InlineParser = 1 << 8, // we can safely use the parser on the IO thread + } + + protected StateFlags Flags => (StateFlags)Volatile.Read(ref _flags); public virtual int MessageCount => 1; protected void InitParser(object? parser) { if (parser is null) { - SetFlag(Flag_InlineParser); // F+F + SetFlag(StateFlags.InlineParser); // F+F } else { - int flags = Flag_Parser; + var flags = StateFlags.HasParser; // detect parsers that want to manually parse attributes, errors, etc. - if (parser is IRespMetadataParser) flags |= Flag_MetadataParser; + if (parser is IRespMetadataParser) flags |= StateFlags.MetadataParser; // detect fast, internal, non-allocating parsers (int, bool, etc.) - if (parser is IRespInlineParser) flags |= Flag_InlineParser; + if (parser is IRespInlineParser) flags |= StateFlags.InlineParser; SetFlag(flags); } } - public bool AllowInlineParsing => HasFlag(Flag_InlineParser); + public bool AllowInlineParsing => HasFlag(StateFlags.InlineParser); public bool TrySetResult(short token, ref RespReader reader) { - if (HasFlag(Flag_OutcomeKnown) | Token != token) return false; - var flags = _flags & (Flag_MetadataParser | Flag_Parser); + var flags = Flags & (StateFlags.MetadataParser | StateFlags.HasParser | StateFlags.OutcomeKnown); + if ((flags & StateFlags.OutcomeKnown) != 0 | Token != token) return false; switch (flags) { - case Flag_Parser: - case Flag_Parser | Flag_MetadataParser: + case StateFlags.HasParser: + case StateFlags.HasParser | StateFlags.MetadataParser: try { - if ((flags & Flag_MetadataParser) == 0) + if ((flags & StateFlags.MetadataParser) == 0) { reader.MoveNext(); } @@ -101,36 +105,36 @@ public bool TrySetResult(short token, in ReadOnlySequence response) public bool IsSent(short token) { CheckToken(token); - return HasFlag(Flag_Sent); + return HasFlag(StateFlags.IsSent); } - protected bool SetFlag(int flag) + protected bool SetFlag(StateFlags flag) { Debug.Assert(flag != 0, "trying to set a zero flag"); #if NET5_0_OR_GREATER - return (Interlocked.Or(ref _flags, flag) & flag) == 0; + return (Interlocked.Or(ref _flags, (int)flag) & (int)flag) == 0; #else while (true) { var oldValue = Volatile.Read(ref _flags); - var newValue = oldValue | flag; + var newValue = oldValue | (int)flag; if (oldValue == newValue || Interlocked.CompareExchange(ref _flags, newValue, oldValue) == oldValue) { - return (oldValue & flag) == 0; + return (oldValue & (int)flag) == 0; } } #endif } // in the "any" sense - protected bool HasFlag(int flag) => (Volatile.Read(ref _flags) & flag) != 0; + protected bool HasFlag(StateFlags flag) => (Volatile.Read(ref _flags) & (int)flag) != 0; public void Init(bool sent, CancellationToken cancellationToken) { - Debug.Assert(_flags == 0, "flags should be zero"); + Debug.Assert(Flags is 0 or StateFlags.InlineParser, $"flags should be zero; got {Flags}"); Debug.Assert(_requestRefCount == 0, "trying to set a request more than once"); - if (sent) SetFlag(Flag_Sent); + if (sent) SetFlag(StateFlags.IsSent); if (cancellationToken.CanBeCanceled) { _cancellationToken = cancellationToken; @@ -195,7 +199,7 @@ public bool TryReserveRequest(short token, out ReadOnlySequence payload, b if (Interlocked.CompareExchange(ref _requestRefCount, checked(oldCount + 1), oldCount) == oldCount) { - if (recordSent) SetFlag(Flag_Sent); + if (recordSent) SetFlag(StateFlags.IsSent); payload = _request; return true; @@ -254,7 +258,7 @@ private bool TrySetOutcomeKnown(short token, bool withSuccess) protected bool TrySetOutcomeKnownPrecheckedToken(bool withSuccess) { - if (!SetFlag(Flag_OutcomeKnown)) return false; + if (!SetFlag(StateFlags.OutcomeKnown)) return false; UnregisterCancellation(); TryReleaseRequest(); // we won't be needing this again @@ -310,10 +314,10 @@ private bool TrySetExceptionPrecheckedToken(Exception exception) protected void SetFullyComplete(bool success) { - var pulse = !HasFlag(Flag_NoPulse); + var pulse = !HasFlag(StateFlags.NoPulse); SetFlag(success - ? (Flag_Complete | Flag_NoPulse) - : (Flag_Complete | Flag_NoPulse | Flag_Doomed)); + ? (StateFlags.Complete | StateFlags.NoPulse) + : (StateFlags.Complete | StateFlags.NoPulse | StateFlags.Doomed)); // for safety, always take the lock unless we know they've actively exited if (pulse) diff --git a/src/RESPite/Internal/RespMessageBaseT.cs b/src/RESPite/Internal/RespMessageBaseT.cs index f4e90e6f5..c26d2beb8 100644 --- a/src/RESPite/Internal/RespMessageBaseT.cs +++ b/src/RESPite/Internal/RespMessageBaseT.cs @@ -36,7 +36,7 @@ public override void OnCompleted( ValueTaskSourceOnCompletedFlags flags) { CheckToken(token); - SetFlag(Flag_NoPulse); // async doesn't need to be pulsed + SetFlag(StateFlags.NoPulse); // async doesn't need to be pulsed _asyncCore.OnCompleted(continuation, state, token, flags); } @@ -47,8 +47,8 @@ public override void OnCompletedWithNotSentDetection( ValueTaskSourceOnCompletedFlags flags) { CheckToken(token); - if (!HasFlag(Flag_Sent)) SetNotSentAsync(token); - SetFlag(Flag_NoPulse); // async doesn't need to be pulsed + if (!HasFlag(StateFlags.IsSent)) SetNotSentAsync(token); + SetFlag(StateFlags.NoPulse); // async doesn't need to be pulsed _asyncCore.OnCompleted(continuation, state, token, flags); } @@ -60,11 +60,11 @@ private protected override void SetRunContinuationsAsynchronously(bool value) public TResponse Wait(short token, TimeSpan timeout) { - switch (Flags & (Flag_Complete | Flag_Sent)) + switch (Flags & (StateFlags.Complete | StateFlags.IsSent)) { - case Flag_Sent: // this is the normal case + case StateFlags.IsSent: // this is the normal case break; - case Flag_Complete | Flag_Sent: // already complete + case StateFlags.Complete | StateFlags.IsSent: // already complete return GetResult(token); default: ThrowNotSent(token); // always throws @@ -75,10 +75,10 @@ public TResponse Wait(short token, TimeSpan timeout) CheckToken(token); lock (this) { - switch (Flags & (Flag_Complete | Flag_NoPulse)) + switch (Flags & (StateFlags.Complete | StateFlags.NoPulse)) { - case Flag_NoPulse | Flag_Complete: - case Flag_Complete: + case StateFlags.NoPulse | StateFlags.Complete: + case StateFlags.Complete: break; // fine, we're complete case 0: // THIS IS OUR EXPECTED BRANCH; not complete, and will pulse @@ -89,11 +89,11 @@ public TResponse Wait(short token, TimeSpan timeout) else if (!Monitor.Wait(this, timeout)) { isTimeout = true; - SetFlag(Flag_NoPulse); // no point in being woken, we're exiting + SetFlag(StateFlags.NoPulse); // no point in being woken, we're exiting } break; - case Flag_NoPulse: + case StateFlags.NoPulse: ThrowWillNotPulse(); break; } @@ -133,7 +133,7 @@ private TResponse ThrowFailure(short token) public TResponse GetResult(short token) { // failure uses some try/catch logic, let's put that to one side - if (HasFlag(Flag_Doomed)) return ThrowFailure(token); + if (HasFlag(StateFlags.Doomed)) return ThrowFailure(token); var result = _asyncCore.GetResult(token); /* If we get here, we're successful; increment "version"/"token" *immediately*. Technically diff --git a/src/RESPite/Internal/RespMultiMessage.cs b/src/RESPite/Internal/RespMultiMessage.cs index 58f41fb08..6ac129fa2 100644 --- a/src/RESPite/Internal/RespMultiMessage.cs +++ b/src/RESPite/Internal/RespMultiMessage.cs @@ -18,7 +18,7 @@ internal static RespMultiMessage Get(RespOperation[] oversized, int count) _threadStaticSpare = null; obj._oversized = oversized; obj._count = count; - obj.SetFlag(Flag_Parser | Flag_MetadataParser); + obj.SetFlag(StateFlags.HasParser | StateFlags.MetadataParser); return obj; } diff --git a/src/RESPite/Messages/RespReader.AggregateEnumerator.cs b/src/RESPite/Messages/RespReader.AggregateEnumerator.cs index 99e3aa2ca..d325789f6 100644 --- a/src/RESPite/Messages/RespReader.AggregateEnumerator.cs +++ b/src/RESPite/Messages/RespReader.AggregateEnumerator.cs @@ -144,7 +144,7 @@ public void MovePast(out RespReader reader) public void DemandNext() { - if (!MoveNext()) ThrowEOF(); + if (!MoveNext()) ThrowEof(); Value.MoveNext(); // skip any attributes etc } @@ -158,7 +158,7 @@ public void FillAll(scoped Span target, Projection projection) { for (int i = 0; i < target.Length; i++) { - if (!MoveNext()) ThrowEOF(); + if (!MoveNext()) ThrowEof(); Value.MoveNext(); // skip any attributes etc target[i] = projection(ref Value); diff --git a/src/RESPite/Messages/RespReader.ScalarEnumerator.cs b/src/RESPite/Messages/RespReader.ScalarEnumerator.cs index a2894393d..9e8ffbe70 100644 --- a/src/RESPite/Messages/RespReader.ScalarEnumerator.cs +++ b/src/RESPite/Messages/RespReader.ScalarEnumerator.cs @@ -47,7 +47,7 @@ private void InitSegment() _tail = _reader._tail; _offset = CurrentLength = 0; _remaining = _reader._length; - if (_reader.TotalAvailable < _remaining) ThrowEOF(); + if (_reader.TotalAvailable < _remaining) ThrowEof(); } /// @@ -68,7 +68,7 @@ public bool MoveNext() } // otherwise, we expect more tail data - if (_tail is null) ThrowEOF(); + if (_tail is null) ThrowEof(); _current = _tail.Memory.Span; _offset = 0; diff --git a/src/RESPite/Messages/RespReader.Utils.cs b/src/RESPite/Messages/RespReader.Utils.cs index 01caecd6a..da6b641d8 100644 --- a/src/RESPite/Messages/RespReader.Utils.cs +++ b/src/RESPite/Messages/RespReader.Utils.cs @@ -97,7 +97,7 @@ private static void ThrowProtocolFailure(string message) => throw new InvalidOperationException("RESP protocol failure: " + message); // protocol exception? [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] - internal static void ThrowEOF() => throw new EndOfStreamException(); + internal static void ThrowEof() => throw new EndOfStreamException(); [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] private static void ThrowFormatException() => throw new FormatException(); @@ -218,7 +218,7 @@ private void RawFillBytes(scoped Span target) } } while (TryMoveToNextSegment()); - ThrowEOF(); + ThrowEof(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -298,7 +298,7 @@ private string GetDebuggerDisplay() return ToString(); } - internal int GetInitialScanCount(out ushort streamingAggregateDepth) + internal readonly int GetInitialScanCount(out ushort streamingAggregateDepth) { // this is *similar* to GetDelta, but: without any discount for attributes switch (_flags & (RespFlags.IsAggregate | RespFlags.IsStreaming)) diff --git a/src/RESPite/Messages/RespReader.cs b/src/RESPite/Messages/RespReader.cs index ee13aaf8a..9b711ea8b 100644 --- a/src/RESPite/Messages/RespReader.cs +++ b/src/RESPite/Messages/RespReader.cs @@ -160,7 +160,7 @@ private readonly int AggregateLengthSlow() var reader = Clone(); while (true) { - if (!reader.TryMoveNext()) ThrowEOF(); + if (!reader.TryMoveNext()) ThrowEof(); if (reader.Prefix == RespPrefix.StreamTerminator) { return count; @@ -234,7 +234,7 @@ public void DemandEnd() { while (IsStreamingScalar) { - if (!TryReadNext()) ThrowEOF(); + if (!TryReadNext()) ThrowEof(); } if (TryReadNext()) { @@ -284,7 +284,7 @@ public bool TryMoveNext() { while (IsStreamingScalar) // close out the current streaming scalar { - if (!TryReadNextSkipAttributes()) ThrowEOF(); + if (!TryReadNextSkipAttributes()) ThrowEof(); } if (TryReadNextSkipAttributes()) @@ -305,7 +305,7 @@ public bool TryMoveNext(bool checkError) { while (IsStreamingScalar) // close out the current streaming scalar { - if (!TryReadNextSkipAttributes()) ThrowEOF(); + if (!TryReadNextSkipAttributes()) ThrowEof(); } if (TryReadNextSkipAttributes()) @@ -328,7 +328,7 @@ public bool TryMoveNext(RespAttributeReader respAttributeReader, ref T att { while (IsStreamingScalar) // close out the current streaming scalar { - if (!TryReadNextSkipAttributes()) ThrowEOF(); + if (!TryReadNextSkipAttributes()) ThrowEof(); } if (TryReadNextProcessAttributes(respAttributeReader, ref attributes)) @@ -360,7 +360,7 @@ public bool TryMoveNext(RespPrefix prefix) /// If the data contains an explicit error element. public void MoveNext() { - if (!TryMoveNext()) ThrowEOF(); + if (!TryMoveNext()) ThrowEof(); } /// @@ -373,7 +373,7 @@ public void MoveNext() /// The type of data represented by this reader. public void MoveNext(RespAttributeReader respAttributeReader, ref T attributes) { - if (!TryMoveNext(respAttributeReader, ref attributes)) ThrowEOF(); + if (!TryMoveNext(respAttributeReader, ref attributes)) ThrowEof(); } private bool MoveNextStreamingScalar() @@ -392,7 +392,7 @@ private bool MoveNextStreamingScalar() return _length > 0; } } - ThrowEOF(); // we should have found something! + ThrowEof(); // we should have found something! } return false; } @@ -479,7 +479,7 @@ public void SkipChildren() default: // something more complex RespScanState state = new(in this); - if (!state.TryRead(ref this, out _)) ThrowEOF(); + if (!state.TryRead(ref this, out _)) ThrowEof(); break; } } @@ -1246,6 +1246,21 @@ internal readonly bool IsOK() // go mad with this, because it is used so often public readonly bool Is(ReadOnlySpan value) => TryGetSpan(out var span) ? span.SequenceEqual(value) : IsSlow(value); + /// + /// Indicates whether the current element is a scalar with a value that matches the provided . + /// + /// The payload value to verify. + public readonly bool Is(ReadOnlySpan value) + { + var bytes = RespConstants.UTF8.GetMaxByteCount(value.Length); + byte[]? oversized = null; + Span buffer = bytes <= 128 ? stackalloc byte[128] : (oversized = ArrayPool.Shared.Rent(bytes)); + bytes = RespConstants.UTF8.GetBytes(value, buffer); + bool result = Is(buffer.Slice(0, bytes)); + if (oversized is not null) ArrayPool.Shared.Return(oversized); + return result; + } + internal readonly bool IsInlneCpuUInt32(uint value) { if (IsInlineScalar && _length == sizeof(uint)) diff --git a/src/RESPite/RESPite.csproj b/src/RESPite/RESPite.csproj index f3c5ec8a4..c7526160c 100644 --- a/src/RESPite/RESPite.csproj +++ b/src/RESPite/RESPite.csproj @@ -17,7 +17,7 @@ - + diff --git a/tests/RESP.Core.Tests/BasicIntegrationTests.cs b/tests/RESPite.Tests/BasicIntegrationTests.cs similarity index 99% rename from tests/RESP.Core.Tests/BasicIntegrationTests.cs rename to tests/RESPite.Tests/BasicIntegrationTests.cs index 9c624b30a..af1fd318f 100644 --- a/tests/RESP.Core.Tests/BasicIntegrationTests.cs +++ b/tests/RESPite.Tests/BasicIntegrationTests.cs @@ -8,7 +8,7 @@ using RESPite.Redis.Alt; // needed for AsStrings() etc using Xunit; -namespace RESP.Core.Tests; +namespace RESPite.Tests; public class BasicIntegrationTests(ConnectionFixture fixture, ITestOutputHelper log) : IntegrationTestBase(fixture, log) { diff --git a/tests/RESP.Core.Tests/BatchTests.cs b/tests/RESPite.Tests/BatchTests.cs similarity index 86% rename from tests/RESP.Core.Tests/BatchTests.cs rename to tests/RESPite.Tests/BatchTests.cs index bab447c7a..61d35a97d 100644 --- a/tests/RESP.Core.Tests/BatchTests.cs +++ b/tests/RESPite.Tests/BatchTests.cs @@ -3,7 +3,7 @@ using RESPite; using Xunit; -namespace RESP.Core.Tests; +namespace RESPite.Tests; public partial class BatchTests { @@ -16,7 +16,7 @@ public async Task TestInfrastructure() await TestServer.Execute(ctx => BarAsync(ctx), "*1\r\n$3\r\nbar\r\n", "+OK\r\n"); } - [Fact(Timeout = 1000)] + [Fact(Timeout = 500)] // this should be very fast unless something is very wrong public async Task SimpleBatching() { // server setup @@ -76,6 +76,13 @@ public async Task SimpleBatching() Assert.Equal(7, await c); Assert.Equal(8, await d); Assert.Equal(9, await e); + + // but can only be awaited once + await Assert.ThrowsAsync(async () => await a); + await Assert.ThrowsAsync(async () => await b); + await Assert.ThrowsAsync(async () => await c); + await Assert.ThrowsAsync(async () => await d); + await Assert.ThrowsAsync(async () => await e); } [RespCommand] diff --git a/tests/RESP.Core.Tests/BlockBufferTests.cs b/tests/RESPite.Tests/BlockBufferTests.cs similarity index 99% rename from tests/RESP.Core.Tests/BlockBufferTests.cs rename to tests/RESPite.Tests/BlockBufferTests.cs index 6cf09d701..cac8deafd 100644 --- a/tests/RESP.Core.Tests/BlockBufferTests.cs +++ b/tests/RESPite.Tests/BlockBufferTests.cs @@ -6,7 +6,7 @@ using RESPite.Internal; using Xunit; -namespace RESP.Core.Tests; +namespace RESPite.Tests; public class BlockBufferTests(ITestOutputHelper log) { diff --git a/tests/RESP.Core.Tests/ConnectionFixture.cs b/tests/RESPite.Tests/ConnectionFixture.cs similarity index 79% rename from tests/RESP.Core.Tests/ConnectionFixture.cs rename to tests/RESPite.Tests/ConnectionFixture.cs index 0dca06726..4eb910431 100644 --- a/tests/RESP.Core.Tests/ConnectionFixture.cs +++ b/tests/RESPite.Tests/ConnectionFixture.cs @@ -1,13 +1,11 @@ using System; using System.Net; -using RESP.Core.Tests; -using RESPite; using RESPite.Connections; using Xunit; -[assembly: AssemblyFixture(typeof(ConnectionFixture))] +[assembly: AssemblyFixture(typeof(RESPite.Tests.ConnectionFixture))] -namespace RESP.Core.Tests; +namespace RESPite.Tests; public class ConnectionFixture : IDisposable { diff --git a/tests/RESP.Core.Tests/CycleBufferTests.cs b/tests/RESPite.Tests/CycleBufferTests.cs similarity index 99% rename from tests/RESP.Core.Tests/CycleBufferTests.cs rename to tests/RESPite.Tests/CycleBufferTests.cs index ff0722aec..bbffdb51b 100644 --- a/tests/RESP.Core.Tests/CycleBufferTests.cs +++ b/tests/RESPite.Tests/CycleBufferTests.cs @@ -6,7 +6,7 @@ using RESPite.Messages; using Xunit; -namespace RESP.Core.Tests; +namespace RESPite.Tests; public class CycleBufferTests { diff --git a/tests/RESP.Core.Tests/IntegrationTestBase.cs b/tests/RESPite.Tests/IntegrationTestBase.cs similarity index 96% rename from tests/RESP.Core.Tests/IntegrationTestBase.cs rename to tests/RESPite.Tests/IntegrationTestBase.cs index c76caff69..117f447b2 100644 --- a/tests/RESP.Core.Tests/IntegrationTestBase.cs +++ b/tests/RESPite.Tests/IntegrationTestBase.cs @@ -3,7 +3,7 @@ using RESPite.Redis.Alt; using Xunit; -namespace RESP.Core.Tests; +namespace RESPite.Tests; public abstract class IntegrationTestBase(ConnectionFixture fixture, ITestOutputHelper log) { diff --git a/tests/RESP.Core.Tests/OperationUnitTests.cs b/tests/RESPite.Tests/OperationUnitTests.cs similarity index 92% rename from tests/RESP.Core.Tests/OperationUnitTests.cs rename to tests/RESPite.Tests/OperationUnitTests.cs index 5872a080f..46310f86f 100644 --- a/tests/RESP.Core.Tests/OperationUnitTests.cs +++ b/tests/RESPite.Tests/OperationUnitTests.cs @@ -7,7 +7,7 @@ using Xunit; using Xunit.Internal; -namespace RESP.Core.Tests; +namespace RESPite.Tests; [SuppressMessage( "Usage", @@ -51,6 +51,7 @@ public void ManuallyImplementedAsync_NotSent_Untyped(bool sent, bool @unsafe) var ex = Assert.Throws(() => awaiter.GetResult()); Assert.Contains("This command has not yet been sent", ex.Message); } + Assert.Throws(() => awaiter.GetResult()); } @@ -88,6 +89,7 @@ public void ManuallyImplementedAsync_NotSent_Typed(bool sent, bool @unsafe) var ex = Assert.Throws(() => awaiter.GetResult()); Assert.Contains("This command has not yet been sent", ex.Message); } + Assert.Throws(() => awaiter.GetResult()); } @@ -125,6 +127,7 @@ public void ManuallyImplementedAsync_NotSent_Stateful(bool sent, bool @unsafe) var ex = Assert.Throws(() => awaiter.GetResult()); Assert.Contains("This command has not yet been sent", ex.Message); } + Assert.Throws(() => awaiter.GetResult()); } @@ -152,9 +155,7 @@ public async Task UnsentNotDetected_ValueTask_Async() cts.CancelAfter(100); var op = RespOperation.Create(out var remote, false, cts.Token); var ex = await Assert.ThrowsAsync(async () => await op.AsValueTask()); - Assert.True(ex.CancellationToken.IsCancellationRequested, "CT token should be intact"); - Assert.True(cts.Token != CancellationToken, "should not be test CT"); - Assert.True(cts.Token == ex.CancellationToken, "should be local CT"); + AssertCT(ex.CancellationToken, cts.Token); } [Fact(Timeout = 1000)] @@ -163,10 +164,18 @@ public async Task UnsentNotDetected_Task_Async() using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken); cts.CancelAfter(100); var op = RespOperation.Create(out var remote, false, cts.Token); - var ex = await Assert.ThrowsAsync(async () => await op.AsTask()); - Assert.True(ex.CancellationToken.IsCancellationRequested, "CT token should be intact"); - Assert.True(cts.Token != CancellationToken, "should not be test CT"); - Assert.True(cts.Token == ex.CancellationToken, "should be local CT"); + var ex = await Assert.ThrowsAnyAsync(async () => await op.AsTask()); + AssertCT(ex.CancellationToken, cts.Token); + } + + private static void AssertCT(CancellationToken actual, CancellationToken expected) + { + string problems = ""; + if (actual == CancellationToken.None) problems += "default;"; + if (!actual.IsCancellationRequested) problems += "not cancelled;"; + if (actual == CancellationToken) problems += "test CT;"; + if (actual != expected) problems += "not local CT"; + Assert.Empty(problems.TrimEnd(';')); } [Fact(Timeout = 1000)] diff --git a/tests/RESP.Core.Tests/RESP.Core.Tests.csproj b/tests/RESPite.Tests/RESPite.Tests.csproj similarity index 100% rename from tests/RESP.Core.Tests/RESP.Core.Tests.csproj rename to tests/RESPite.Tests/RESPite.Tests.csproj diff --git a/tests/RESP.Core.Tests/RedisStringsIntegrationTests.cs b/tests/RESPite.Tests/RedisStringsIntegrationTests.cs similarity index 97% rename from tests/RESP.Core.Tests/RedisStringsIntegrationTests.cs rename to tests/RESPite.Tests/RedisStringsIntegrationTests.cs index b4cdb496e..fb0594ac1 100644 --- a/tests/RESP.Core.Tests/RedisStringsIntegrationTests.cs +++ b/tests/RESPite.Tests/RedisStringsIntegrationTests.cs @@ -4,7 +4,7 @@ using Xunit; using FactAttribute = StackExchange.Redis.Tests.FactAttribute; -namespace RESP.Core.Tests; +namespace RESPite.Tests; public class RedisStringsIntegrationTests(ConnectionFixture fixture, ITestOutputHelper log) : IntegrationTestBase(fixture, log) diff --git a/tests/RESPite.Tests/RespReaderTests.cs b/tests/RESPite.Tests/RespReaderTests.cs new file mode 100644 index 000000000..1cc04bba2 --- /dev/null +++ b/tests/RESPite.Tests/RespReaderTests.cs @@ -0,0 +1,863 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using RESPite.Internal; +using RESPite.Messages; +using Xunit; +using Xunit.Sdk; +using Xunit.v3; + +namespace RESPite.Tests; + +public class RespReaderTests(ITestOutputHelper logger) +{ + public readonly struct RespPayload(string label, ReadOnlySequence payload, byte[] expected, bool? outOfBand, int count) + { + public override string ToString() => Label; + public string Label { get; } = label; + public ReadOnlySequence PayloadRaw { get; } = payload; + public int Length { get; } = CheckPayload(payload, expected, outOfBand, count); + private static int CheckPayload(scoped in ReadOnlySequence actual, byte[] expected, bool? outOfBand, int count) + { + Assert.Equal(expected.LongLength, actual.Length); + var pool = ArrayPool.Shared.Rent(expected.Length); + actual.CopyTo(pool); + bool isSame = pool.AsSpan(0, expected.Length).SequenceEqual(expected); + ArrayPool.Shared.Return(pool); + Assert.True(isSame, "Data mismatch"); + + // verify that the data exactly passes frame-scanning + long totalBytes = 0; + RespReader reader = new(actual); + while (count > 0) + { + RespScanState state = default; + Assert.True(state.TryRead(ref reader, out long bytesRead)); + totalBytes += bytesRead; + Assert.True(state.IsComplete, nameof(state.IsComplete)); + if (outOfBand.HasValue) + { + if (outOfBand.Value) + { + Assert.Equal(RespPrefix.Push, state.Prefix); + } + else + { + Assert.NotEqual(RespPrefix.Push, state.Prefix); + } + } + count--; + } + Assert.Equal(expected.Length, totalBytes); + reader.DemandEnd(); + return expected.Length; + } + + public RespReader Reader() => new(PayloadRaw); + } + + public sealed class RespAttribute : DataAttribute + { + public override bool SupportsDiscoveryEnumeration() => true; + + private readonly object _value; + public bool OutOfBand { get; init; } = false; + + private bool? EffectiveOutOfBand => Count == 1 ? OutOfBand : default(bool?); + public int Count { get; init; } = 1; + + public RespAttribute(string value) => _value = value; + public RespAttribute(params string[] values) => _value = values; + + public override ValueTask> GetData(MethodInfo testMethod, DisposalTracker disposalTracker) + => new(GetData(testMethod).ToArray()); + + public IEnumerable GetData(MethodInfo testMethod) + { + switch (_value) + { + case string s: + foreach (var item in GetVariants(s, EffectiveOutOfBand, Count)) + { + yield return new TheoryDataRow(item); + } + break; + case string[] arr: + foreach (string s in arr) + { + foreach (var item in GetVariants(s, EffectiveOutOfBand, Count)) + { + yield return new TheoryDataRow(item); + } + } + break; + } + } + + private static IEnumerable GetVariants(string value, bool? outOfBand, int count) + { + var bytes = Encoding.UTF8.GetBytes(value); + + // all in one + yield return new("Right-sized", new(bytes), bytes, outOfBand, count); + + var bigger = new byte[bytes.Length + 4]; + bytes.CopyTo(bigger.AsSpan(2, bytes.Length)); + bigger.AsSpan(0, 2).Fill(0xFF); + bigger.AsSpan(bytes.Length + 2, 2).Fill(0xFF); + + // all in one, oversized + yield return new("Oversized", new(bigger, 2, bytes.Length), bytes, outOfBand, count); + + // two-chunks + for (int i = 0; i <= bytes.Length; i++) + { + int offset = 2 + i; + var left = new Segment(new ReadOnlyMemory(bigger, 0, offset), null); + var right = new Segment(new ReadOnlyMemory(bigger, offset, bigger.Length - offset), left); + yield return new($"Split:{i}", new ReadOnlySequence(left, 2, right, right.Length - 2), bytes, outOfBand, count); + } + + // N-chunks + Segment head = new(new(bytes, 0, 1), null), tail = head; + for (int i = 1; i < bytes.Length; i++) + { + tail = new(new(bytes, i, 1), tail); + } + yield return new("Chunk-per-byte", new(head, 0, tail, 1), bytes, outOfBand, count); + } + } + + [Theory, Resp("$3\r\n128\r\n")] + public void HandleSplitTokens(RespPayload payload) + { + RespReader reader = payload.Reader(); + RespScanState scan = default; + bool readResult = scan.TryRead(ref reader, out _); + logger.WriteLine(scan.ToString()); + Assert.Equal(payload.Length, reader.BytesConsumed); + Assert.True(readResult); + } + + // the examples from https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md + [Theory, Resp("$11\r\nhello world\r\n", "$?\r\n;6\r\nhello \r\n;5\r\nworld\r\n;0\r\n")] + public void BlobString(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.BulkString); + Assert.True(reader.Is("hello world"u8)); + Assert.Equal("hello world", reader.ReadString()); + Assert.Equal("hello world", reader.ReadString(out var prefix)); + Assert.Equal("", prefix); +#if NET7_0_OR_GREATER + Assert.Equal("hello world", reader.ParseChars()); +#endif + /* interestingly, string does not implement IUtf8SpanParsable +#if NET8_0_OR_GREATER + Assert.Equal("hello world", reader.ParseBytes()); +#endif + */ + reader.DemandEnd(); + } + + [Theory, Resp("$0\r\n\r\n", "$?\r\n;0\r\n")] + public void EmptyBlobString(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.BulkString); + Assert.True(reader.Is(""u8)); + Assert.Equal("", reader.ReadString()); + reader.DemandEnd(); + } + + [Theory, Resp("+hello world\r\n")] + public void SimpleString(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.SimpleString); + Assert.True(reader.Is("hello world"u8)); + Assert.Equal("hello world", reader.ReadString()); + Assert.Equal("hello world", reader.ReadString(out var prefix)); + Assert.Equal("", prefix); + reader.DemandEnd(); + } + + [Theory, Resp("-ERR this is the error description\r\n")] + public void SimpleError_ImplicitErrors(RespPayload payload) + { + var ex = Assert.Throws(() => + { + var reader = payload.Reader(); + reader.MoveNext(); + }); + Assert.Equal("ERR this is the error description", ex.Message); + } + + [Theory, Resp("-ERR this is the error description\r\n")] + public void SimpleError_Careful(RespPayload payload) + { + var reader = payload.Reader(); + Assert.True(reader.TryReadNext()); + Assert.Equal(RespPrefix.SimpleError, reader.Prefix); + Assert.True(reader.Is("ERR this is the error description"u8)); + Assert.Equal("ERR this is the error description", reader.ReadString()); + reader.DemandEnd(); + } + + [Theory, Resp(":1234\r\n")] + public void Number(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Integer); + Assert.True(reader.Is("1234"u8)); + Assert.Equal("1234", reader.ReadString()); + Assert.Equal(1234, reader.ReadInt32()); + Assert.Equal(1234D, reader.ReadDouble()); + Assert.Equal(1234M, reader.ReadDecimal()); +#if NET7_0_OR_GREATER + Assert.Equal(1234, reader.ParseChars()); + Assert.Equal(1234D, reader.ParseChars()); + Assert.Equal(1234M, reader.ParseChars()); +#endif +#if NET8_0_OR_GREATER + Assert.Equal(1234, reader.ParseBytes()); + Assert.Equal(1234D, reader.ParseBytes()); + Assert.Equal(1234M, reader.ParseBytes()); +#endif + reader.DemandEnd(); + } + + [Theory, Resp("_\r\n")] + public void Null(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Null); + Assert.True(reader.Is(""u8)); + Assert.Null(reader.ReadString()); + reader.DemandEnd(); + } + + [Theory, Resp("$-1\r\n")] + public void NullString(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.BulkString); + Assert.True(reader.IsNull); + Assert.Null(reader.ReadString()); + Assert.Equal(0, reader.ScalarLength()); + Assert.True(reader.Is(""u8)); + Assert.True(reader.ScalarIsEmpty()); + + var iterator = reader.ScalarChunks(); + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + reader.DemandEnd(); + } + + [Theory, Resp(",1.23\r\n")] + public void Double(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Double); + Assert.True(reader.Is("1.23"u8)); + Assert.Equal("1.23", reader.ReadString()); + Assert.Equal(1.23D, reader.ReadDouble()); + Assert.Equal(1.23M, reader.ReadDecimal()); + reader.DemandEnd(); + } + + [Theory, Resp(":10\r\n")] + public void Integer_Simple(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Integer); + Assert.True(reader.Is("10"u8)); + Assert.Equal("10", reader.ReadString()); + Assert.Equal(10, reader.ReadInt32()); + Assert.Equal(10D, reader.ReadDouble()); + Assert.Equal(10M, reader.ReadDecimal()); + reader.DemandEnd(); + } + + [Theory, Resp(",10\r\n")] + public void Double_Simple(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Double); + Assert.True(reader.Is("10"u8)); + Assert.Equal("10", reader.ReadString()); + Assert.Equal(10, reader.ReadInt32()); + Assert.Equal(10D, reader.ReadDouble()); + Assert.Equal(10M, reader.ReadDecimal()); + reader.DemandEnd(); + } + + [Theory, Resp(",inf\r\n")] + public void Double_Infinity(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Double); + Assert.True(reader.Is("inf"u8)); + Assert.Equal("inf", reader.ReadString()); + var val = reader.ReadDouble(); + Assert.True(double.IsInfinity(val)); + Assert.True(double.IsPositiveInfinity(val)); + reader.DemandEnd(); + } + + [Theory, Resp(",+inf\r\n")] + public void Double_PosInfinity(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Double); + Assert.True(reader.Is("+inf"u8)); + Assert.Equal("+inf", reader.ReadString()); + var val = reader.ReadDouble(); + Assert.True(double.IsInfinity(val)); + Assert.True(double.IsPositiveInfinity(val)); + reader.DemandEnd(); + } + + [Theory, Resp(",-inf\r\n")] + public void Double_NegInfinity(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Double); + Assert.True(reader.Is("-inf"u8)); + Assert.Equal("-inf", reader.ReadString()); + var val = reader.ReadDouble(); + Assert.True(double.IsInfinity(val)); + Assert.True(double.IsNegativeInfinity(val)); + reader.DemandEnd(); + } + + [Theory, Resp(",nan\r\n")] + public void Double_NaN(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Double); + Assert.True(reader.Is("nan"u8)); + Assert.Equal("nan", reader.ReadString()); + var val = reader.ReadDouble(); + Assert.True(double.IsNaN(val)); + reader.DemandEnd(); + } + + [Theory, Resp("#t\r\n")] + public void Boolean_T(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Boolean); + Assert.True(reader.ReadBoolean()); + reader.DemandEnd(); + } + + [Theory, Resp("#f\r\n")] + public void Boolean_F(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Boolean); + Assert.False(reader.ReadBoolean()); + reader.DemandEnd(); + } + + [Theory, Resp(":1\r\n")] + public void Boolean_1(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Integer); + Assert.True(reader.ReadBoolean()); + reader.DemandEnd(); + } + + [Theory, Resp(":0\r\n")] + public void Boolean_0(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Integer); + Assert.False(reader.ReadBoolean()); + reader.DemandEnd(); + } + + [Theory, Resp("!21\r\nSYNTAX invalid syntax\r\n", "!?\r\n;6\r\nSYNTAX\r\n;15\r\n invalid syntax\r\n;0\r\n")] + public void BlobError_ImplicitErrors(RespPayload payload) + { + var ex = Assert.Throws(() => + { + var reader = payload.Reader(); + reader.MoveNext(); + }); + Assert.Equal("SYNTAX invalid syntax", ex.Message); + } + + [Theory, Resp("!21\r\nSYNTAX invalid syntax\r\n", "!?\r\n;6\r\nSYNTAX\r\n;15\r\n invalid syntax\r\n;0\r\n")] + public void BlobError_Careful(RespPayload payload) + { + var reader = payload.Reader(); + Assert.True(reader.TryReadNext()); + Assert.Equal(RespPrefix.BulkError, reader.Prefix); + Assert.True(reader.Is("SYNTAX invalid syntax"u8)); + Assert.Equal("SYNTAX invalid syntax", reader.ReadString()); + reader.DemandEnd(); + } + + [Theory, Resp("=15\r\ntxt:Some string\r\n", "=?\r\n;4\r\ntxt:\r\n;11\r\nSome string\r\n;0\r\n")] + public void VerbatimString(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.VerbatimString); + Assert.Equal("Some string", reader.ReadString()); + Assert.Equal("Some string", reader.ReadString(out var prefix)); + Assert.Equal("txt", prefix); + + Assert.Equal("Some string", reader.ReadString(out var prefix2)); + Assert.Same(prefix, prefix2); // check prefix recognized and reuse literal + reader.DemandEnd(); + } + + [Theory, Resp("(3492890328409238509324850943850943825024385\r\n")] + public void BigIntegers(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.BigInteger); + Assert.Equal("3492890328409238509324850943850943825024385", reader.ReadString()); +#if NET8_0_OR_GREATER + var actual = reader.ParseChars(chars => BigInteger.Parse(chars, CultureInfo.InvariantCulture)); + + var expected = BigInteger.Parse("3492890328409238509324850943850943825024385"); + Assert.Equal(expected, actual); +#endif + } + + [Theory, Resp("*3\r\n:1\r\n:2\r\n:3\r\n", "*?\r\n:1\r\n:2\r\n:3\r\n.\r\n")] + public void Array(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array); + Assert.Equal(3, reader.AggregateLength()); + var iterator = reader.AggregateChildren(); + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(1, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(2, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(3, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext(RespPrefix.Integer)); + iterator.MovePast(out reader); + reader.DemandEnd(); + + reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array); + int[] arr = new int[reader.AggregateLength()]; + int i = 0; + foreach (var sub in reader.AggregateChildren()) + { + sub.MoveNext(RespPrefix.Integer); + arr[i++] = sub.ReadInt32(); + sub.DemandEnd(); + } + iterator.MovePast(out reader); + reader.DemandEnd(); + + Assert.Equal([1, 2, 3], arr); + } + + [Theory, Resp("*-1\r\n")] + public void NullArray(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array); + Assert.True(reader.IsNull); + Assert.Equal(0, reader.AggregateLength()); + var iterator = reader.AggregateChildren(); + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + reader.DemandEnd(); + } + + [Theory, Resp("*2\r\n*3\r\n:1\r\n$5\r\nhello\r\n:2\r\n#f\r\n", "*?\r\n*?\r\n:1\r\n$5\r\nhello\r\n:2\r\n.\r\n#f\r\n.\r\n")] + public void NestedArray(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array); + + Assert.Equal(2, reader.AggregateLength()); + + var iterator = reader.AggregateChildren(); + Assert.True(iterator.MoveNext(RespPrefix.Array)); + + Assert.Equal(3, iterator.Value.AggregateLength()); + var subIterator = iterator.Value.AggregateChildren(); + Assert.True(subIterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(1, subIterator.Value.ReadInt64()); + subIterator.Value.DemandEnd(); + + Assert.True(subIterator.MoveNext(RespPrefix.BulkString)); + Assert.True(subIterator.Value.Is("hello"u8)); + subIterator.Value.DemandEnd(); + + Assert.True(subIterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(2, subIterator.Value.ReadInt64()); + subIterator.Value.DemandEnd(); + + Assert.False(subIterator.MoveNext()); + + Assert.True(iterator.MoveNext(RespPrefix.Boolean)); + Assert.False(iterator.Value.ReadBoolean()); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + + reader.DemandEnd(); + } + + [Theory, Resp("%2\r\n+first\r\n:1\r\n+second\r\n:2\r\n", "%?\r\n+first\r\n:1\r\n+second\r\n:2\r\n.\r\n")] + public void Map(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Map); + + Assert.Equal(4, reader.AggregateLength()); + + var iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("first".AsSpan())); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(1, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("second"u8)); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(2, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + + iterator.MovePast(out reader); + reader.DemandEnd(); + } + + [Theory, Resp("~5\r\n+orange\r\n+apple\r\n#t\r\n:100\r\n:999\r\n", "~?\r\n+orange\r\n+apple\r\n#t\r\n:100\r\n:999\r\n.\r\n")] + public void Set(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Set); + + Assert.Equal(5, reader.AggregateLength()); + + var iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("orange".AsSpan())); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("apple"u8)); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Boolean)); + Assert.True(iterator.Value.ReadBoolean()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(100, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(999, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + + iterator.MovePast(out reader); + reader.DemandEnd(); + } + + private sealed class TestAttributeReader : RespAttributeReader<(int Count, int Ttl, decimal A, decimal B)> + { + public override void Read(ref RespReader reader, ref (int Count, int Ttl, decimal A, decimal B) value) + { + value.Count += ReadKeyValuePairs(ref reader, ref value); + } + private TestAttributeReader() { } + public static readonly TestAttributeReader Instance = new(); + public static (int Count, int Ttl, decimal A, decimal B) Zero = (0, 0, 0, 0); + public override bool ReadKeyValuePair(scoped ReadOnlySpan key, ref RespReader reader, ref (int Count, int Ttl, decimal A, decimal B) value) + { + if (key.SequenceEqual("ttl"u8) && reader.IsScalar) + { + value.Ttl = reader.ReadInt32(); + } + else if (key.SequenceEqual("key-popularity"u8) && reader.IsAggregate) + { + ReadKeyValuePairs(ref reader, ref value); // recurse to process a/b below + } + else if (key.SequenceEqual("a"u8) && reader.IsScalar) + { + value.A = reader.ReadDecimal(); + } + else if (key.SequenceEqual("b"u8) && reader.IsScalar) + { + value.B = reader.ReadDecimal(); + } + else + { + return false; // not recognized + } + return true; // recognized + } + } + + [Theory, Resp( + "|1\r\n+key-popularity\r\n%2\r\n$1\r\na\r\n,0.1923\r\n$1\r\nb\r\n,0.0012\r\n*2\r\n:2039123\r\n:9543892\r\n", + "|1\r\n+key-popularity\r\n%2\r\n$1\r\na\r\n,0.1923\r\n$1\r\nb\r\n,0.0012\r\n*?\r\n:2039123\r\n:9543892\r\n.\r\n")] + public void AttributeRoot(RespPayload payload) + { + // ignore the attribute data + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array); + Assert.Equal(2, reader.AggregateLength()); + var iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(2039123, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(9543892, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + reader.DemandEnd(); + + // process the attribute data + var state = TestAttributeReader.Zero; + reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array, TestAttributeReader.Instance, ref state); + Assert.Equal(1, state.Count); + Assert.Equal(0.1923M, state.A); + Assert.Equal(0.0012M, state.B); + state = TestAttributeReader.Zero; + + Assert.Equal(2, reader.AggregateLength()); + iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer, TestAttributeReader.Instance, ref state)); + Assert.Equal(2039123, iterator.Value.ReadInt32()); + Assert.Equal(0, state.Count); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer, TestAttributeReader.Instance, ref state)); + Assert.Equal(9543892, iterator.Value.ReadInt32()); + Assert.Equal(0, state.Count); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + reader.DemandEnd(); + } + + [Theory, Resp("*3\r\n:1\r\n:2\r\n|1\r\n+ttl\r\n:3600\r\n:3\r\n", "*?\r\n:1\r\n:2\r\n|1\r\n+ttl\r\n:3600\r\n:3\r\n.\r\n")] + public void AttributeInner(RespPayload payload) + { + // ignore the attribute data + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array); + Assert.Equal(3, reader.AggregateLength()); + var iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(1, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(2, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(3, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + reader.DemandEnd(); + + // process the attribute data + var state = TestAttributeReader.Zero; + reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array, TestAttributeReader.Instance, ref state); + Assert.Equal(0, state.Count); + Assert.Equal(3, reader.AggregateLength()); + iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer, TestAttributeReader.Instance, ref state)); + Assert.Equal(0, state.Count); + Assert.Equal(1, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer, TestAttributeReader.Instance, ref state)); + Assert.Equal(0, state.Count); + Assert.Equal(2, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer, TestAttributeReader.Instance, ref state)); + Assert.Equal(1, state.Count); + Assert.Equal(3600, state.Ttl); + state = TestAttributeReader.Zero; // reset + Assert.Equal(3, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext(TestAttributeReader.Instance, ref state)); + Assert.Equal(0, state.Count); + iterator.MovePast(out reader); + reader.DemandEnd(); + } + + [Theory, Resp(">3\r\n+message\r\n+somechannel\r\n+this is the message\r\n", OutOfBand = true)] + public void Push(RespPayload payload) + { + // ignore the attribute data + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Push); + Assert.Equal(3, reader.AggregateLength()); + var iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("message"u8)); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("somechannel"u8)); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("this is the message"u8)); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + reader.DemandEnd(); + } + + [Theory, Resp(">3\r\n+message\r\n+somechannel\r\n+this is the message\r\n$9\r\nGet-Reply\r\n", Count = 2)] + public void PushThenGetReply(RespPayload payload) + { + // ignore the attribute data + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Push); + Assert.Equal(3, reader.AggregateLength()); + var iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("message"u8)); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("somechannel"u8)); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("this is the message"u8)); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + + reader.MoveNext(RespPrefix.BulkString); + Assert.True(reader.Is("Get-Reply"u8)); + reader.DemandEnd(); + } + + [Theory, Resp("$9\r\nGet-Reply\r\n>3\r\n+message\r\n+somechannel\r\n+this is the message\r\n", Count = 2)] + public void GetReplyThenPush(RespPayload payload) + { + // ignore the attribute data + var reader = payload.Reader(); + + reader.MoveNext(RespPrefix.BulkString); + Assert.True(reader.Is("Get-Reply"u8)); + + reader.MoveNext(RespPrefix.Push); + Assert.Equal(3, reader.AggregateLength()); + var iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("message"u8)); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("somechannel"u8)); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("this is the message"u8)); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + + reader.DemandEnd(); + } + + [Theory, Resp("*0\r\n$4\r\npass\r\n", "*1\r\n+ok\r\n$4\r\npass\r\n", "*-1\r\n$4\r\npass\r\n", "*?\r\n.\r\n$4\r\npass\r\n", Count = 2)] + public void ArrayThenString(RespPayload payload) + { + var reader = payload.Reader(); + Assert.True(reader.TryMoveNext(RespPrefix.Array)); + reader.SkipChildren(); + + Assert.True(reader.TryMoveNext(RespPrefix.BulkString)); + Assert.True(reader.Is("pass"u8)); + + reader.DemandEnd(); + + // and the same using child iterator + reader = payload.Reader(); + Assert.True(reader.TryMoveNext(RespPrefix.Array)); + var iterator = reader.AggregateChildren(); + iterator.MovePast(out reader); + + Assert.True(reader.TryMoveNext(RespPrefix.BulkString)); + Assert.True(reader.Is("pass"u8)); + + reader.DemandEnd(); + } + + private sealed class Segment : ReadOnlySequenceSegment + { + public override string ToString() => RespConstants.UTF8.GetString(Memory.Span) + .Replace("\r", "\\r").Replace("\n", "\\n"); + + public Segment(ReadOnlyMemory value, Segment? head) + { + Memory = value; + if (head is not null) + { + RunningIndex = head.RunningIndex + head.Memory.Length; + head.Next = this; + } + } + public bool IsEmpty => Memory.IsEmpty; + public int Length => Memory.Length; + } +} diff --git a/tests/RESPite.Tests/RespWriterTests.cs b/tests/RESPite.Tests/RespWriterTests.cs new file mode 100644 index 000000000..9a114dede --- /dev/null +++ b/tests/RESPite.Tests/RespWriterTests.cs @@ -0,0 +1,43 @@ +using System.Buffers; +using RESPite.Messages; +using Xunit; + +namespace RESPite.Tests; + +public class RespWriterTests +{ + [Theory] + [InlineData(0, "$1\r\n0\r\n")] + [InlineData(-1, "$2\r\n-1\r\n")] + [InlineData(-12, "$3\r\n-12\r\n")] + [InlineData(-123, "$4\r\n-123\r\n")] + [InlineData(-1234, "$5\r\n-1234\r\n")] + [InlineData(-12345, "$6\r\n-12345\r\n")] + [InlineData(-123456, "$7\r\n-123456\r\n")] + [InlineData(-1234567, "$8\r\n-1234567\r\n")] + [InlineData(-12345678, "$9\r\n-12345678\r\n")] + [InlineData(-123456789, "$10\r\n-123456789\r\n")] + [InlineData(-1234567890, "$11\r\n-1234567890\r\n")] + [InlineData(int.MinValue, "$11\r\n-2147483648\r\n")] + [InlineData(1, "$1\r\n1\r\n")] + [InlineData(12, "$2\r\n12\r\n")] + [InlineData(123, "$3\r\n123\r\n")] + [InlineData(1234, "$4\r\n1234\r\n")] + [InlineData(12345, "$5\r\n12345\r\n")] + [InlineData(123456, "$6\r\n123456\r\n")] + [InlineData(1234567, "$7\r\n1234567\r\n")] + [InlineData(12345678, "$8\r\n12345678\r\n")] + [InlineData(123456789, "$9\r\n123456789\r\n")] + [InlineData(1234567890, "$10\r\n1234567890\r\n")] + [InlineData(int.MaxValue, "$10\r\n2147483647\r\n")] + + public void BulkStringInteger(int value, string expected) + { + using var aw = new TestBufferWriter(); + var writer = new RespWriter(aw); + writer.WriteBulkString(value); + writer.Flush(); + var actual = aw.ToString(); + Assert.Equal(expected, actual); + } +} diff --git a/tests/RESPite.Tests/TestBufferWriter.cs b/tests/RESPite.Tests/TestBufferWriter.cs new file mode 100644 index 000000000..c258498dc --- /dev/null +++ b/tests/RESPite.Tests/TestBufferWriter.cs @@ -0,0 +1,52 @@ +using System; +using System.Buffers; +using System.Text; + +namespace RESPite.Tests; + +// note that ArrayBufferWriter{T} is not available on all target platforms +public sealed class TestBufferWriter : IBufferWriter, IDisposable +{ + private byte[] _buffer = []; + private int _committed; + + public override string ToString() => Encoding.UTF8.GetString(_buffer, 0, _committed); + public ReadOnlySpan Committed => _buffer.AsSpan(0, _committed); + + public void Advance(int count) + { + if (count < 0 | count + _committed > _buffer.Length) throw new ArgumentOutOfRangeException(nameof(count)); + _committed += count; + } + + private void Ensure(int sizeHint) + { + sizeHint = Math.Max(sizeHint, 128); + if (_buffer.Length < _committed + sizeHint) + { + var newBuffer = ArrayPool.Shared.Rent(Math.Max(_buffer.Length * 2, _committed + sizeHint)); + Committed.CopyTo(newBuffer); + ArrayPool.Shared.Return(_buffer); + _buffer = newBuffer; + } + } + + public Memory GetMemory(int sizeHint = 0) + { + Ensure(sizeHint); + return _buffer.AsMemory(_committed); + } + + public Span GetSpan(int sizeHint = 0) + { + Ensure(sizeHint); + return _buffer.AsSpan(_committed); + } + + public void Dispose() + { + _committed = 0; + ArrayPool.Shared.Return(_buffer); + _buffer = []; + } +} diff --git a/tests/RESP.Core.Tests/TestServer.cs b/tests/RESPite.Tests/TestServer.cs similarity index 99% rename from tests/RESP.Core.Tests/TestServer.cs rename to tests/RESPite.Tests/TestServer.cs index 2889e24ea..c19fcfb45 100644 --- a/tests/RESP.Core.Tests/TestServer.cs +++ b/tests/RESPite.Tests/TestServer.cs @@ -8,7 +8,7 @@ using RESPite.Internal; using Xunit; -namespace RESP.Core.Tests; +namespace RESPite.Tests; internal sealed class TestServer : IDisposable { From 11ee7b99060ddb60eea58dcc7190d598e265a222 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 3 Sep 2025 11:23:20 +0100 Subject: [PATCH 039/108] nits --- StackExchange.Redis.sln.DotSettings | 1 + src/RESPite/Internal/RespMessageBase.cs | 6 +++--- src/RESPite/Internal/RespMultiMessage.cs | 2 +- src/RESPite/Internal/RespStatefulMessage.cs | 2 +- src/RESPite/Internal/RespStatelessMessage.cs | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/StackExchange.Redis.sln.DotSettings b/StackExchange.Redis.sln.DotSettings index de893e54d..3f7abd6a9 100644 --- a/StackExchange.Redis.sln.DotSettings +++ b/StackExchange.Redis.sln.DotSettings @@ -1,4 +1,5 @@  OK PONG + True True \ No newline at end of file diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs index 6e4f37e40..b34c70f93 100644 --- a/src/RESPite/Internal/RespMessageBase.cs +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -1,7 +1,6 @@ using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Threading.Tasks.Sources; using RESPite.Messages; @@ -18,7 +17,7 @@ internal abstract class RespMessageBase : IValueTaskSource public ref readonly CancellationToken CancellationToken => ref _cancellationToken; [Flags] - protected enum StateFlags : int + protected enum StateFlags { None = 0, IsSent = 1 << 0, // the request has been sent @@ -167,7 +166,7 @@ protected void UnregisterCancellation() _cancellationToken = CancellationToken.None; } - public virtual void Reset(bool recycle) + protected virtual void Reset(bool recycle) { Debug.Assert( !recycle || OwnStatus == ValueTaskSourceStatus.Succeeded, @@ -253,6 +252,7 @@ protected void SetNotSentAsync(short token) // spoof untyped on top of typed void IValueTaskSource.GetResult(short token) => GetResultVoid(token); + // ReSharper disable once UnusedMember.Local private bool TrySetOutcomeKnown(short token, bool withSuccess) => Token == token && TrySetOutcomeKnownPrecheckedToken(withSuccess); diff --git a/src/RESPite/Internal/RespMultiMessage.cs b/src/RESPite/Internal/RespMultiMessage.cs index 6ac129fa2..451d936cf 100644 --- a/src/RESPite/Internal/RespMultiMessage.cs +++ b/src/RESPite/Internal/RespMultiMessage.cs @@ -29,7 +29,7 @@ internal static RespMultiMessage Get(RespOperation[] oversized, int count) protected override int Parse(ref RespReader reader) => MultiMessageParser.Default.Parse(new ReadOnlySpan(_oversized, 0, _count), ref reader); - public override void Reset(bool recycle) + protected override void Reset(bool recycle) { _oversized = []; _count = 0; diff --git a/src/RESPite/Internal/RespStatefulMessage.cs b/src/RESPite/Internal/RespStatefulMessage.cs index 457b84e43..6d818b664 100644 --- a/src/RESPite/Internal/RespStatefulMessage.cs +++ b/src/RESPite/Internal/RespStatefulMessage.cs @@ -26,7 +26,7 @@ internal static RespStatefulMessage Get(in TState state, IRes protected override TResponse Parse(ref RespReader reader) => _parser!.Parse(in _state, ref reader); - public override void Reset(bool recycle) + protected override void Reset(bool recycle) { _state = default!; _parser = null!; diff --git a/src/RESPite/Internal/RespStatelessMessage.cs b/src/RESPite/Internal/RespStatelessMessage.cs index e46721df5..fd7e1768b 100644 --- a/src/RESPite/Internal/RespStatelessMessage.cs +++ b/src/RESPite/Internal/RespStatelessMessage.cs @@ -24,7 +24,7 @@ private RespStatelessMessage() { } protected override TResponse Parse(ref RespReader reader) => _parser!.Parse(ref reader); - public override void Reset(bool recycle) + protected override void Reset(bool recycle) { _parser = null!; base.Reset(recycle); From 60df5c7c66ef83c55b4ed3da84d566ca2bb3a8fa Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 3 Sep 2025 11:23:52 +0100 Subject: [PATCH 040/108] necessary to make spell-checker happy --- StackExchange.Redis.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/StackExchange.Redis.sln.DotSettings b/StackExchange.Redis.sln.DotSettings index 3f7abd6a9..5a7779446 100644 --- a/StackExchange.Redis.sln.DotSettings +++ b/StackExchange.Redis.sln.DotSettings @@ -1,5 +1,6 @@  OK PONG + True True True \ No newline at end of file From db81ca001adda5c11da5408be9221f9a3e576a03 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 3 Sep 2025 11:55:35 +0100 Subject: [PATCH 041/108] fix broken test re IVTS:Task cancellation --- tests/RESPite.Tests/OperationUnitTests.cs | 86 +++++++++++++++++++++-- 1 file changed, 79 insertions(+), 7 deletions(-) diff --git a/tests/RESPite.Tests/OperationUnitTests.cs b/tests/RESPite.Tests/OperationUnitTests.cs index 46310f86f..45068a8bb 100644 --- a/tests/RESPite.Tests/OperationUnitTests.cs +++ b/tests/RESPite.Tests/OperationUnitTests.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; +using System.Threading.Tasks.Sources; using RESPite; using Xunit; using Xunit.Internal; @@ -158,6 +159,57 @@ public async Task UnsentNotDetected_ValueTask_Async() AssertCT(ex.CancellationToken, cts.Token); } + [Fact] + public void CoreValueTaskToTaskSupportsCancellation() + { + // The purpose of this test is to show that there are some inherent limitations in netfx + // regarding IVTS:AsTask (compared with modern .NET), specifically: + // - it manifests as TaskCanceledException instead of OperationCanceledException + // - the token is not propagated correctly - it comes back as .None + var cts = new CancellationTokenSource(); + cts.Cancel(); + var ta = new TestAwaitable(); + var task = ta.AsValueTask().AsTask(); + Assert.Equal(TaskStatus.WaitingForActivation, task.Status); + ta.Cancel(cts.Token); + Assert.Equal(TaskStatus.Canceled, task.Status); + // ReSharper disable once MethodSupportsCancellation - this task is not incomplete +#pragma warning disable xUnit1051 + // use awaiter to unroll aggregate exception +#if NETFRAMEWORK + var ex = Assert.Throws(() => task.GetAwaiter().GetResult()); +#else + var ex = Assert.Throws(() => task.GetAwaiter().GetResult()); +#endif +#pragma warning restore xUnit1051 + var summary = SummarizeCT(ex.CancellationToken, cts.Token); + +#if NETFRAMEWORK // I *wish* this wasn't the case, but: wishes are free + Assert.Equal( + CancellationProblems.DefaultToken | CancellationProblems.NotCanceled + | CancellationProblems.CannotBeCanceled | CancellationProblems.NotExpectedToken, + summary); +#else + Assert.Equal(CancellationProblems.None, summary); +#endif + } + + private class TestAwaitable : IValueTaskSource + { + private ManualResetValueTaskSourceCore _core; + public ValueTask AsValueTask() => new(this, _core.Version); + public void GetResult(short token) => _core.GetResult(token); + public void Cancel(CancellationToken token) => _core.SetException(new OperationCanceledException(token)); + public ValueTaskSourceStatus GetStatus(short token) => _core.GetStatus(token); + + public void OnCompleted( + Action continuation, + object? state, + short token, + ValueTaskSourceOnCompletedFlags flags) + => _core.OnCompleted(continuation, state, token, flags); + } + [Fact(Timeout = 1000)] public async Task UnsentNotDetected_Task_Async() { @@ -165,19 +217,39 @@ public async Task UnsentNotDetected_Task_Async() cts.CancelAfter(100); var op = RespOperation.Create(out var remote, false, cts.Token); var ex = await Assert.ThrowsAnyAsync(async () => await op.AsTask()); + + #if NETFRAMEWORK // see CoreValueTaskToTaskSupportsCancellation for more context + Assert.Equal(CancellationToken.None, ex.CancellationToken); + #else AssertCT(ex.CancellationToken, cts.Token); + #endif } - private static void AssertCT(CancellationToken actual, CancellationToken expected) + [Flags] + private enum CancellationProblems + { + None = 0, + DefaultToken = 1 << 0, + NotCanceled = 1 << 1, + CannotBeCanceled = 1 << 2, + TestInfrastuctureToken = 1 << 3, + NotExpectedToken = 1 << 4, + } + + private static CancellationProblems SummarizeCT(CancellationToken actual, CancellationToken expected) { - string problems = ""; - if (actual == CancellationToken.None) problems += "default;"; - if (!actual.IsCancellationRequested) problems += "not cancelled;"; - if (actual == CancellationToken) problems += "test CT;"; - if (actual != expected) problems += "not local CT"; - Assert.Empty(problems.TrimEnd(';')); + CancellationProblems problems = 0; + if (actual == CancellationToken.None) problems |= CancellationProblems.DefaultToken; + if (!actual.IsCancellationRequested) problems |= CancellationProblems.NotCanceled; + if (!actual.CanBeCanceled) problems |= CancellationProblems.CannotBeCanceled; + if (actual == CancellationToken) problems |= CancellationProblems.TestInfrastuctureToken; + if (actual != expected) problems |= CancellationProblems.NotExpectedToken; + return problems; } + private static void AssertCT(CancellationToken actual, CancellationToken expected) + => Assert.Equal(CancellationProblems.None, SummarizeCT(actual, expected)); + [Fact(Timeout = 1000)] public void CanCreateAndCompleteOperation() { From 2ffdedba1e431a1f20154da43ab8c596e82c5db4 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 3 Sep 2025 12:34:26 +0100 Subject: [PATCH 042/108] improve token-error message --- src/RESPite/Internal/RespMessageBaseT.cs | 39 ++++++++++++++++++-- tests/RESPite.Tests/BasicIntegrationTests.cs | 2 + 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/RESPite/Internal/RespMessageBaseT.cs b/src/RESPite/Internal/RespMessageBaseT.cs index c26d2beb8..c9cf4b26a 100644 --- a/src/RESPite/Internal/RespMessageBaseT.cs +++ b/src/RESPite/Internal/RespMessageBaseT.cs @@ -22,8 +22,11 @@ private protected override void CheckToken(short token) { if (token != _asyncCore.Version) // use cheap test { - _ = _asyncCore.GetStatus(token); // get consistent exception message + // note that _asyncCore just gives a default InvalidOperationException message; let's see if we can do better + ThrowInvalidToken(); } + static void ThrowInvalidToken() => throw new InvalidOperationException( + $"The {nameof(RespOperation)} token is invalid; the most likely cause is awaiting an operation multiple times."); } // this is used from Task/ValueTask; we can't avoid that - in theory @@ -117,7 +120,7 @@ private bool TrySetResultPrecheckedToken(TResponse response) return true; } - private TResponse ThrowFailure(short token) + private TResponse ThrowFailureWithCleanup(short token) { try { @@ -130,10 +133,40 @@ private TResponse ThrowFailure(short token) } } + private static void ThrowSentNotComplete() => throw new InvalidOperationException( + "This operation has been sent but has not yet completed; the result is not available."); + public TResponse GetResult(short token) { // failure uses some try/catch logic, let's put that to one side - if (HasFlag(StateFlags.Doomed)) return ThrowFailure(token); + CheckToken(token); + if (HasFlag(StateFlags.Doomed)) return ThrowFailureWithCleanup(token); + +#if DEBUG // more detail + // Failure uses some try/catch logic, let's put that to one side, and concentrate on success. + // Also, note that we use OutcomeKnown, not Complete, because it might be an inline callback, + // in which case we need the caller to be able to get the result *right now*. + var flags = Flags & (StateFlags.OutcomeKnown | StateFlags.Doomed | StateFlags.IsSent); + switch (flags) + { + // anything doomed + case StateFlags.OutcomeKnown | StateFlags.Doomed | StateFlags.IsSent: + case StateFlags.Doomed | StateFlags.IsSent: + case StateFlags.OutcomeKnown | StateFlags.Doomed: + case StateFlags.Doomed: + return ThrowFailureWithCleanup(token); + // not complete, but sent + case StateFlags.IsSent when _asyncCore.GetStatus(token) == ValueTaskSourceStatus.Pending: + ThrowSentNotComplete(); + break; + // not sent + case 0: + ThrowNotSent(token); + break; + // everything else is success + } +#endif + var result = _asyncCore.GetResult(token); /* If we get here, we're successful; increment "version"/"token" *immediately*. Technically diff --git a/tests/RESPite.Tests/BasicIntegrationTests.cs b/tests/RESPite.Tests/BasicIntegrationTests.cs index af1fd318f..5b3db34e2 100644 --- a/tests/RESPite.Tests/BasicIntegrationTests.cs +++ b/tests/RESPite.Tests/BasicIntegrationTests.cs @@ -37,6 +37,7 @@ public void Parse() [InlineData(1)] [InlineData(5)] [InlineData(100)] + [InlineData(1000)] public void Ping(int count) { using var conn = GetConnection(); @@ -54,6 +55,7 @@ public void Ping(int count) [InlineData(1)] [InlineData(5)] [InlineData(100)] + [InlineData(1000)] public async Task PingAsync(int count) { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); From e606bb797b667018a0baadd522531528cc2df582 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 3 Sep 2025 13:26:07 +0100 Subject: [PATCH 043/108] improve error message when pending --- src/RESPite/Internal/RespMessageBase.cs | 7 +++ src/RESPite/Internal/RespMessageBaseT.cs | 59 ++++++++++-------------- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs index b34c70f93..cb15bc5b0 100644 --- a/src/RESPite/Internal/RespMessageBase.cs +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -95,6 +95,7 @@ public bool TrySetResult(short token, in ReadOnlySequence response) public abstract short Token { get; } + [Obsolete("Prefer de-virtualized version via CheckTokenCore")] private protected abstract void CheckToken(short token); private protected abstract ValueTaskSourceStatus OwnStatus { get; } @@ -103,7 +104,9 @@ public bool TrySetResult(short token, in ReadOnlySequence response) public bool IsSent(short token) { +#pragma warning disable CS0618 // can't access CheckTokenCore in base-class CheckToken(token); +#pragma warning restore CS0618 return HasFlag(StateFlags.IsSent); } @@ -236,7 +239,9 @@ static void ThrowReleased() => [MethodImpl(MethodImplOptions.NoInlining)] protected void ThrowNotSent(short token) { +#pragma warning disable CS0618 // can't access CheckTokenCore in base-class CheckToken(token); // prefer a token explanation +#pragma warning restore CS0618 throw new InvalidOperationException( "This command has not yet been sent; waiting is not possible. If this is a transaction or batch, you must execute that first."); } @@ -244,7 +249,9 @@ protected void ThrowNotSent(short token) [MethodImpl(MethodImplOptions.NoInlining)] protected void SetNotSentAsync(short token) { +#pragma warning disable CS0618 // can't access CheckTokenCore in base-class CheckToken(token); +#pragma warning restore CS0618 TrySetExceptionPrecheckedToken(new InvalidOperationException( "This command has not yet been sent; awaiting is not possible. If this is a transaction or batch, you must execute that first.")); } diff --git a/src/RESPite/Internal/RespMessageBaseT.cs b/src/RESPite/Internal/RespMessageBaseT.cs index c9cf4b26a..648535d38 100644 --- a/src/RESPite/Internal/RespMessageBaseT.cs +++ b/src/RESPite/Internal/RespMessageBaseT.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks.Sources; +using System.Runtime.CompilerServices; +using System.Threading.Tasks.Sources; using RESPite.Messages; namespace RESPite.Internal; @@ -16,9 +17,13 @@ internal abstract class RespMessageBase : RespMessageBase, IValueTask /* asking about the status too early is usually a very bad sign that they're doing something like awaiting a message in a transaction that hasn't been sent */ public override ValueTaskSourceStatus GetStatus(short token) - => _asyncCore.GetStatus(token); + { + CheckTokenCore(token); + return _asyncCore.GetStatus(token); + } - private protected override void CheckToken(short token) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CheckTokenCore(short token) { if (token != _asyncCore.Version) // use cheap test { @@ -29,6 +34,9 @@ private protected override void CheckToken(short token) $"The {nameof(RespOperation)} token is invalid; the most likely cause is awaiting an operation multiple times."); } + [Obsolete("Prefer de-virtualized version via " + nameof(CheckTokenCore))] + private protected override void CheckToken(short token) => CheckTokenCore(token); + // this is used from Task/ValueTask; we can't avoid that - in theory // we *coiuld* sort of make it work for ValueTask, but if anyone // calls .AsTask() on it, it would fail @@ -38,7 +46,7 @@ public override void OnCompleted( short token, ValueTaskSourceOnCompletedFlags flags) { - CheckToken(token); + CheckTokenCore(token); SetFlag(StateFlags.NoPulse); // async doesn't need to be pulsed _asyncCore.OnCompleted(continuation, state, token, flags); } @@ -49,7 +57,7 @@ public override void OnCompletedWithNotSentDetection( short token, ValueTaskSourceOnCompletedFlags flags) { - CheckToken(token); + CheckTokenCore(token); if (!HasFlag(StateFlags.IsSent)) SetNotSentAsync(token); SetFlag(StateFlags.NoPulse); // async doesn't need to be pulsed _asyncCore.OnCompleted(continuation, state, token, flags); @@ -75,7 +83,7 @@ public TResponse Wait(short token, TimeSpan timeout) } bool isTimeout = false; - CheckToken(token); + CheckTokenCore(token); lock (this) { switch (Flags & (StateFlags.Complete | StateFlags.NoPulse)) @@ -122,8 +130,15 @@ private bool TrySetResultPrecheckedToken(TResponse response) private TResponse ThrowFailureWithCleanup(short token) { + var status = GetStatus(token); try { + if (status == ValueTaskSourceStatus.Pending) + { + if (!HasFlag(StateFlags.IsSent)) ThrowNotSent(_asyncCore.Version); + throw new InvalidOperationException( + "This operation has been sent but has not yet completed; the result is not available."); + } return _asyncCore.GetResult(token); } finally @@ -133,40 +148,14 @@ private TResponse ThrowFailureWithCleanup(short token) } } - private static void ThrowSentNotComplete() => throw new InvalidOperationException( - "This operation has been sent but has not yet completed; the result is not available."); - public TResponse GetResult(short token) { // failure uses some try/catch logic, let's put that to one side - CheckToken(token); - if (HasFlag(StateFlags.Doomed)) return ThrowFailureWithCleanup(token); - -#if DEBUG // more detail - // Failure uses some try/catch logic, let's put that to one side, and concentrate on success. - // Also, note that we use OutcomeKnown, not Complete, because it might be an inline callback, - // in which case we need the caller to be able to get the result *right now*. - var flags = Flags & (StateFlags.OutcomeKnown | StateFlags.Doomed | StateFlags.IsSent); - switch (flags) + // (it is very tempting to peek inside GetStatus with UnsafeAccessor...) + if (_asyncCore.Version != token || _asyncCore.GetStatus(token) != ValueTaskSourceStatus.Succeeded) { - // anything doomed - case StateFlags.OutcomeKnown | StateFlags.Doomed | StateFlags.IsSent: - case StateFlags.Doomed | StateFlags.IsSent: - case StateFlags.OutcomeKnown | StateFlags.Doomed: - case StateFlags.Doomed: - return ThrowFailureWithCleanup(token); - // not complete, but sent - case StateFlags.IsSent when _asyncCore.GetStatus(token) == ValueTaskSourceStatus.Pending: - ThrowSentNotComplete(); - break; - // not sent - case 0: - ThrowNotSent(token); - break; - // everything else is success + return ThrowFailureWithCleanup(token); } -#endif - var result = _asyncCore.GetResult(token); /* If we get here, we're successful; increment "version"/"token" *immediately*. Technically From cf3f83b3cba9b57c329b9a2dab7203f380ef53b1 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 3 Sep 2025 16:34:05 +0100 Subject: [PATCH 044/108] deploy: multi-message batching --- src/RESPite.Benchmark/BenchmarkBase.cs | 10 ++- .../Internal/BufferingBatchConnection.cs | 27 ++++++- src/RESPite/Internal/DebugCounters.cs | 38 ++++++++- src/RESPite/Internal/RespMessageBase.cs | 21 ++++- src/RESPite/Internal/RespMessageBaseT.cs | 2 +- src/RESPite/Internal/RespMultiMessage.cs | 80 +++++++++---------- src/RESPite/Internal/StreamConnection.cs | 39 ++++++++- src/RESPite/RespContext.cs | 13 ++- src/RESPite/RespOperation.cs | 6 ++ tests/RESPite.Tests/BatchTests.cs | 8 +- 10 files changed, 183 insertions(+), 61 deletions(-) diff --git a/src/RESPite.Benchmark/BenchmarkBase.cs b/src/RESPite.Benchmark/BenchmarkBase.cs index 0eecdc816..8c2fda320 100644 --- a/src/RESPite.Benchmark/BenchmarkBase.cs +++ b/src/RESPite.Benchmark/BenchmarkBase.cs @@ -269,7 +269,7 @@ private async Task PipelineTyped(TClient client, Func( $"Batch growth; {counters.BatchGrowCount:#,##0} events, {counters.BatchGrowCopyCount:#,###,##0} elements copied"); } + if (counters.BatchBufferLeaseCount != 0 | counters.BatchMultiRootMessageCount != 0) + { + Console.Write($"Multi-message batching: {counters.BatchMultiRootMessageCount:#,###,##0} batches, {counters.BatchMultiChildMessageCount:#,###,##0} sub-messages"); + if (counters.BatchBufferLeaseCount != 0) + Console.Write($"; {counters.BatchBufferLeaseCount:#,###,##0} blocks leased, {counters.BatchBufferReturnCount:#,###,##0} blocks returned, {counters.BatchBufferElementsOutstanding:#,###,##0} elements outstanding"); + Console.WriteLine(); + } + if (counters.BufferCreatedCount != 0 || counters.BufferRecycledCount != 0 | counters.BufferMessageCount != 0) { diff --git a/src/RESPite/Connections/Internal/BufferingBatchConnection.cs b/src/RESPite/Connections/Internal/BufferingBatchConnection.cs index a176da2c1..1cf0264ff 100644 --- a/src/RESPite/Connections/Internal/BufferingBatchConnection.cs +++ b/src/RESPite/Connections/Internal/BufferingBatchConnection.cs @@ -10,7 +10,26 @@ namespace RESPite.Connections.Internal; /// internal abstract class BufferingBatchConnection(in RespContext context, int sizeHint) : RespBatch(context) { - private RespOperation[] _buffer = sizeHint <= 0 ? [] : ArrayPool.Shared.Rent(sizeHint); + internal static void Return(ref RespOperation[] buffer) + { + if (buffer.Length != 0) + { + DebugCounters.OnBatchBufferReturn(buffer.Length); + ArrayPool.Shared.Return(buffer); + buffer = []; + } + } + + private static RespOperation[] Rent(int sizeHint) + { + if (sizeHint <= 0) return []; + var arr = ArrayPool.Shared.Rent(sizeHint); + DebugCounters.OnBatchBufferLease(arr.Length); + return arr; + } + + private RespOperation[] _buffer = Rent(sizeHint); + private int _count = 0; protected object SyncLock => this; @@ -36,7 +55,7 @@ items will be added */ message.Message.TrySetException(message.Token, CreateObjectDisposedException()); } - ArrayPool.Shared.Return(buffer); + Return(ref buffer); ConnectionError = null; } @@ -81,10 +100,10 @@ private void GrowLocked(int required) if ((uint)newCapacity > maxLength) newCapacity = maxLength; // account for max if (newCapacity < required) newCapacity = required; // in case doubling wasn't enough - var newBuffer = ArrayPool.Shared.Rent(newCapacity); + var newBuffer = Rent(newCapacity); DebugCounters.OnBatchGrow(_count); _buffer.AsSpan(0, _count).CopyTo(newBuffer); - ArrayPool.Shared.Return(_buffer); + Return(ref _buffer); _buffer = newBuffer; } diff --git a/src/RESPite/Internal/DebugCounters.cs b/src/RESPite/Internal/DebugCounters.cs index ddb011945..dd3d9f6d4 100644 --- a/src/RESPite/Internal/DebugCounters.cs +++ b/src/RESPite/Internal/DebugCounters.cs @@ -29,7 +29,10 @@ internal partial class DebugCounters _tallyBufferMessageCount, _tallyBufferPinCount, _tallyBufferLeakCount, - _tallyBatchGrowCount; + _tallyBatchGrowCount, + _tallyBatchBufferLeaseCount, + _tallyBatchBufferReturnCount, + _tallyBatchMultiRootMessageCount; private static long _tallyWriteBytes, _tallyReadBytes, @@ -39,7 +42,9 @@ internal partial class DebugCounters _tallyBufferRecycledBytes, _tallyBufferMaxOutstandingBytes, _tallyBufferTotalBytes, - _tallyBatchGrowCopyCount; + _tallyBatchGrowCopyCount, + _tallyBatchBufferElementsOutstanding, + _tallyBatchMultiChildMessageCount; #endif [Conditional("DEBUG")] @@ -81,6 +86,30 @@ public static void OnBatchWritePartialPage() #endif } + public static void OnBatchBufferLease(int length) + { +#if DEBUG + Interlocked.Increment(ref _tallyBatchBufferLeaseCount); + Interlocked.Add(ref _tallyBatchBufferElementsOutstanding, length); +#endif + } + + public static void OnBatchBufferReturn(int length) + { +#if DEBUG + Interlocked.Increment(ref _tallyBatchBufferReturnCount); + Interlocked.Add(ref _tallyBatchBufferElementsOutstanding, -length); +#endif + } + + public static void OnMultiMessageWrite(int length) + { +#if DEBUG + Interlocked.Increment(ref _tallyBatchMultiRootMessageCount); + Interlocked.Add(ref _tallyBatchMultiChildMessageCount, length); +#endif + } + [Conditional("DEBUG")] internal static void OnAsyncRead(int bytes, bool inline) { @@ -270,6 +299,11 @@ private static void EstimatedMovingRangeAverage(ref long field, long value) public int BatchWriteMessageCount { get; } = Interlocked.Exchange(ref _tallyBatchWriteMessageCount, 0); public int BatchGrowCount { get; } = Interlocked.Exchange(ref _tallyBatchGrowCount, 0); public long BatchGrowCopyCount { get; } = Interlocked.Exchange(ref _tallyBatchGrowCopyCount, 0); + public int BatchBufferLeaseCount { get; } = Interlocked.Exchange(ref _tallyBatchBufferLeaseCount, 0); + public int BatchBufferReturnCount { get; } = Interlocked.Exchange(ref _tallyBatchBufferReturnCount, 0); + public long BatchBufferElementsOutstanding { get; } = Interlocked.Exchange(ref _tallyBatchBufferElementsOutstanding, 0); + public int BatchMultiRootMessageCount { get; } = Interlocked.Exchange(ref _tallyBatchMultiRootMessageCount, 0); + public long BatchMultiChildMessageCount { get; } = Interlocked.Exchange(ref _tallyBatchMultiChildMessageCount, 0); public int BufferCreatedCount { get; } = Interlocked.Exchange(ref _tallyBufferCreatedCount, 0); public int BufferRecycledCount { get; } = Interlocked.Exchange(ref _tallyBufferRecycledCount, 0); diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs index cb15bc5b0..b114e17fb 100644 --- a/src/RESPite/Internal/RespMessageBase.cs +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -78,6 +78,17 @@ public bool TrySetResult(short token, ref RespReader reader) } } + // if this is a multi-message type, then when adding to the "sent awaiting resport" queue, + // instead of adding the message, we add the sub-messages **instead** (and not the root message) + public virtual bool TryGetSubMessages(short token, out ReadOnlySpan operations) + { + operations = default; + return false; + } + + // if this is a multi-message type, this does cleanup after TryGetSubMessages has been consumed + public virtual bool TrySetResultAfterUnloadingSubMessages(short token) => false; + public bool TrySetResult(short token, scoped ReadOnlySpan response) { RespReader reader = new(response); @@ -186,6 +197,14 @@ protected virtual void Reset(bool recycle) protected abstract void Recycle(); protected abstract void NextToken(); + internal void OnSent(short token) + { + // only if our token matches, but: don't throw + if (token == Token) OnSent(); + } + + protected virtual void OnSent() => SetFlag(StateFlags.IsSent); + public bool TryReserveRequest(short token, out ReadOnlySequence payload, bool recordSent = true) { while (true) // redo in case of CEX failure @@ -201,7 +220,7 @@ public bool TryReserveRequest(short token, out ReadOnlySequence payload, b if (Interlocked.CompareExchange(ref _requestRefCount, checked(oldCount + 1), oldCount) == oldCount) { - if (recordSent) SetFlag(StateFlags.IsSent); + if (recordSent) OnSent(); payload = _request; return true; diff --git a/src/RESPite/Internal/RespMessageBaseT.cs b/src/RESPite/Internal/RespMessageBaseT.cs index 648535d38..498440ca6 100644 --- a/src/RESPite/Internal/RespMessageBaseT.cs +++ b/src/RESPite/Internal/RespMessageBaseT.cs @@ -119,7 +119,7 @@ public TResponse Wait(short token, TimeSpan timeout) "This operation cannot be waited because it entered async/await mode - most likely by calling AsTask()"); } - private bool TrySetResultPrecheckedToken(TResponse response) + protected bool TrySetResultPrecheckedToken(TResponse response) { if (!TrySetOutcomeKnownPrecheckedToken(true)) return false; diff --git a/src/RESPite/Internal/RespMultiMessage.cs b/src/RESPite/Internal/RespMultiMessage.cs index 451d936cf..44d13da28 100644 --- a/src/RESPite/Internal/RespMultiMessage.cs +++ b/src/RESPite/Internal/RespMultiMessage.cs @@ -1,4 +1,6 @@ -using System.Runtime.CompilerServices; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using RESPite.Connections.Internal; using RESPite.Messages; namespace RESPite.Internal; @@ -6,71 +8,67 @@ namespace RESPite.Internal; internal sealed class RespMultiMessage : RespMessageBase { private RespOperation[] _oversized; - private int _count = 0; + private int _count; [ThreadStatic] // used for object recycling of the async machinery private static RespMultiMessage? _threadStaticSpare; + private ReadOnlySpan Operations => new(_oversized, 0, _count); + internal static RespMultiMessage Get(RespOperation[] oversized, int count) { RespMultiMessage obj = _threadStaticSpare ?? new(); _threadStaticSpare = null; obj._oversized = oversized; obj._count = count; - obj.SetFlag(StateFlags.HasParser | StateFlags.MetadataParser); return obj; } + public override bool TryGetSubMessages(short token, out ReadOnlySpan operations) + { + operations = token == Token ? Operations : default; + return true; // always return true; this means that flush gets called + } + + public override bool TrySetResultAfterUnloadingSubMessages(short token) + { + if (token == Token && TrySetResultPrecheckedToken(_count)) + { + // release the buffer immediately - it isn't needed any more + _count = 0; + BufferingBatchConnection.Return(ref _oversized); + return true; + } + + return false; + } + protected override void Recycle() => _threadStaticSpare = this; private RespMultiMessage() => Unsafe.SkipInit(out _oversized); protected override int Parse(ref RespReader reader) - => MultiMessageParser.Default.Parse(new ReadOnlySpan(_oversized, 0, _count), ref reader); + { + Debug.Fail("Not expecting to see results, since unrolled during write"); + return _count; + } + + protected override void OnSent() + { + base.OnSent(); + foreach (var op in Operations) + { + op.OnSent(); + } + } protected override void Reset(bool recycle) { - _oversized = []; _count = 0; + BufferingBatchConnection.Return(ref _oversized); base.Reset(recycle); } public override int MessageCount => _count; - - private sealed class MultiMessageParser - { - private MultiMessageParser() { } - public static readonly MultiMessageParser Default = new(); - - public int Parse(ReadOnlySpan operations, ref RespReader reader) - { - int count = 0; - foreach (var op in operations) - { - // we need to give each sub-operation an isolated reader - no bleeding - // data between misbehaving readers (for example, that don't consume - // all of their data) - var clone = reader; // track the start position - if (!reader.TryMoveNext(checkError: false)) ThrowEOF(); // we definitely expected enough for all - - reader.SkipChildren(); // track the end position (for scalar, this is "move past current") - - // now clamp this sub-reader, passing *that* to the operation - clone.TrimToTotal(reader.BytesConsumed); - if (op.Message.TrySetResult(op.Token, ref clone)) - { - // track how many we successfully processed, ignoring things - // that, for example, failed due to cancellation before we got here - count++; - } - } - - if (reader.TryMoveNext()) ThrowTrailing(); - return count; - - static void ThrowTrailing() => throw new FormatException("Unexpected trailing data"); - static void ThrowEOF() => throw new EndOfStreamException(); - } - } } diff --git a/src/RESPite/Internal/StreamConnection.cs b/src/RESPite/Internal/StreamConnection.cs index c176929f6..4b2cef488 100644 --- a/src/RESPite/Internal/StreamConnection.cs +++ b/src/RESPite/Internal/StreamConnection.cs @@ -482,6 +482,37 @@ private void OnRequestUnavailable(in RespOperation message) } } + [MethodImpl(MethodImplOptions.NoInlining)] + private void EnqueueMultiMessage(in RespOperation operation, ReadOnlySpan operations) + { + // This typically *does not* include the batch message itself. + DebugCounters.OnMultiMessageWrite(operations.Length); + foreach (var message in operations) + { + _outstanding.Enqueue(message); + } + // The root message typically gets completed here - on the receiving side, all + // we see is N unrelated inbound messages; the batch terminates at write. + if (!operation.TrySetResultAfterUnloadingSubMessages()) + { + _outstanding.Enqueue(operation); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Enqueue(in RespOperation operation) + { + if (operation.TryGetSubMessages(out var operations)) + { + // rare path - multi-message batch + EnqueueMultiMessage(in operation, operations); + } + else + { + _outstanding.Enqueue(operation); + } + } + public override void Write(in RespOperation message) { bool releaseRequest = message.Message.TryReserveRequest(message.Token, out var bytes); @@ -495,7 +526,7 @@ public override void Write(in RespOperation message) TakeWriter(); try { - _outstanding.Enqueue(message); + Enqueue(in message); releaseRequest = false; // once we write, only release on success if (bytes.IsSingleSegment) { @@ -576,7 +607,7 @@ internal override void Write(ReadOnlySpan messages) } DebugValidateFrameCount(bytes, message.MessageCount); - _outstanding.Enqueue(message); + Enqueue(in message); toRelease = null; // once we write, only release on success if (bytes.IsSingleSegment) { @@ -625,7 +656,7 @@ public override Task WriteAsync(in RespOperation message) DebugValidateFrameCount(bytes, message.MessageCount); try { - _outstanding.Enqueue(message); + Enqueue(in message); releaseRequest = false; // once we write, only release on success ValueTask pendingWrite; if (bytes.IsSingleSegment) @@ -715,7 +746,7 @@ private async Task CombineAndSendMultipleAsync(StreamConnection @this, ReadOnlyM _writeBuffer.Write(bytes); toRelease = null; message.Message.ReleaseRequest(); - @this._outstanding.Enqueue(message); + @this.Enqueue(in message); // do we have any full segments? if so, write them and narrow "messages" if (_writeBuffer.TryGetFirstCommittedMemory(CycleBuffer.GetFullPagesOnly, out var memory)) diff --git a/src/RESPite/RespContext.cs b/src/RESPite/RespContext.cs index eca30ecce..92bd70a40 100644 --- a/src/RESPite/RespContext.cs +++ b/src/RESPite/RespContext.cs @@ -1,7 +1,7 @@ -using System.Runtime.CompilerServices; +#define MULTI_BATCH // use combining batches, rather than simple batches + +using System.Runtime.CompilerServices; using RESPite.Connections.Internal; -using RESPite.Internal; -using RESPite.Messages; namespace RESPite; @@ -154,5 +154,10 @@ public RespContext ConfigureAwait(bool continueOnCapturedContext) return clone; } - public RespBatch CreateBatch(int sizeHint = 0) => new BasicBatchConnection(in this, sizeHint); + public RespBatch CreateBatch(int sizeHint = 0) + #if MULTI_BATCH + => new MergingBatchConnection(in this, sizeHint); + #else + => new BasicBatchConnection(in this, sizeHint); + #endif } diff --git a/src/RESPite/RespOperation.cs b/src/RESPite/RespOperation.cs index 49b81fa83..9154396e9 100644 --- a/src/RESPite/RespOperation.cs +++ b/src/RESPite/RespOperation.cs @@ -220,4 +220,10 @@ public static RespOperation Create( remote = new(msg); return new RespOperation(msg); } + + internal void OnSent() => Message.OnSent(Token); + + internal bool TryGetSubMessages(out ReadOnlySpan operations) + => Message.TryGetSubMessages(Token, out operations); + internal bool TrySetResultAfterUnloadingSubMessages() => Message.TrySetResultAfterUnloadingSubMessages(Token); } diff --git a/tests/RESPite.Tests/BatchTests.cs b/tests/RESPite.Tests/BatchTests.cs index 61d35a97d..0f7d9ec3d 100644 --- a/tests/RESPite.Tests/BatchTests.cs +++ b/tests/RESPite.Tests/BatchTests.cs @@ -58,14 +58,16 @@ public async Task SimpleBatching() // we *can* safely await if the batch is disposed await Assert.ThrowsAsync(async () => await f); - Assert.True(d.AsRespOperation().IsSent); - e = TestAsync(server.Context, 4); // uses SERVER again - // check what was sent server.AssertSent("*2\r\n$4\r\ntest\r\n$1\r\n0\r\n"u8); server.AssertSent("*2\r\n$4\r\ntest\r\n$1\r\n1\r\n"u8); server.AssertSent("*2\r\n$4\r\ntest\r\n$1\r\n2\r\n"u8); server.AssertSent("*2\r\n$4\r\ntest\r\n$1\r\n3\r\n"u8); + + server.AssertAllSent(); // that's everything + + Assert.True(d.AsRespOperation().IsSent, "batch ops should report as sent"); + e = TestAsync(server.Context, 4); // uses SERVER again server.AssertSent("*2\r\n$4\r\ntest\r\n$1\r\n4\r\n"u8); server.AssertAllSent(); // that's everything From 7c01375250f65f0d8327a535d7ac08c801fc4410 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 3 Sep 2025 17:13:39 +0100 Subject: [PATCH 045/108] add --basic, not functional yet --- src/RESPite.Benchmark/NewCoreBenchmark.cs | 95 ++++++++++++++++++++++- src/RESPite.Benchmark/Program.cs | 14 +++- 2 files changed, 104 insertions(+), 5 deletions(-) diff --git a/src/RESPite.Benchmark/NewCoreBenchmark.cs b/src/RESPite.Benchmark/NewCoreBenchmark.cs index 5d415d8d6..d799dd371 100644 --- a/src/RESPite.Benchmark/NewCoreBenchmark.cs +++ b/src/RESPite.Benchmark/NewCoreBenchmark.cs @@ -170,7 +170,8 @@ protected override void PrepareBatch(RespContext client, int count) [DisplayName("RPOP")] private ValueTask RPop(RespContext ctx) => ctx.RPopAsync(_listKey); - private ValueTask LPopInit(RespContext ctx) => ctx.LPushAsync(_listKey, _payload, TotalOperations).AsUntypedValueTask(); + private ValueTask LPopInit(RespContext ctx) => + ctx.LPushAsync(_listKey, _payload, TotalOperations).AsUntypedValueTask(); [DisplayName("SADD")] private ValueTask SAdd(RespContext ctx) => ctx.SAddAsync(_setKey, "element:__rand_int__"); @@ -210,10 +211,91 @@ private async ValueTask SPopInit(RespContext ctx) [DisplayName("MSET"), Description("10 keys")] private ValueTask MSet(RespContext ctx) => ctx.MSetAsync(_pairs); - private ValueTask LRangeInit(RespContext ctx) => ctx.LPushAsync(_listKey, _payload, TotalOperations).AsUntypedValueTask(); + private ValueTask LRangeInit(RespContext ctx) => + ctx.LPushAsync(_listKey, _payload, TotalOperations).AsUntypedValueTask(); [DisplayName("XADD")] - private ValueTask XAdd(RespContext ctx) => ctx.XAddAsync(_streamKey, "*", "myfield", _payload); + private ValueTask XAdd(RespContext ctx) => + ctx.XAddAsync(_streamKey, "*", "myfield", _payload); + + public async Task RunBasicLoopAsync() + { + var client = GetClient(0); + _ = await client.DelAsync(_counterKey).ConfigureAwait(false); + + if (ClientCount <= 1) + { + await RunBasicLoopAsync(0); + } + else + { + Task[] tasks = new Task[ClientCount]; + for (int i = 0; i < ClientCount; i++) + { + var loopSnapshot = i; + tasks[i] = Task.Run(() => RunBasicLoopAsync(loopSnapshot)); + } + + await Task.WhenAll(tasks); + } + + return 0; + } + + public async Task RunBasicLoopAsync(int clientId) + { + var client = GetClient(clientId); + var depth = PipelineDepth; + int localCount = 0; + long lastValue = client.GetInt32(_counterKey); + var previous = DateTime.UtcNow; + + void Tick() + { + DateTime now; + if (clientId == 0 && ((now = DateTime.Now) - previous).TotalSeconds >= 1) + { + var newValue = client.GetInt32(_counterKey); + Console.WriteLine($"{newValue - lastValue} ops in {now - previous}"); + previous = now; + lastValue = newValue; + localCount = 0; + } + } + if (depth <= 1) + { + while (true) + { + await client.IncrAsync(_counterKey).ConfigureAwait(false); + + if (++localCount >= 1000) Tick(); + } + } + else + { + ValueTask[] pending = new ValueTask[depth]; + using (var batch = client.CreateBatch(depth)) + { + var ctx = batch.Context; + while (true) + { + for (int i = 0; i < depth; i++) + { + pending[i] = ctx.IncrAsync(_counterKey); + } + + await batch.FlushAsync().ConfigureAwait(false); + for (int i = 0; i < depth; i++) + { + await pending[i].ConfigureAwait(false); + } + + localCount += depth; + if (localCount >= 1000) Tick(); + } + } + } + } } internal static partial class RedisCommands @@ -240,6 +322,7 @@ private sealed class LPushFormatter : IRespFormatter<(string Key, byte[] Payload { private LPushFormatter() { } public static readonly LPushFormatter Instance = new(); + public void Format( scoped ReadOnlySpan command, ref RespWriter writer, @@ -265,7 +348,8 @@ public void Format( internal static partial RespParsers.ResponseSummary RPop(this in RespContext ctx, string key); [RespCommand] - internal static partial RespParsers.ResponseSummary LRange(this in RespContext ctx, string key, int start, int stop); + internal static partial RespParsers.ResponseSummary + LRange(this in RespContext ctx, string key, int start, int stop); [RespCommand] internal static partial int HSet(this in RespContext ctx, string key, string field, byte[] payload); @@ -285,6 +369,9 @@ public void Format( [RespCommand] internal static partial int ZAdd(this in RespContext ctx, string key, double score, string payload); + [RespCommand("get")] + internal static partial int GetInt32(this in RespContext ctx, string key); + [RespCommand] internal static partial RespParsers.ResponseSummary XAdd( this in RespContext ctx, diff --git a/src/RESPite.Benchmark/Program.cs b/src/RESPite.Benchmark/Program.cs index c7af0e861..cd25f5a67 100644 --- a/src/RESPite.Benchmark/Program.cs +++ b/src/RESPite.Benchmark/Program.cs @@ -19,10 +19,12 @@ private static async Task Main(string[] args) case "--old": benchmarks.Add(new OldCoreBenchmark(args)); break; - case "--new": benchmarks.Add(new NewCoreBenchmark(args)); break; + case "--basic": + await BasicLoopAsync(args); + return 0; } } @@ -46,6 +48,10 @@ private static async Task Main(string[] args) // ReSharper disable once LoopVariableIsNeverChangedInsideLoop while (benchmarks[0].Loop); + foreach (var bench in benchmarks) + { + bench.Dispose(); + } return 0; } catch (Exception ex) @@ -55,6 +61,12 @@ private static async Task Main(string[] args) } } + private static Task BasicLoopAsync(string[] args) + { + using var bench = new NewCoreBenchmark(args); + return bench.RunBasicLoopAsync(); + } + internal static void WriteException(Exception? ex, [CallerMemberName] string operation = "") { Console.Error.WriteLine(); From 1271e1b884a0b75436e09afe6c4486fd6cef8563 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 3 Sep 2025 18:16:32 +0100 Subject: [PATCH 046/108] nits --- src/RESPite.Benchmark/NewCoreBenchmark.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/RESPite.Benchmark/NewCoreBenchmark.cs b/src/RESPite.Benchmark/NewCoreBenchmark.cs index d799dd371..2e4c10f0e 100644 --- a/src/RESPite.Benchmark/NewCoreBenchmark.cs +++ b/src/RESPite.Benchmark/NewCoreBenchmark.cs @@ -247,7 +247,8 @@ public async Task RunBasicLoopAsync(int clientId) var client = GetClient(clientId); var depth = PipelineDepth; int localCount = 0; - long lastValue = client.GetInt32(_counterKey); + long lastValue = await client.GetInt32Async(_counterKey).ConfigureAwait(false), + currentValue = lastValue; var previous = DateTime.UtcNow; void Tick() @@ -255,10 +256,9 @@ void Tick() DateTime now; if (clientId == 0 && ((now = DateTime.Now) - previous).TotalSeconds >= 1) { - var newValue = client.GetInt32(_counterKey); - Console.WriteLine($"{newValue - lastValue} ops in {now - previous}"); + Console.WriteLine($"{currentValue - lastValue} ops in {now - previous}"); previous = now; - lastValue = newValue; + lastValue = currentValue; localCount = 0; } } @@ -285,9 +285,10 @@ void Tick() } await batch.FlushAsync().ConfigureAwait(false); + batch.EnsureCapacity(depth); // batches don't assume re-use for (int i = 0; i < depth; i++) { - await pending[i].ConfigureAwait(false); + currentValue = await pending[i].ConfigureAwait(false); } localCount += depth; From 95319b3e1a9a19c474c0f514272ab27408ec4ecd Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 4 Sep 2025 08:11:32 +0100 Subject: [PATCH 047/108] fix the basic loop, and implement for down-level --- src/RESPite.Benchmark/BenchmarkBase.cs | 44 +++++++-- src/RESPite.Benchmark/NewCoreBenchmark.cs | 89 ++++++++++--------- src/RESPite.Benchmark/OldCoreBenchmark.cs | 83 ++++++++++++++++- src/RESPite.Benchmark/Program.cs | 20 +++-- .../Internal/StreamConnection.cs | 4 +- 5 files changed, 177 insertions(+), 63 deletions(-) rename src/RESPite/{ => Connections}/Internal/StreamConnection.cs (99%) diff --git a/src/RESPite.Benchmark/BenchmarkBase.cs b/src/RESPite.Benchmark/BenchmarkBase.cs index 8c2fda320..025fb696f 100644 --- a/src/RESPite.Benchmark/BenchmarkBase.cs +++ b/src/RESPite.Benchmark/BenchmarkBase.cs @@ -119,10 +119,36 @@ public BenchmarkBase(string[] args) } public abstract Task RunAll(); + + public async Task RunBasicLoopAsync() + { + await DeleteAsync(_counterKey).ConfigureAwait(false); + + if (ClientCount <= 1) + { + await RunBasicLoopAsync(0); + } + else + { + Task[] tasks = new Task[ClientCount]; + for (int i = 0; i < ClientCount; i++) + { + var loopSnapshot = i; + tasks[i] = Task.Run(() => RunBasicLoopAsync(loopSnapshot)); + } + + await Task.WhenAll(tasks); + } + } + + protected abstract Task RunBasicLoopAsync(int clientId); + protected abstract Task DeleteAsync(string key); } public abstract class BenchmarkBase(string[] args) : BenchmarkBase(args) { + protected override Task DeleteAsync(string key) => DeleteAsync(GetClient(0), key); + protected virtual Task OnCleanupAsync(TClient client) => Task.CompletedTask; protected virtual Task InitAsync(TClient client) => Task.CompletedTask; @@ -132,13 +158,13 @@ public async Task CleanupAsync() try { var client = GetClient(0); - await Delete(client, _getSetKey).ConfigureAwait(false); - await Delete(client, _counterKey).ConfigureAwait(false); - await Delete(client, _listKey).ConfigureAwait(false); - await Delete(client, _setKey).ConfigureAwait(false); - await Delete(client, _hashKey).ConfigureAwait(false); - await Delete(client, _sortedSetKey).ConfigureAwait(false); - await Delete(client, _streamKey).ConfigureAwait(false); + await DeleteAsync(client, _getSetKey).ConfigureAwait(false); + await DeleteAsync(client, _counterKey).ConfigureAwait(false); + await DeleteAsync(client, _listKey).ConfigureAwait(false); + await DeleteAsync(client, _setKey).ConfigureAwait(false); + await DeleteAsync(client, _hashKey).ConfigureAwait(false); + await DeleteAsync(client, _sortedSetKey).ConfigureAwait(false); + await DeleteAsync(client, _streamKey).ConfigureAwait(false); await OnCleanupAsync(client).ConfigureAwait(false); } catch (Exception ex) @@ -311,7 +337,7 @@ public async Task InitAsync() protected abstract TClient GetClient(int index); protected virtual TClient WithCancellation(TClient client, CancellationToken cancellationToken) => client; - protected abstract Task Delete(TClient client, string key); + protected abstract Task DeleteAsync(TClient client, string key); protected abstract TClient CreateBatch(TClient client); @@ -401,7 +427,7 @@ protected async Task RunAsyncCore( { if (key is not null) { - await Delete(GetClient(0), key).ConfigureAwait(false); + await DeleteAsync(GetClient(0), key).ConfigureAwait(false); } try diff --git a/src/RESPite.Benchmark/NewCoreBenchmark.cs b/src/RESPite.Benchmark/NewCoreBenchmark.cs index 2e4c10f0e..2d1cf8473 100644 --- a/src/RESPite.Benchmark/NewCoreBenchmark.cs +++ b/src/RESPite.Benchmark/NewCoreBenchmark.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using RESPite.Connections; @@ -18,7 +19,7 @@ public sealed class NewCoreBenchmark : BenchmarkBase protected override RespContext GetClient(int index) => _clients[index]; - protected override Task Delete(RespContext client, string key) => client.DelAsync(key).AsTask(); + protected override Task DeleteAsync(RespContext client, string key) => client.DelAsync(key).AsTask(); protected override RespContext WithCancellation(RespContext client, CancellationToken cancellationToken) => client.WithCancellationToken(cancellationToken); @@ -218,57 +219,61 @@ private ValueTask LRangeInit(RespContext ctx) => private ValueTask XAdd(RespContext ctx) => ctx.XAddAsync(_streamKey, "*", "myfield", _payload); - public async Task RunBasicLoopAsync() - { - var client = GetClient(0); - _ = await client.DelAsync(_counterKey).ConfigureAwait(false); - - if (ClientCount <= 1) - { - await RunBasicLoopAsync(0); - } - else - { - Task[] tasks = new Task[ClientCount]; - for (int i = 0; i < ClientCount; i++) - { - var loopSnapshot = i; - tasks[i] = Task.Run(() => RunBasicLoopAsync(loopSnapshot)); - } - - await Task.WhenAll(tasks); - } - - return 0; - } - - public async Task RunBasicLoopAsync(int clientId) + protected override async Task RunBasicLoopAsync(int clientId) { + // The purpose of this is to represent a more realistic loop using natural code + // rather than code that is drowning in test infrastructure. var client = GetClient(clientId); var depth = PipelineDepth; - int localCount = 0; - long lastValue = await client.GetInt32Async(_counterKey).ConfigureAwait(false), - currentValue = lastValue; - var previous = DateTime.UtcNow; + int tickCount = 0; // this is just so we don't query DateTime. + long previousValue = (await client.GetInt32Async(_counterKey).ConfigureAwait(false)) ?? 0, + currentValue = previousValue; + var watch = Stopwatch.StartNew(); + long previousMillis = watch.ElapsedMilliseconds; - void Tick() + bool Tick() { - DateTime now; - if (clientId == 0 && ((now = DateTime.Now) - previous).TotalSeconds >= 1) + var currentMillis = watch.ElapsedMilliseconds; + var elapsedMillis = currentMillis - previousMillis; + if (elapsedMillis >= 1000) { - Console.WriteLine($"{currentValue - lastValue} ops in {now - previous}"); - previous = now; - lastValue = currentValue; - localCount = 0; + if (clientId == 0) // only one client needs to update the UI + { + var qty = currentValue - previousValue; + var seconds = elapsedMillis / 1000.0; + Console.WriteLine( + $"{qty:#,###,##0} ops in {seconds:#0.00}s, {qty / seconds:#,###,##0}/s\ttotal: {currentValue:#,###,###,##0}"); + + // reset for next UI update + previousValue = currentValue; + previousMillis = currentMillis; + } + + if (currentMillis >= 20_000) + { + if (clientId == 0) + { + Console.WriteLine(); + Console.WriteLine( + $"\t Overall: {currentValue:#,###,###,##0} ops in {currentMillis / 1000:#0.00}s, {currentValue / (currentMillis / 1000.0):#,###,##0}/s"); + Console.WriteLine(); + } + + return true; // stop after some time + } } + + tickCount = 0; + return false; } + if (depth <= 1) { while (true) { - await client.IncrAsync(_counterKey).ConfigureAwait(false); + currentValue = await client.IncrAsync(_counterKey).ConfigureAwait(false); - if (++localCount >= 1000) Tick(); + if (++tickCount >= 1000 && Tick()) break; // only check whether to output every N iterations } } else @@ -291,8 +296,8 @@ void Tick() currentValue = await pending[i].ConfigureAwait(false); } - localCount += depth; - if (localCount >= 1000) Tick(); + tickCount += depth; + if (tickCount >= 1000 && Tick()) break; // only check whether to output every N iterations } } } @@ -371,7 +376,7 @@ internal static partial RespParsers.ResponseSummary internal static partial int ZAdd(this in RespContext ctx, string key, double score, string payload); [RespCommand("get")] - internal static partial int GetInt32(this in RespContext ctx, string key); + internal static partial int? GetInt32(this in RespContext ctx, string key); [RespCommand] internal static partial RespParsers.ResponseSummary XAdd( diff --git a/src/RESPite.Benchmark/OldCoreBenchmark.cs b/src/RESPite.Benchmark/OldCoreBenchmark.cs index e758ea5ab..90fd86540 100644 --- a/src/RESPite.Benchmark/OldCoreBenchmark.cs +++ b/src/RESPite.Benchmark/OldCoreBenchmark.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Threading.Tasks; using StackExchange.Redis; @@ -42,7 +43,7 @@ public override void Dispose() } protected override IDatabaseAsync GetClient(int index) => _client; - protected override Task Delete(IDatabaseAsync client, string key) => client.KeyDeleteAsync(key); + protected override Task DeleteAsync(IDatabaseAsync client, string key) => client.KeyDeleteAsync(key); public override async Task RunAll() { @@ -86,6 +87,86 @@ protected override ValueTask Flush(IDatabaseAsync client) return default; } + protected override async Task RunBasicLoopAsync(int clientId) + { + // The purpose of this is to represent a more realistic loop using natural code + // rather than code that is drowning in test infrastructure. + var client = (IDatabase)GetClient(clientId); // need IDatabase for CreateBatch + var depth = PipelineDepth; + int tickCount = 0; // this is just so we don't query DateTime. + var tmp = await client.StringGetAsync(_counterKey).ConfigureAwait(false); + long previousValue = tmp.IsNull ? 0 : (long)tmp, currentValue = previousValue; + var watch = Stopwatch.StartNew(); + long previousMillis = watch.ElapsedMilliseconds; + + bool Tick() + { + var currentMillis = watch.ElapsedMilliseconds; + var elapsedMillis = currentMillis - previousMillis; + if (elapsedMillis >= 1000) + { + if (clientId == 0) // only one client needs to update the UI + { + var qty = currentValue - previousValue; + var seconds = elapsedMillis / 1000.0; + Console.WriteLine( + $"{qty:#,###,##0} ops in {seconds:#0.00}s, {qty / seconds:#,###,##0}/s\ttotal: {currentValue:#,###,###,##0}"); + + // reset for next UI update + previousValue = currentValue; + previousMillis = currentMillis; + } + + if (currentMillis >= 20_000) + { + if (clientId == 0) + { + Console.WriteLine(); + Console.WriteLine( + $"\t Overall: {currentValue:#,###,###,##0} ops in {currentMillis / 1000:#0.00}s, {currentValue / (currentMillis / 1000.0):#,###,##0}/s"); + Console.WriteLine(); + } + + return true; // stop after some time + } + } + + tickCount = 0; + return false; + } + + if (depth <= 1) + { + while (true) + { + currentValue = await client.StringIncrementAsync(_counterKey).ConfigureAwait(false); + + if (++tickCount >= 1000 && Tick()) break; // only check whether to output every N iterations + } + } + else + { + Task[] pending = new Task[depth]; + var batch = client.CreateBatch(depth); + while (true) + { + for (int i = 0; i < depth; i++) + { + pending[i] = batch.StringIncrementAsync(_counterKey); + } + + batch.Execute(); + for (int i = 0; i < depth; i++) + { + currentValue = await pending[i].ConfigureAwait(false); + } + + tickCount += depth; + if (tickCount >= 1000 && Tick()) break; // only check whether to output every N iterations + } + } + } + [DisplayName("GET")] private ValueTask Get(IDatabaseAsync client) => GetAndMeasureString(client); diff --git a/src/RESPite.Benchmark/Program.cs b/src/RESPite.Benchmark/Program.cs index cd25f5a67..34f3c790b 100644 --- a/src/RESPite.Benchmark/Program.cs +++ b/src/RESPite.Benchmark/Program.cs @@ -9,6 +9,7 @@ internal static class Program { private static async Task Main(string[] args) { + bool basic = false; try { List benchmarks = []; @@ -23,8 +24,8 @@ private static async Task Main(string[] args) benchmarks.Add(new NewCoreBenchmark(args)); break; case "--basic": - await BasicLoopAsync(args); - return 0; + basic = true; + break; } } @@ -42,7 +43,14 @@ private static async Task Main(string[] args) Console.WriteLine($"### {bench} ###"); } - await bench.RunAll().ConfigureAwait(false); + if (basic) + { + await bench.RunBasicLoopAsync().ConfigureAwait(false); + } + else + { + await bench.RunAll().ConfigureAwait(false); + } } } // ReSharper disable once LoopVariableIsNeverChangedInsideLoop @@ -61,12 +69,6 @@ private static async Task Main(string[] args) } } - private static Task BasicLoopAsync(string[] args) - { - using var bench = new NewCoreBenchmark(args); - return bench.RunBasicLoopAsync(); - } - internal static void WriteException(Exception? ex, [CallerMemberName] string operation = "") { Console.Error.WriteLine(); diff --git a/src/RESPite/Internal/StreamConnection.cs b/src/RESPite/Connections/Internal/StreamConnection.cs similarity index 99% rename from src/RESPite/Internal/StreamConnection.cs rename to src/RESPite/Connections/Internal/StreamConnection.cs index 4b2cef488..2f6cf8e96 100644 --- a/src/RESPite/Internal/StreamConnection.cs +++ b/src/RESPite/Connections/Internal/StreamConnection.cs @@ -7,11 +7,11 @@ using System.Buffers; using System.Collections.Concurrent; using System.Diagnostics; -using System.Net.Mime; using System.Runtime.CompilerServices; +using RESPite.Internal; using RESPite.Messages; -namespace RESPite.Internal; +namespace RESPite.Connections.Internal; internal sealed class StreamConnection : RespConnection { From d5f0b58767da7dd782c3d96c3ce46f77624edb3c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 4 Sep 2025 08:41:25 +0100 Subject: [PATCH 048/108] nit: using --- src/RESPite.Benchmark/NewCoreBenchmark.cs | 30 +++++++++++------------ 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/RESPite.Benchmark/NewCoreBenchmark.cs b/src/RESPite.Benchmark/NewCoreBenchmark.cs index 2d1cf8473..e626664b6 100644 --- a/src/RESPite.Benchmark/NewCoreBenchmark.cs +++ b/src/RESPite.Benchmark/NewCoreBenchmark.cs @@ -279,26 +279,24 @@ bool Tick() else { ValueTask[] pending = new ValueTask[depth]; - using (var batch = client.CreateBatch(depth)) + await using var batch = client.CreateBatch(depth); + var ctx = batch.Context; + while (true) { - var ctx = batch.Context; - while (true) + for (int i = 0; i < depth; i++) { - for (int i = 0; i < depth; i++) - { - pending[i] = ctx.IncrAsync(_counterKey); - } - - await batch.FlushAsync().ConfigureAwait(false); - batch.EnsureCapacity(depth); // batches don't assume re-use - for (int i = 0; i < depth; i++) - { - currentValue = await pending[i].ConfigureAwait(false); - } + pending[i] = ctx.IncrAsync(_counterKey); + } - tickCount += depth; - if (tickCount >= 1000 && Tick()) break; // only check whether to output every N iterations + await batch.FlushAsync().ConfigureAwait(false); + batch.EnsureCapacity(depth); // batches don't assume re-use + for (int i = 0; i < depth; i++) + { + currentValue = await pending[i].ConfigureAwait(false); } + + tickCount += depth; + if (tickCount >= 1000 && Tick()) break; // only check whether to output every N iterations } } } From 0b5e6add8e0d86f72c0c8bc017f257594f98f6ac Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 4 Sep 2025 17:09:39 +0100 Subject: [PATCH 049/108] intermediate: RESPite.SERedis --- Directory.Packages.props | 4 +- .../RespCommandGenerator.cs | 63 +- .../StackExchange.Redis.Build.csproj | 1 + src/Directory.Build.props | 3 +- .../RESPite.Benchmark.csproj | 2 +- src/RESPite.StackExchange.Redis/Global.cs | 9 + .../IRespContextProxy.cs | 10 + src/RESPite.StackExchange.Redis/Node.cs | 265 ++ src/RESPite.StackExchange.Redis/NodeServer.cs | 447 +++ .../ProxiedDatabase.cs | 3407 +++++++++++++++++ .../RESPite.StackExchange.Redis.csproj | 17 +- .../RedisCommands.Server.cs | 19 + .../RedisCommands.Strings.cs | 19 + .../RedisCommands.cs | 7 + .../RespMultiplexer.cs | 331 ++ .../RoutingRespConnection.cs | 15 + src/RESPite.StackExchange.Redis/Utils.cs | 34 + src/RESPite.StackExchange.Redis/readme.md | 5 + src/RESPite/RESPite.csproj | 1 + src/RESPite/RespConnection.cs | 36 +- .../StackExchange.Redis.csproj | 1 + tests/RESPite.Tests/LogWriter.cs | 11 + tests/RESPite.Tests/RESPite.Tests.csproj | 1 + tests/RESPite.Tests/RespMultiplexerTests.cs | 24 + 24 files changed, 4718 insertions(+), 14 deletions(-) create mode 100644 src/RESPite.StackExchange.Redis/Global.cs create mode 100644 src/RESPite.StackExchange.Redis/IRespContextProxy.cs create mode 100644 src/RESPite.StackExchange.Redis/Node.cs create mode 100644 src/RESPite.StackExchange.Redis/NodeServer.cs create mode 100644 src/RESPite.StackExchange.Redis/ProxiedDatabase.cs create mode 100644 src/RESPite.StackExchange.Redis/RedisCommands.Server.cs create mode 100644 src/RESPite.StackExchange.Redis/RedisCommands.Strings.cs create mode 100644 src/RESPite.StackExchange.Redis/RedisCommands.cs create mode 100644 src/RESPite.StackExchange.Redis/RespMultiplexer.cs create mode 100644 src/RESPite.StackExchange.Redis/RoutingRespConnection.cs create mode 100644 src/RESPite.StackExchange.Redis/Utils.cs create mode 100644 src/RESPite.StackExchange.Redis/readme.md create mode 100644 tests/RESPite.Tests/LogWriter.cs create mode 100644 tests/RESPite.Tests/RespMultiplexerTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 6d0a8b05f..16da209c0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,6 +11,9 @@ + + + @@ -27,7 +30,6 @@ - diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index 3bde746dc..261d91659 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -150,12 +150,15 @@ private static string GetName(ITypeSymbol type) } } - foreach (var member in method.ContainingType.GetMembers()) + if (context is null) { - if (member is IFieldSymbol { IsStatic: false } field && IsRespContext(field.Type)) + foreach (var member in method.ContainingType.GetMembers()) { - context = field.Name; - break; + if (member is IFieldSymbol { IsStatic: false } field && IsRespContext(field.Type)) + { + context = field.Name; + break; + } } } @@ -179,6 +182,58 @@ private static string GetName(ITypeSymbol type) } } + if (context is null) + { + // look for indirect from parameter + foreach (var param in method.Parameters) + { + if (IsIndirectRespContext(param.Type, out var memberName)) + { + context = $"{param.Name}.{memberName}"; + break; + } + } + } + if (context is null) + { + // look for indirect from field + foreach (var member in method.ContainingType.GetMembers()) + { + if (member is IFieldSymbol { IsStatic: false } field && IsIndirectRespContext(field.Type, out var memberName)) + { + context = $"{field.Name}.{memberName}"; + break; + } + } + } + + // See whether instead of x (param, etc) *being* a RespContext, it could be something that *provides* + // a RespContext; this is especially useful for using punned structs (that just wrap a RespContext) to + // narrow the methods into logical groups, i.e. "strings", "hashes", etc. + static bool IsIndirectRespContext(ITypeSymbol type, out string memberName) + { + foreach (var member in type.GetMembers()) + { + if (member is IFieldSymbol { IsStatic: false } field + && IsRespContext(field.Type)) + { + memberName = field.Name; + return true; + } + } + foreach (var member in type.GetMembers()) + { + if (member is IPropertySymbol { IsStatic: false } prop + && IsRespContext(prop.Type) && prop.GetMethod is not null) + { + memberName = prop.Name; + return true; + } + } + memberName = ""; + return false; + } + if (context is null) { // last ditch, get context from properties diff --git a/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj index 5363bef38..09b8b16da 100644 --- a/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj +++ b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj @@ -7,6 +7,7 @@ enable enable true + true diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 5fb9cc5c5..8e5481092 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -11,6 +11,7 @@ - + + diff --git a/src/RESPite.Benchmark/RESPite.Benchmark.csproj b/src/RESPite.Benchmark/RESPite.Benchmark.csproj index be171656d..f3066e102 100644 --- a/src/RESPite.Benchmark/RESPite.Benchmark.csproj +++ b/src/RESPite.Benchmark/RESPite.Benchmark.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/RESPite.StackExchange.Redis/Global.cs b/src/RESPite.StackExchange.Redis/Global.cs new file mode 100644 index 000000000..0751e11a8 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/Global.cs @@ -0,0 +1,9 @@ +#if NET5_0_OR_GREATER +[module:global::System.Runtime.CompilerServices.SkipLocalsInit] +#else +// we've gone some disambiguation to do... +extern alias seredis; +global using DoesNotReturnAttribute = seredis::System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute; + +[module:seredis::System.Runtime.CompilerServices.SkipLocalsInit] +#endif diff --git a/src/RESPite.StackExchange.Redis/IRespContextProxy.cs b/src/RESPite.StackExchange.Redis/IRespContextProxy.cs new file mode 100644 index 000000000..e52433ef0 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/IRespContextProxy.cs @@ -0,0 +1,10 @@ +namespace RESPite.StackExchange.Redis; + +/// +/// Provides access to a RESP context to use for operations; this context could be direct to a known server or routed. +/// +internal interface IRespContextProxy +{ + RespMultiplexer Multiplexer { get; } + ref readonly RespContext Context { get; } +} diff --git a/src/RESPite.StackExchange.Redis/Node.cs b/src/RESPite.StackExchange.Redis/Node.cs new file mode 100644 index 000000000..238d5d018 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/Node.cs @@ -0,0 +1,265 @@ +using System.Net; +using System.Net.Sockets; +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal sealed class Node : IDisposable, IAsyncDisposable, IRespContextProxy +{ + private bool _isDisposed; + + public Version Version { get; } + public EndPoint EndPoint => _interactive.EndPoint; + public RespMultiplexer Multiplexer => _interactive.Multiplexer; + public Node(RespMultiplexer multiplexer, EndPoint endPoint) + { + _interactive = new(multiplexer, endPoint, ConnectionType.Interactive); + Version = multiplexer.Options.DefaultVersion; + // defer on pub/sub + } + + public bool IsConnected => _interactive.IsConnected; + public bool IsConnecting => _interactive.IsConnecting; + + public void Dispose() + { + _isDisposed = true; + _interactive.Dispose(); + _subscription?.Dispose(); + } + + public async ValueTask DisposeAsync() + { + _isDisposed = true; + await _interactive.DisposeAsync().ConfigureAwait(false); + if (_subscription is { } obj) + { + await obj.DisposeAsync().ConfigureAwait(false); + } + } + + private readonly NodeConnection _interactive; + private NodeConnection? _subscription; + + public ref readonly RespContext Context => ref _interactive.Context; + public RespConnection InteractiveConnection => _interactive.Connection; + + public Task ConnectAsync(TextWriter? log = null, bool force = false, ConnectionType connectionType = ConnectionType.Interactive) + { + if (_isDisposed) return Task.FromResult(false); + if (connectionType == ConnectionType.Interactive) + { + return _interactive.ConnectAsync(log, force); + } + else if (connectionType == ConnectionType.Subscription) + { + _subscription ??= new(_interactive.Multiplexer, _interactive.EndPoint, ConnectionType.Subscription); + return _subscription.ConnectAsync(log, force); + } + else + { + throw new ArgumentOutOfRangeException(nameof(connectionType)); + } + } + + private IServer? _server; + public IServer AsServer() => _server ??= new NodeServer(this); +} + +internal sealed class NodeConnection : IDisposable, IAsyncDisposable, IRespContextProxy +{ + private EventHandler? _onConnectionError; + private readonly RespMultiplexer _multiplexer; + private readonly EndPoint _endPoint; + private readonly ConnectionType _connectionType; + + public RespMultiplexer Multiplexer => _multiplexer; + + public NodeConnection(RespMultiplexer multiplexer, EndPoint endPoint, ConnectionType connectionType) + { + _multiplexer = multiplexer; + _endPoint = endPoint; + _connectionType = connectionType; + _label = Format.ToString(endPoint); + } + + public EndPoint EndPoint => _endPoint; + private int _state = (int)NodeState.Disconnected; + private readonly string _label; + + public override string ToString() => $"{_label}: {State}"; + private NodeState State => (NodeState)_state; + + private enum NodeState + { + Disconnected, + Connecting, + Connected, + Faulted, + Disposed, + } + + public bool IsFaulted => State == NodeState.Faulted; + public bool IsConnected => State == NodeState.Connected; + public bool IsConnecting => State == NodeState.Connecting; + + public ref readonly RespContext Context => ref _connection.Context; + private RespConnection _connection = RespContext.Null.Connection; + public RespConnection Connection => _connection; + + public async Task ConnectAsync(TextWriter? log = null, bool force = false) + { + int state; + bool connecting = false; + do + { + state = _state; + switch ((NodeState)state) + { + case NodeState.Connected when force: + case NodeState.Connecting when force: + log.LogLocked($"[{_label}] (already {(NodeState)state}, but forcing reconnect...)"); + break; // reconnect anyway! + case NodeState.Connected: + case NodeState.Connecting: + log.LogLocked($"[{_label}] (already {(NodeState)state})"); + return true; + case NodeState.Disposed: + log.LogLocked($"[{_label}] (already {(NodeState)state})"); + return false; + } + } + // otherwise: move to connecting (or retry, if there was a race) + while (Interlocked.CompareExchange(ref _state, (int)NodeState.Connecting, state) != state); + + try + { + log.LogLocked($"[{_label}] Connecting..."); + connecting = true; + var connection = await RespConnection.CreateAsync( + _endPoint, + cancellationToken: _multiplexer.Lifetime).ConfigureAwait(false); + connecting = false; + + log.LogLocked($"[{_label}] Performing handshake..."); + // TODO: handshake + + // finalize the connections + log.LogLocked($"[{_label}] Finalizing..."); + var oldConnection = _connection; + _connection = connection; + await oldConnection.DisposeAsync().ConfigureAwait(false); + + // check nothing changed while we weren't looking + if (Interlocked.CompareExchange(ref _state, (int)NodeState.Connected, state) == state) + { + // success + log.LogLocked($"[{_label}] (success)"); + connection.ConnectionError += _onConnectionError ??= OnConnectionError; + + if (state == (int)NodeState.Faulted) OnConnectionRestored(); + return true; + } + + log.LogLocked($"[{_label}] (unable to complete; became {State})"); + _connection = oldConnection; + return false; + } + catch (Exception ex) + { + log.LogLocked($"[{_label}] Faulted: {ex.Message}"); + // something failed; cleanup and move to faulted, unless disposed + if (State != NodeState.Disposed) + { + _state = (int)NodeState.Faulted; + } + + var conn = _connection; + _connection = RespContext.Null.Connection; + await conn.DisposeAsync(); + + var failureType = ConnectionFailureType.InternalFailure; + if (connecting) + { + failureType = ConnectionFailureType.UnableToConnect; + } + else if (ex is SocketException) + { + failureType = ConnectionFailureType.SocketFailure; + } + else if (ex is ObjectDisposedException) + { + failureType = ConnectionFailureType.ConnectionDisposed; + } + + OnConnectionError(failureType, ex); + return false; + } + } + + private void OnConnectionError(object? sender, RespConnection.RespConnectionErrorEventArgs e) + { + var handler = _multiplexer.DirectConnectionFailed; + if (handler is not null) + { + handler(_multiplexer, new ConnectionFailedEventArgs( + handler, + _multiplexer, + _endPoint, + _connectionType, + ConnectionFailureType.InternalFailure, + e.Exception, + _label)); + } + } + + private void OnConnectionError(ConnectionFailureType failureType, Exception? exception = null) + { + var handler = _multiplexer.DirectConnectionFailed; + if (handler is not null) + { + handler(_multiplexer, new ConnectionFailedEventArgs( + handler, + _multiplexer, + _endPoint, + _connectionType, + failureType, + exception, + _label)); + } + } + + private void OnConnectionRestored() + { + var handler = _multiplexer.DirectConnectionRestored; + if (handler is not null) + { + handler(_multiplexer, new ConnectionFailedEventArgs( + handler, + _multiplexer, + _endPoint, + _connectionType, + ConnectionFailureType.None, + null, + _label)); + } + } + + public void Dispose() + { + _state = (int)NodeState.Disposed; + var conn = _connection; + _connection = RespContext.Null.Connection; + conn.Dispose(); + OnConnectionError(ConnectionFailureType.ConnectionDisposed); + } + + public async ValueTask DisposeAsync() + { + _state = (int)NodeState.Disposed; + var conn = _connection; + _connection = RespContext.Null.Connection; + await conn.DisposeAsync().ConfigureAwait(false); + OnConnectionError(ConnectionFailureType.ConnectionDisposed); + } +} diff --git a/src/RESPite.StackExchange.Redis/NodeServer.cs b/src/RESPite.StackExchange.Redis/NodeServer.cs new file mode 100644 index 000000000..c07cd90b0 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/NodeServer.cs @@ -0,0 +1,447 @@ +using System.Net; +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +/// +/// Implements IServer on top of a , which represents a fixed single connection +/// to a single redis instance. The connection exposed is the "interactive" connection. +/// +internal sealed class NodeServer(Node node) : IServer +{ + // deliberately not caching this - if the connection changes, we want to know about it + internal ref readonly RespContext Context => ref node.Context; + + public IConnectionMultiplexer Multiplexer => node.Multiplexer; + public Task PingAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public bool TryWait(Task task) => node.Multiplexer.TryWait(task); + + public void Wait(Task task) => node.Multiplexer.Wait(task); + + public T Wait(Task task) => node.Multiplexer.Wait(task); + + public void WaitAll(params Task[] tasks) => node.Multiplexer.WaitAll(tasks); + + public TimeSpan Ping(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public ClusterConfiguration? ClusterConfiguration { get; } + public EndPoint EndPoint => node.EndPoint; + public RedisFeatures Features => new(Version); + public bool IsConnected => node.IsConnected; + public RedisProtocol Protocol { get; } + public bool IsSlave { get; } + public bool IsReplica { get; } + public bool AllowSlaveWrites { get; set; } + public bool AllowReplicaWrites { get; set; } + public ServerType ServerType { get; } + public Version Version => node.Version; + public int DatabaseCount { get; } + public void ClientKill(EndPoint endpoint, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ClientKillAsync(EndPoint endpoint, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long ClientKill( + long? id = null, + ClientType? clientType = null, + EndPoint? endpoint = null, + bool skipMe = true, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ClientKillAsync( + long? id = null, + ClientType? clientType = null, + EndPoint? endpoint = null, + bool skipMe = true, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ClientKill(ClientKillFilter filter, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ClientKillAsync( + ClientKillFilter filter, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public ClientInfo[] ClientList(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ClientListAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public ClusterConfiguration? ClusterNodes(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ClusterNodesAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public string? ClusterNodesRaw(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ClusterNodesRawAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public KeyValuePair[] ConfigGet( + RedisValue pattern = default, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task[]> ConfigGetAsync( + RedisValue pattern = default, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public void ConfigResetStatistics(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ConfigResetStatisticsAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public void ConfigRewrite(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ConfigRewriteAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public void ConfigSet( + RedisValue setting, + RedisValue value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ConfigSetAsync( + RedisValue setting, + RedisValue value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long CommandCount(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task CommandCountAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisKey[] CommandGetKeys( + RedisValue[] command, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task CommandGetKeysAsync( + RedisValue[] command, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public string[] CommandList( + RedisValue? moduleName = null, + RedisValue? category = null, + RedisValue? pattern = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task CommandListAsync( + RedisValue? moduleName = null, + RedisValue? category = null, + RedisValue? pattern = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long DatabaseSize( + int database = -1, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task DatabaseSizeAsync( + int database = -1, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisValue Echo( + RedisValue message, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task EchoAsync( + RedisValue message, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisResult Execute(string command, params object[] args) => throw new NotImplementedException(); + + public Task ExecuteAsync(string command, params object[] args) => throw new NotImplementedException(); + + public RedisResult Execute( + string command, + ICollection args, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ExecuteAsync( + string command, + ICollection args, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public void FlushAllDatabases(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task FlushAllDatabasesAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public void FlushDatabase( + int database = -1, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task FlushDatabaseAsync( + int database = -1, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public ServerCounters GetCounters() => throw new NotImplementedException(); + + public IGrouping>[] Info( + RedisValue section = default, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task>[]> InfoAsync( + RedisValue section = default, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public string? InfoRaw( + RedisValue section = default, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task InfoRawAsync( + RedisValue section = default, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public IEnumerable Keys( + int database, + RedisValue pattern, + int pageSize, + CommandFlags flags) => throw new NotImplementedException(); + + public IEnumerable Keys( + int database = -1, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IAsyncEnumerable KeysAsync( + int database = -1, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public DateTime LastSave(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task LastSaveAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public void MakeMaster( + ReplicationChangeOptions options, + TextWriter? log = null) => throw new NotImplementedException(); + + public Task MakePrimaryAsync( + ReplicationChangeOptions options, + TextWriter? log = null) => throw new NotImplementedException(); + + public Role Role(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task RoleAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public void Save( + SaveType type, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SaveAsync( + SaveType type, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public bool ScriptExists( + string script, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ScriptExistsAsync( + string script, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public bool ScriptExists( + byte[] sha1, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ScriptExistsAsync( + byte[] sha1, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public void ScriptFlush(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ScriptFlushAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public byte[] ScriptLoad( + string script, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ScriptLoadAsync( + string script, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public LoadedLuaScript ScriptLoad( + LuaScript script, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ScriptLoadAsync( + LuaScript script, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public void Shutdown( + ShutdownMode shutdownMode = ShutdownMode.Default, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public void SlaveOf( + EndPoint master, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SlaveOfAsync( + EndPoint master, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public void ReplicaOf( + EndPoint master, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ReplicaOfAsync( + EndPoint master, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public CommandTrace[] SlowlogGet( + int count = 0, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SlowlogGetAsync( + int count = 0, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public void SlowlogReset(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SlowlogResetAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisChannel[] SubscriptionChannels( + RedisChannel pattern = default, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SubscriptionChannelsAsync( + RedisChannel pattern = default, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long SubscriptionPatternCount(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SubscriptionPatternCountAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long SubscriptionSubscriberCount( + RedisChannel channel, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SubscriptionSubscriberCountAsync( + RedisChannel channel, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public void SwapDatabases( + int first, + int second, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SwapDatabasesAsync( + int first, + int second, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public DateTime Time(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task TimeAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public string LatencyDoctor(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task LatencyDoctorAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long LatencyReset( + string[]? eventNames = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task LatencyResetAsync( + string[]? eventNames = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public LatencyHistoryEntry[] LatencyHistory( + string eventName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task LatencyHistoryAsync( + string eventName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public LatencyLatestEntry[] LatencyLatest(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task LatencyLatestAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public string MemoryDoctor(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task MemoryDoctorAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public void MemoryPurge(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task MemoryPurgeAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisResult MemoryStats(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task MemoryStatsAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public string? MemoryAllocatorStats(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task MemoryAllocatorStatsAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public EndPoint? SentinelGetMasterAddressByName( + string serviceName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SentinelGetMasterAddressByNameAsync( + string serviceName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public EndPoint[] SentinelGetSentinelAddresses( + string serviceName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SentinelGetSentinelAddressesAsync( + string serviceName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public EndPoint[] SentinelGetReplicaAddresses( + string serviceName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SentinelGetReplicaAddressesAsync( + string serviceName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public KeyValuePair[] SentinelMaster( + string serviceName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task[]> SentinelMasterAsync( + string serviceName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public KeyValuePair[][] SentinelMasters(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task[][]> SentinelMastersAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public KeyValuePair[][] SentinelSlaves( + string serviceName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task[][]> SentinelSlavesAsync( + string serviceName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public KeyValuePair[][] SentinelReplicas( + string serviceName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task[][]> SentinelReplicasAsync( + string serviceName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public void SentinelFailover( + string serviceName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SentinelFailoverAsync( + string serviceName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public KeyValuePair[][] SentinelSentinels( + string serviceName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task[][]> SentinelSentinelsAsync( + string serviceName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); +} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs new file mode 100644 index 000000000..2d2b10ecc --- /dev/null +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs @@ -0,0 +1,3407 @@ +using System.Net; +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +/// +/// Implements IDatabase on top of a , which provides access to a RESP context; this +/// could be direct to a known server or routed - the is responsible for +/// that determination. +/// +internal class ProxiedDatabase(IRespContextProxy proxy, int db) : IDatabase +{ + // Question: cache this, or rebuild each time? the latter handles shutdown better. + // internal readonly RespContext Context = proxy.Context.WithDatabase(db); + internal RespContext Context => proxy.Context.WithDatabase(db); + + public int Database => db; + + public IConnectionMultiplexer Multiplexer => proxy.Multiplexer; + public Task PingAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public bool TryWait(Task task) => proxy.Multiplexer.TryWait(task); + + public void Wait(Task task) => proxy.Multiplexer.Wait(task); + + public T Wait(Task task) => proxy.Multiplexer.Wait(task); + + public void WaitAll(params Task[] tasks) => proxy.Multiplexer.WaitAll(tasks); + + public TimeSpan Ping(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public bool IsConnected( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyMigrateAsync( + RedisKey key, + EndPoint toServer, + int toDatabase = 0, + int timeoutMilliseconds = 0, + MigrateOptions migrateOptions = MigrateOptions.None, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task DebugObjectAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoAddAsync( + RedisKey key, + double longitude, + double latitude, + RedisValue member, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoAddAsync( + RedisKey key, + GeoEntry value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoAddAsync( + RedisKey key, + GeoEntry[] values, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoRemoveAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoDistanceAsync( + RedisKey key, + RedisValue member1, + RedisValue member2, + GeoUnit unit = GeoUnit.Meters, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoHashAsync( + RedisKey key, + RedisValue[] members, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoHashAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoPositionAsync( + RedisKey key, + RedisValue[] members, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task GeoPositionAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoRadiusAsync( + RedisKey key, + RedisValue member, + double radius, + GeoUnit unit = GeoUnit.Meters, + int count = -1, + Order? order = null, + GeoRadiusOptions options = GeoRadiusOptions.Default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoRadiusAsync( + RedisKey key, + double longitude, + double latitude, + double radius, + GeoUnit unit = GeoUnit.Meters, + int count = -1, + Order? order = null, + GeoRadiusOptions options = GeoRadiusOptions.Default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoSearchAsync( + RedisKey key, + RedisValue member, + GeoSearchShape shape, + int count = -1, + bool demandClosest = true, + Order? order = null, + GeoRadiusOptions options = GeoRadiusOptions.Default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoSearchAsync( + RedisKey key, + double longitude, + double latitude, + GeoSearchShape shape, + int count = -1, + bool demandClosest = true, + Order? order = null, + GeoRadiusOptions options = GeoRadiusOptions.Default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoSearchAndStoreAsync( + RedisKey sourceKey, + RedisKey destinationKey, + RedisValue member, + GeoSearchShape shape, + int count = -1, + bool demandClosest = true, + Order? order = null, + bool storeDistances = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoSearchAndStoreAsync( + RedisKey sourceKey, + RedisKey destinationKey, + double longitude, + double latitude, + GeoSearchShape shape, + int count = -1, + bool demandClosest = true, + Order? order = null, + bool storeDistances = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashDecrementAsync( + RedisKey key, + RedisValue hashField, + long value = 1, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task HashDecrementAsync( + RedisKey key, + RedisValue hashField, + double value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task HashDeleteAsync( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashDeleteAsync( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashExistsAsync( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldGetAndDeleteAsync( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task?> HashFieldGetLeaseAndDeleteAsync( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task HashFieldGetAndDeleteAsync( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task HashFieldGetAndSetExpiryAsync( + RedisKey key, + RedisValue hashField, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldGetAndSetExpiryAsync( + RedisKey key, + RedisValue hashField, + DateTime expiry, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task?> HashFieldGetLeaseAndSetExpiryAsync( + RedisKey key, + RedisValue hashField, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task?> HashFieldGetLeaseAndSetExpiryAsync( + RedisKey key, + RedisValue hashField, + DateTime expiry, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldGetAndSetExpiryAsync( + RedisKey key, + RedisValue[] hashFields, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldGetAndSetExpiryAsync( + RedisKey key, + RedisValue[] hashFields, + DateTime expiry, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldSetAndSetExpiryAsync( + RedisKey key, + RedisValue field, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldSetAndSetExpiryAsync( + RedisKey key, + RedisValue field, + RedisValue value, + DateTime expiry, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldSetAndSetExpiryAsync( + RedisKey key, + HashEntry[] hashFields, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldSetAndSetExpiryAsync( + RedisKey key, + HashEntry[] hashFields, + DateTime expiry, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldExpireAsync( + RedisKey key, + RedisValue[] hashFields, + TimeSpan expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldExpireAsync( + RedisKey key, + RedisValue[] hashFields, + DateTime expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldGetExpireDateTimeAsync( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task HashFieldPersistAsync( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task HashFieldGetTimeToLiveAsync( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task HashGetAsync( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task?> HashGetLeaseAsync( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task HashGetAsync( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task HashGetAllAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashIncrementAsync( + RedisKey key, + RedisValue hashField, + long value = 1, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task HashIncrementAsync( + RedisKey key, + RedisValue hashField, + double value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task HashKeysAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashLengthAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashRandomFieldAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashRandomFieldsAsync( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashRandomFieldsWithValuesAsync( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public IAsyncEnumerable HashScanAsync( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IAsyncEnumerable HashScanNoValuesAsync( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashSetAsync( + RedisKey key, + HashEntry[] hashFields, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashSetAsync( + RedisKey key, + RedisValue hashField, + RedisValue value, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashStringLengthAsync( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashValuesAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HyperLogLogAddAsync( + RedisKey key, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HyperLogLogAddAsync( + RedisKey key, + RedisValue[] values, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HyperLogLogLengthAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HyperLogLogLengthAsync( + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HyperLogLogMergeAsync( + RedisKey destination, + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HyperLogLogMergeAsync( + RedisKey destination, + RedisKey[] sourceKeys, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task IdentifyEndpointAsync( + RedisKey key = default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyCopyAsync( + RedisKey sourceKey, + RedisKey destinationKey, + int destinationDatabase = -1, + bool replace = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyDeleteAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyDeleteAsync( + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyDumpAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyEncodingAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyExistsAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyExistsAsync( + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyExpireAsync( + RedisKey key, + TimeSpan? expiry, + CommandFlags flags) => + throw new NotImplementedException(); + + public Task KeyExpireAsync( + RedisKey key, + TimeSpan? expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyExpireAsync( + RedisKey key, + DateTime? expiry, + CommandFlags flags) => + throw new NotImplementedException(); + + public Task KeyExpireAsync( + RedisKey key, + DateTime? expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyExpireTimeAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyFrequencyAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyIdleTimeAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyMoveAsync( + RedisKey key, + int database, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyPersistAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyRandomAsync( + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task KeyRefCountAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyRenameAsync( + RedisKey key, + RedisKey newKey, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task KeyRestoreAsync( + RedisKey key, + byte[] value, + TimeSpan? expiry = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task KeyTimeToLiveAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyTouchAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyTouchAsync( + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyTypeAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListGetByIndexAsync( + RedisKey key, + long index, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListInsertAfterAsync( + RedisKey key, + RedisValue pivot, + RedisValue value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ListInsertBeforeAsync( + RedisKey key, + RedisValue pivot, + RedisValue value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ListLeftPopAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLeftPopAsync( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLeftPopAsync( + RedisKey[] keys, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListPositionAsync( + RedisKey key, + RedisValue element, + long rank = 1, + long maxLength = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListPositionsAsync( + RedisKey key, + RedisValue element, + long count, + long rank = 1, + long maxLength = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLeftPushAsync( + RedisKey key, + RedisValue value, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ListLeftPushAsync( + RedisKey key, + RedisValue[] values, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLeftPushAsync( + RedisKey key, + RedisValue[] values, + CommandFlags flags) => + throw new NotImplementedException(); + + public Task ListLengthAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListMoveAsync( + RedisKey sourceKey, + RedisKey destinationKey, + ListSide sourceSide, + ListSide destinationSide, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRangeAsync( + RedisKey key, + long start = 0, + long stop = -1, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ListRemoveAsync( + RedisKey key, + RedisValue value, + long count = 0, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ListRightPopAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPopAsync( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPopAsync( + RedisKey[] keys, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPopLeftPushAsync( + RedisKey source, + RedisKey destination, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ListRightPushAsync( + RedisKey key, + RedisValue value, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPushAsync( + RedisKey key, + RedisValue[] values, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPushAsync( + RedisKey key, + RedisValue[] values, + CommandFlags flags) => + throw new NotImplementedException(); + + public Task ListSetByIndexAsync( + RedisKey key, + long index, + RedisValue value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ListTrimAsync( + RedisKey key, + long start, + long stop, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task LockExtendAsync( + RedisKey key, + RedisValue value, + TimeSpan expiry, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task LockQueryAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task LockReleaseAsync( + RedisKey key, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task LockTakeAsync( + RedisKey key, + RedisValue value, + TimeSpan expiry, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task PublishAsync( + RedisChannel channel, + RedisValue message, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ExecuteAsync(string command, params object[] args) => throw new NotImplementedException(); + + public Task ExecuteAsync( + string command, + ICollection? args, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ScriptEvaluateAsync( + string script, + RedisKey[]? keys = null, + RedisValue[]? values = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ScriptEvaluateAsync( + byte[] hash, + RedisKey[]? keys = null, + RedisValue[]? values = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ScriptEvaluateAsync( + LuaScript script, + object? parameters = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ScriptEvaluateAsync( + LoadedLuaScript script, + object? parameters = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task ScriptEvaluateReadOnlyAsync( + string script, + RedisKey[]? keys = null, + RedisValue[]? values = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ScriptEvaluateReadOnlyAsync( + byte[] hash, + RedisKey[]? keys = null, + RedisValue[]? values = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetAddAsync( + RedisKey key, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetAddAsync( + RedisKey key, + RedisValue[] values, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetCombineAsync( + SetOperation operation, + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SetCombineAsync( + SetOperation operation, + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SetCombineAndStoreAsync( + SetOperation operation, + RedisKey destination, + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetCombineAndStoreAsync( + SetOperation operation, + RedisKey destination, + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetContainsAsync( + RedisKey key, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetContainsAsync( + RedisKey key, + RedisValue[] values, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetIntersectionLengthAsync( + RedisKey[] keys, + long limit = 0, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SetLengthAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetMembersAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetMoveAsync( + RedisKey source, + RedisKey destination, + RedisValue value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SetPopAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetPopAsync( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetRandomMemberAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetRandomMembersAsync( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetRemoveAsync( + RedisKey key, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetRemoveAsync( + RedisKey key, + RedisValue[] values, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IAsyncEnumerable SetScanAsync( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortAsync( + RedisKey key, + long skip = 0, + long take = -1, + Order order = Order.Ascending, + SortType sortType = SortType.Numeric, + RedisValue by = default, + RedisValue[]? get = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortAndStoreAsync( + RedisKey destination, + RedisKey key, + long skip = 0, + long take = -1, + Order order = Order.Ascending, + SortType sortType = SortType.Numeric, + RedisValue by = default, + RedisValue[]? get = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetAddAsync( + RedisKey key, + RedisValue member, + double score, + CommandFlags flags) => + throw new NotImplementedException(); + + public Task SortedSetAddAsync( + RedisKey key, + RedisValue member, + double score, + When when, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetAddAsync( + RedisKey key, + RedisValue member, + double score, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetAddAsync( + RedisKey key, + SortedSetEntry[] values, + CommandFlags flags) => + throw new NotImplementedException(); + + public Task SortedSetAddAsync( + RedisKey key, + SortedSetEntry[] values, + When when, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SortedSetAddAsync( + RedisKey key, + SortedSetEntry[] values, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetCombineAsync( + SetOperation operation, + RedisKey[] keys, + double[]? weights = null, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetCombineWithScoresAsync( + SetOperation operation, + RedisKey[] keys, + double[]? weights = null, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetCombineAndStoreAsync( + SetOperation operation, + RedisKey destination, + RedisKey first, + RedisKey second, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetCombineAndStoreAsync( + SetOperation operation, + RedisKey destination, + RedisKey[] keys, + double[]? weights = null, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetDecrementAsync( + RedisKey key, + RedisValue member, + double value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SortedSetIncrementAsync( + RedisKey key, + RedisValue member, + double value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SortedSetIntersectionLengthAsync( + RedisKey[] keys, + long limit = 0, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SortedSetLengthAsync( + RedisKey key, + double min = double.NegativeInfinity, + double max = double.PositiveInfinity, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetLengthByValueAsync( + RedisKey key, + RedisValue min, + RedisValue max, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRandomMemberAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRandomMembersAsync( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SortedSetRandomMembersWithScoresAsync( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SortedSetRangeByRankAsync( + RedisKey key, + long start = 0, + long stop = -1, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeAndStoreAsync( + RedisKey sourceKey, + RedisKey destinationKey, + RedisValue start, + RedisValue stop, + SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long? take = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeByRankWithScoresAsync( + RedisKey key, + long start = 0, + long stop = -1, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeByScoreAsync( + RedisKey key, + double start = double.NegativeInfinity, + double stop = double.PositiveInfinity, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeByScoreWithScoresAsync( + RedisKey key, + double start = double.NegativeInfinity, + double stop = double.PositiveInfinity, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeByValueAsync( + RedisKey key, + RedisValue min, + RedisValue max, + Exclude exclude, + long skip, + long take = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeByValueAsync( + RedisKey key, + RedisValue min = default, + RedisValue max = default, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRankAsync( + RedisKey key, + RedisValue member, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRemoveAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRemoveAsync( + RedisKey key, + RedisValue[] members, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRemoveRangeByRankAsync( + RedisKey key, + long start, + long stop, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SortedSetRemoveRangeByScoreAsync( + RedisKey key, + double start, + double stop, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRemoveRangeByValueAsync( + RedisKey key, + RedisValue min, + RedisValue max, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IAsyncEnumerable SortedSetScanAsync( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetScoreAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetScoresAsync( + RedisKey key, + RedisValue[] members, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SortedSetUpdateAsync( + RedisKey key, + RedisValue member, + double score, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetUpdateAsync( + RedisKey key, + SortedSetEntry[] values, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetPopAsync( + RedisKey key, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SortedSetPopAsync( + RedisKey key, + long count, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task SortedSetPopAsync( + RedisKey[] keys, + long count, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAcknowledgeAsync( + RedisKey key, + RedisValue groupName, + RedisValue messageId, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAcknowledgeAsync( + RedisKey key, + RedisValue groupName, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAcknowledgeAndDeleteAsync( + RedisKey key, + RedisValue groupName, + StreamTrimMode mode, + RedisValue messageId, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAcknowledgeAndDeleteAsync( + RedisKey key, + RedisValue groupName, + StreamTrimMode mode, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAddAsync( + RedisKey key, + RedisValue streamField, + RedisValue streamValue, + RedisValue? messageId, + int? maxLength, + bool useApproximateMaxLength, + CommandFlags flags) => + throw new NotImplementedException(); + + public Task StreamAddAsync( + RedisKey key, + NameValueEntry[] streamPairs, + RedisValue? messageId, + int? maxLength, + bool useApproximateMaxLength, + CommandFlags flags) => + throw new NotImplementedException(); + + public Task StreamAddAsync( + RedisKey key, + RedisValue streamField, + RedisValue streamValue, + RedisValue? messageId = null, + long? maxLength = null, + bool useApproximateMaxLength = false, + long? limit = null, + StreamTrimMode trimMode = StreamTrimMode.KeepReferences, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAddAsync( + RedisKey key, + NameValueEntry[] streamPairs, + RedisValue? messageId = null, + long? maxLength = null, + bool useApproximateMaxLength = false, + long? limit = null, + StreamTrimMode trimMode = StreamTrimMode.KeepReferences, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAutoClaimAsync( + RedisKey key, + RedisValue consumerGroup, + RedisValue claimingConsumer, + long minIdleTimeInMs, + RedisValue startAtId, + int? count = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAutoClaimIdsOnlyAsync( + RedisKey key, + RedisValue consumerGroup, + RedisValue claimingConsumer, + long minIdleTimeInMs, + RedisValue startAtId, + int? count = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamClaimAsync( + RedisKey key, + RedisValue consumerGroup, + RedisValue claimingConsumer, + long minIdleTimeInMs, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamClaimIdsOnlyAsync( + RedisKey key, + RedisValue consumerGroup, + RedisValue claimingConsumer, + long minIdleTimeInMs, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamConsumerGroupSetPositionAsync( + RedisKey key, + RedisValue groupName, + RedisValue position, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamConsumerInfoAsync( + RedisKey key, + RedisValue groupName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task StreamCreateConsumerGroupAsync( + RedisKey key, + RedisValue groupName, + RedisValue? position, + CommandFlags flags) => throw new NotImplementedException(); + + public Task StreamCreateConsumerGroupAsync( + RedisKey key, + RedisValue groupName, + RedisValue? position = null, + bool createStream = true, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamDeleteAsync( + RedisKey key, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamDeleteAsync( + RedisKey key, + RedisValue[] messageIds, + StreamTrimMode mode, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamDeleteConsumerAsync( + RedisKey key, + RedisValue groupName, + RedisValue consumerName, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamDeleteConsumerGroupAsync( + RedisKey key, + RedisValue groupName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task StreamGroupInfoAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamInfoAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamLengthAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamPendingAsync( + RedisKey key, + RedisValue groupName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task StreamPendingMessagesAsync( + RedisKey key, + RedisValue groupName, + int count, + RedisValue consumerName, + RedisValue? minId, + RedisValue? maxId, + CommandFlags flags) => + throw new NotImplementedException(); + + public Task StreamPendingMessagesAsync( + RedisKey key, + RedisValue groupName, + int count, + RedisValue consumerName, + RedisValue? minId = null, + RedisValue? maxId = null, + long? minIdleTimeInMs = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamRangeAsync( + RedisKey key, + RedisValue? minId = null, + RedisValue? maxId = null, + int? count = null, + Order messageOrder = Order.Ascending, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamReadAsync( + RedisKey key, + RedisValue position, + int? count = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task StreamReadAsync( + StreamPosition[] streamPositions, + int? countPerStream = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamReadGroupAsync( + RedisKey key, + RedisValue groupName, + RedisValue consumerName, + RedisValue? position, + int? count, + CommandFlags flags) => + throw new NotImplementedException(); + + public Task StreamReadGroupAsync( + RedisKey key, + RedisValue groupName, + RedisValue consumerName, + RedisValue? position = null, + int? count = null, + bool noAck = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamReadGroupAsync( + StreamPosition[] streamPositions, + RedisValue groupName, + RedisValue consumerName, + int? countPerStream, + CommandFlags flags) => + throw new NotImplementedException(); + + public Task StreamReadGroupAsync( + StreamPosition[] streamPositions, + RedisValue groupName, + RedisValue consumerName, + int? countPerStream = null, + bool noAck = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamTrimAsync( + RedisKey key, + int maxLength, + bool useApproximateMaxLength, + CommandFlags flags) => + throw new NotImplementedException(); + + public Task StreamTrimAsync( + RedisKey key, + long maxLength, + bool useApproximateMaxLength = false, + long? limit = null, + StreamTrimMode mode = StreamTrimMode.KeepReferences, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamTrimByMinIdAsync( + RedisKey key, + RedisValue minId, + bool useApproximateMaxLength = false, + long? limit = null, + StreamTrimMode mode = StreamTrimMode.KeepReferences, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringAppendAsync( + RedisKey key, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringBitCountAsync( + RedisKey key, + long start, + long end, + CommandFlags flags) => + throw new NotImplementedException(); + + public Task StringBitCountAsync( + RedisKey key, + long start = 0, + long end = -1, + StringIndexType indexType = StringIndexType.Byte, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringBitOperationAsync( + Bitwise operation, + RedisKey destination, + RedisKey first, + RedisKey second = default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringBitOperationAsync( + Bitwise operation, + RedisKey destination, + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringBitPositionAsync( + RedisKey key, + bool bit, + long start, + long end, + CommandFlags flags) => + throw new NotImplementedException(); + + public Task StringBitPositionAsync( + RedisKey key, + bool bit, + long start = 0, + long end = -1, + StringIndexType indexType = StringIndexType.Byte, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringDecrementAsync( + RedisKey key, + long value = 1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringDecrementAsync( + RedisKey key, + double value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringGetAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringGetAsync( + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task?> StringGetLeaseAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringGetBitAsync( + RedisKey key, + long offset, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringGetRangeAsync( + RedisKey key, + long start, + long end, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task StringGetSetAsync( + RedisKey key, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringGetSetExpiryAsync( + RedisKey key, + TimeSpan? expiry, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task StringGetSetExpiryAsync( + RedisKey key, + DateTime expiry, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task StringGetDeleteAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringGetWithExpiryAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringIncrementAsync( + RedisKey key, + long value = 1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringIncrementAsync( + RedisKey key, + double value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringLengthAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringLongestCommonSubsequenceAsync( + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task StringLongestCommonSubsequenceLengthAsync( + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task StringLongestCommonSubsequenceWithMatchesAsync( + RedisKey first, + RedisKey second, + long minLength = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringSetAsync( + RedisKey key, + RedisValue value, + TimeSpan? expiry, + When when) => + throw new NotImplementedException(); + + public Task StringSetAsync( + RedisKey key, + RedisValue value, + TimeSpan? expiry, + When when, + CommandFlags flags) => + throw new NotImplementedException(); + + public Task StringSetAsync( + RedisKey key, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringSetAsync( + KeyValuePair[] values, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task StringSetAndGetAsync( + RedisKey key, + RedisValue value, + TimeSpan? expiry, + When when, + CommandFlags flags) => throw new NotImplementedException(); + + public Task StringSetAndGetAsync( + RedisKey key, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringSetBitAsync( + RedisKey key, + long offset, + bool bit, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringSetRangeAsync( + RedisKey key, + long offset, + RedisValue value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public IBatch CreateBatch(object? asyncState = null) => throw new NotImplementedException(); + + public ITransaction CreateTransaction(object? asyncState = null) => throw new NotImplementedException(); + + public void KeyMigrate( + RedisKey key, + EndPoint toServer, + int toDatabase = 0, + int timeoutMilliseconds = 0, + MigrateOptions migrateOptions = MigrateOptions.None, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue DebugObject( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool GeoAdd( + RedisKey key, + double longitude, + double latitude, + RedisValue member, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public bool GeoAdd( + RedisKey key, + GeoEntry value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long GeoAdd( + RedisKey key, + GeoEntry[] values, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool GeoRemove( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public double? GeoDistance( + RedisKey key, + RedisValue member1, + RedisValue member2, + GeoUnit unit = GeoUnit.Meters, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public string?[] GeoHash( + RedisKey key, + RedisValue[] members, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public string? GeoHash( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public GeoPosition?[] GeoPosition( + RedisKey key, + RedisValue[] members, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public GeoPosition? GeoPosition( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public GeoRadiusResult[] GeoRadius( + RedisKey key, + RedisValue member, + double radius, + GeoUnit unit = GeoUnit.Meters, + int count = -1, + Order? order = null, + GeoRadiusOptions options = GeoRadiusOptions.Default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public GeoRadiusResult[] GeoRadius( + RedisKey key, + double longitude, + double latitude, + double radius, + GeoUnit unit = GeoUnit.Meters, + int count = -1, + Order? order = null, + GeoRadiusOptions options = GeoRadiusOptions.Default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public GeoRadiusResult[] GeoSearch( + RedisKey key, + RedisValue member, + GeoSearchShape shape, + int count = -1, + bool demandClosest = true, + Order? order = null, + GeoRadiusOptions options = GeoRadiusOptions.Default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public GeoRadiusResult[] GeoSearch( + RedisKey key, + double longitude, + double latitude, + GeoSearchShape shape, + int count = -1, + bool demandClosest = true, + Order? order = null, + GeoRadiusOptions options = GeoRadiusOptions.Default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long GeoSearchAndStore( + RedisKey sourceKey, + RedisKey destinationKey, + RedisValue member, + GeoSearchShape shape, + int count = -1, + bool demandClosest = true, + Order? order = null, + bool storeDistances = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long GeoSearchAndStore( + RedisKey sourceKey, + RedisKey destinationKey, + double longitude, + double latitude, + GeoSearchShape shape, + int count = -1, + bool demandClosest = true, + Order? order = null, + bool storeDistances = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long HashDecrement( + RedisKey key, + RedisValue hashField, + long value = 1, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public double HashDecrement( + RedisKey key, + RedisValue hashField, + double value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public bool HashDelete( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long HashDelete( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool HashExists( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public ExpireResult[] HashFieldExpire( + RedisKey key, + RedisValue[] hashFields, + TimeSpan expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public ExpireResult[] HashFieldExpire( + RedisKey key, + RedisValue[] hashFields, + DateTime expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long[] HashFieldGetExpireDateTime( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public PersistResult[] HashFieldPersist( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long[] HashFieldGetTimeToLive( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue HashGet( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Lease? HashGetLease( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] HashGet( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue HashFieldGetAndDelete( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Lease? HashFieldGetLeaseAndDelete( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisValue[] HashFieldGetAndDelete( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisValue HashFieldGetAndSetExpiry( + RedisKey key, + RedisValue hashField, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue HashFieldGetAndSetExpiry( + RedisKey key, + RedisValue hashField, + DateTime expiry, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Lease? HashFieldGetLeaseAndSetExpiry( + RedisKey key, + RedisValue hashField, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Lease? HashFieldGetLeaseAndSetExpiry( + RedisKey key, + RedisValue hashField, + DateTime expiry, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] HashFieldGetAndSetExpiry( + RedisKey key, + RedisValue[] hashFields, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] HashFieldGetAndSetExpiry( + RedisKey key, + RedisValue[] hashFields, + DateTime expiry, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue HashFieldSetAndSetExpiry( + RedisKey key, + RedisValue field, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue HashFieldSetAndSetExpiry( + RedisKey key, + RedisValue field, + RedisValue value, + DateTime expiry, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue HashFieldSetAndSetExpiry( + RedisKey key, + HashEntry[] hashFields, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue HashFieldSetAndSetExpiry( + RedisKey key, + HashEntry[] hashFields, + DateTime expiry, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public HashEntry[] HashGetAll( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long HashIncrement( + RedisKey key, + RedisValue hashField, + long value = 1, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public double HashIncrement( + RedisKey key, + RedisValue hashField, + double value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisValue[] HashKeys( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long HashLength( + RedisKey key, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisValue HashRandomField( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] HashRandomFields( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public HashEntry[] HashRandomFieldsWithValues( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IEnumerable HashScan( + RedisKey key, + RedisValue pattern, + int pageSize, + CommandFlags flags) => + throw new NotImplementedException(); + + public IEnumerable HashScan( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IEnumerable HashScanNoValues( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public void HashSet( + RedisKey key, + HashEntry[] hashFields, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool HashSet( + RedisKey key, + RedisValue hashField, + RedisValue value, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long HashStringLength( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] HashValues( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool HyperLogLogAdd( + RedisKey key, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool HyperLogLogAdd( + RedisKey key, + RedisValue[] values, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long HyperLogLogLength( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long HyperLogLogLength( + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public void HyperLogLogMerge( + RedisKey destination, + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public void HyperLogLogMerge( + RedisKey destination, + RedisKey[] sourceKeys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public EndPoint? IdentifyEndpoint( + RedisKey key = default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool KeyCopy( + RedisKey sourceKey, + RedisKey destinationKey, + int destinationDatabase = -1, + bool replace = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool KeyDelete( + RedisKey key, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long KeyDelete( + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public byte[]? KeyDump( + RedisKey key, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public string? KeyEncoding( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool KeyExists( + RedisKey key, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long KeyExists( + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool KeyExpire( + RedisKey key, + TimeSpan? expiry, + CommandFlags flags) => throw new NotImplementedException(); + + public bool KeyExpire( + RedisKey key, + TimeSpan? expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool KeyExpire( + RedisKey key, + DateTime? expiry, + CommandFlags flags) => throw new NotImplementedException(); + + public bool KeyExpire( + RedisKey key, + DateTime? expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public DateTime? KeyExpireTime( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long? KeyFrequency( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public TimeSpan? KeyIdleTime( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool KeyMove( + RedisKey key, + int database, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool KeyPersist( + RedisKey key, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisKey KeyRandom( + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long? KeyRefCount( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool KeyRename( + RedisKey key, + RedisKey newKey, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public void KeyRestore( + RedisKey key, + byte[] value, + TimeSpan? expiry = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public TimeSpan? KeyTimeToLive( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool KeyTouch( + RedisKey key, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long KeyTouch( + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisType KeyType( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue ListGetByIndex( + RedisKey key, + long index, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListInsertAfter( + RedisKey key, + RedisValue pivot, + RedisValue value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long ListInsertBefore( + RedisKey key, + RedisValue pivot, + RedisValue value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisValue ListLeftPop( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] ListLeftPop( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public ListPopResult ListLeftPop( + RedisKey[] keys, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListPosition( + RedisKey key, + RedisValue element, + long rank = 1, + long maxLength = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long[] ListPositions( + RedisKey key, + RedisValue element, + long count, + long rank = 1, + long maxLength = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListLeftPush( + RedisKey key, + RedisValue value, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long ListLeftPush( + RedisKey key, + RedisValue[] values, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long ListLeftPush( + RedisKey key, + RedisValue[] values, + CommandFlags flags) => + throw new NotImplementedException(); + + public long ListLength( + RedisKey key, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisValue ListMove( + RedisKey sourceKey, + RedisKey destinationKey, + ListSide sourceSide, + ListSide destinationSide, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] + ListRange(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListRemove( + RedisKey key, + RedisValue value, + long count = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue ListRightPop( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] ListRightPop( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public ListPopResult ListRightPop( + RedisKey[] keys, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue ListRightPopLeftPush( + RedisKey source, + RedisKey destination, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long ListRightPush( + RedisKey key, + RedisValue value, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long ListRightPush( + RedisKey key, + RedisValue[] values, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long ListRightPush( + RedisKey key, + RedisValue[] values, + CommandFlags flags) => + throw new NotImplementedException(); + + public void ListSetByIndex( + RedisKey key, + long index, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public void ListTrim( + RedisKey key, + long start, + long stop, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool LockExtend( + RedisKey key, + RedisValue value, + TimeSpan expiry, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue LockQuery( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool LockRelease( + RedisKey key, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool LockTake( + RedisKey key, + RedisValue value, + TimeSpan expiry, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long Publish( + RedisChannel channel, + RedisValue message, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult Execute(string command, params object[] args) => throw new NotImplementedException(); + + public RedisResult Execute( + string command, + ICollection args, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult ScriptEvaluate( + string script, + RedisKey[]? keys = null, + RedisValue[]? values = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult ScriptEvaluate( + byte[] hash, + RedisKey[]? keys = null, + RedisValue[]? values = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult ScriptEvaluate( + LuaScript script, + object? parameters = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisResult ScriptEvaluate( + LoadedLuaScript script, + object? parameters = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisResult ScriptEvaluateReadOnly( + string script, + RedisKey[]? keys = null, + RedisValue[]? values = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult ScriptEvaluateReadOnly( + byte[] hash, + RedisKey[]? keys = null, + RedisValue[]? values = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SetAdd( + RedisKey key, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetAdd( + RedisKey key, + RedisValue[] values, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SetCombine( + SetOperation operation, + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SetCombine( + SetOperation operation, + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetCombineAndStore( + SetOperation operation, + RedisKey destination, + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetCombineAndStore( + SetOperation operation, + RedisKey destination, + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SetContains( + RedisKey key, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool[] SetContains( + RedisKey key, + RedisValue[] values, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetIntersectionLength( + RedisKey[] keys, + long limit = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetLength( + RedisKey key, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisValue[] SetMembers( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SetMove( + RedisKey source, + RedisKey destination, + RedisValue value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisValue SetPop( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SetPop( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue SetRandomMember( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SetRandomMembers( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SetRemove( + RedisKey key, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetRemove( + RedisKey key, + RedisValue[] values, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IEnumerable SetScan( + RedisKey key, + RedisValue pattern, + int pageSize, + CommandFlags flags) => + throw new NotImplementedException(); + + public IEnumerable SetScan( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] Sort( + RedisKey key, + long skip = 0, + long take = -1, + Order order = Order.Ascending, + SortType sortType = SortType.Numeric, + RedisValue by = default, + RedisValue[]? get = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortAndStore( + RedisKey destination, + RedisKey key, + long skip = 0, + long take = -1, + Order order = Order.Ascending, + SortType sortType = SortType.Numeric, + RedisValue by = default, + RedisValue[]? get = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SortedSetAdd( + RedisKey key, + RedisValue member, + double score, + CommandFlags flags) => + throw new NotImplementedException(); + + public bool SortedSetAdd( + RedisKey key, + RedisValue member, + double score, + When when, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public bool SortedSetAdd( + RedisKey key, + RedisValue member, + double score, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetAdd( + RedisKey key, + SortedSetEntry[] values, + CommandFlags flags) => + throw new NotImplementedException(); + + public long SortedSetAdd( + RedisKey key, + SortedSetEntry[] values, + When when, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long SortedSetAdd( + RedisKey key, + SortedSetEntry[] values, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SortedSetCombine( + SetOperation operation, + RedisKey[] keys, + double[]? weights = null, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public SortedSetEntry[] SortedSetCombineWithScores( + SetOperation operation, + RedisKey[] keys, + double[]? weights = null, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetCombineAndStore( + SetOperation operation, + RedisKey destination, + RedisKey first, + RedisKey second, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetCombineAndStore( + SetOperation operation, + RedisKey destination, + RedisKey[] keys, + double[]? weights = null, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public double SortedSetDecrement( + RedisKey key, + RedisValue member, + double value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public double SortedSetIncrement( + RedisKey key, + RedisValue member, + double value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long SortedSetIntersectionLength( + RedisKey[] keys, + long limit = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetLength( + RedisKey key, + double min = double.NegativeInfinity, + double max = double.PositiveInfinity, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetLengthByValue( + RedisKey key, + RedisValue min, + RedisValue max, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue SortedSetRandomMember( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SortedSetRandomMembers( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public SortedSetEntry[] SortedSetRandomMembersWithScores(RedisKey key, long count, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisValue[] SortedSetRangeByRank(RedisKey key, long start = 0, long stop = -1, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetRangeAndStore( + RedisKey sourceKey, + RedisKey destinationKey, + RedisValue start, + RedisValue stop, + SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long? take = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public SortedSetEntry[] SortedSetRangeByRankWithScores( + RedisKey key, + long start = 0, + long stop = -1, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SortedSetRangeByScore( + RedisKey key, + double start = double.NegativeInfinity, + double stop = double.PositiveInfinity, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public SortedSetEntry[] SortedSetRangeByScoreWithScores( + RedisKey key, + double start = double.NegativeInfinity, + double stop = double.PositiveInfinity, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SortedSetRangeByValue( + RedisKey key, + RedisValue min, + RedisValue max, + Exclude exclude, + long skip, + long take = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SortedSetRangeByValue( + RedisKey key, + RedisValue min = default, + RedisValue max = default, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long? SortedSetRank( + RedisKey key, + RedisValue member, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SortedSetRemove( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetRemove( + RedisKey key, + RedisValue[] members, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetRemoveRangeByRank( + RedisKey key, + long start, + long stop, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long SortedSetRemoveRangeByScore( + RedisKey key, + double start, + double stop, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetRemoveRangeByValue( + RedisKey key, + RedisValue min, + RedisValue max, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IEnumerable + SortedSetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) => + throw new NotImplementedException(); + + public IEnumerable SortedSetScan( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public double? SortedSetScore( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public double?[] SortedSetScores( + RedisKey key, + RedisValue[] members, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public SortedSetEntry? SortedSetPop( + RedisKey key, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public SortedSetEntry[] SortedSetPop( + RedisKey key, + long count, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public SortedSetPopResult SortedSetPop( + RedisKey[] keys, + long count, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SortedSetUpdate( + RedisKey key, + RedisValue member, + double score, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetUpdate( + RedisKey key, + SortedSetEntry[] values, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamAcknowledge( + RedisKey key, + RedisValue groupName, + RedisValue messageId, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamAcknowledge( + RedisKey key, + RedisValue groupName, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamTrimResult StreamAcknowledgeAndDelete( + RedisKey key, + RedisValue groupName, + StreamTrimMode mode, + RedisValue messageId, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamTrimResult[] StreamAcknowledgeAndDelete(RedisKey key, + RedisValue groupName, + StreamTrimMode mode, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StreamAdd( + RedisKey key, + RedisValue streamField, + RedisValue streamValue, + RedisValue? messageId, + int? maxLength, + bool useApproximateMaxLength, + CommandFlags flags) => + throw new NotImplementedException(); + + public RedisValue StreamAdd( + RedisKey key, + NameValueEntry[] streamPairs, + RedisValue? messageId, + int? maxLength, + bool useApproximateMaxLength, + CommandFlags flags) => + throw new NotImplementedException(); + + public RedisValue StreamAdd( + RedisKey key, + RedisValue streamField, + RedisValue streamValue, + RedisValue? messageId = null, + long? maxLength = null, + bool useApproximateMaxLength = false, + long? limit = null, + StreamTrimMode trimMode = StreamTrimMode.KeepReferences, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StreamAdd( + RedisKey key, + NameValueEntry[] streamPairs, + RedisValue? messageId = null, + long? maxLength = null, + bool useApproximateMaxLength = false, + long? limit = null, + StreamTrimMode trimMode = StreamTrimMode.KeepReferences, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamAutoClaimResult StreamAutoClaim( + RedisKey key, + RedisValue consumerGroup, + RedisValue claimingConsumer, + long minIdleTimeInMs, + RedisValue startAtId, + int? count = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamAutoClaimIdsOnlyResult StreamAutoClaimIdsOnly( + RedisKey key, + RedisValue consumerGroup, + RedisValue claimingConsumer, + long minIdleTimeInMs, + RedisValue startAtId, + int? count = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamEntry[] StreamClaim( + RedisKey key, + RedisValue consumerGroup, + RedisValue claimingConsumer, + long minIdleTimeInMs, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] StreamClaimIdsOnly( + RedisKey key, + RedisValue consumerGroup, + RedisValue claimingConsumer, + long minIdleTimeInMs, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool StreamConsumerGroupSetPosition( + RedisKey key, + RedisValue groupName, + RedisValue position, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamConsumerInfo[] StreamConsumerInfo( + RedisKey key, + RedisValue groupName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public bool StreamCreateConsumerGroup( + RedisKey key, + RedisValue groupName, + RedisValue? position, + CommandFlags flags) => throw new NotImplementedException(); + + public bool StreamCreateConsumerGroup( + RedisKey key, + RedisValue groupName, + RedisValue? position = null, + bool createStream = true, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamDelete( + RedisKey key, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamTrimResult[] StreamDelete( + RedisKey key, + RedisValue[] messageIds, + StreamTrimMode mode, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamDeleteConsumer( + RedisKey key, + RedisValue groupName, + RedisValue consumerName, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool StreamDeleteConsumerGroup( + RedisKey key, + RedisValue groupName, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamGroupInfo[] StreamGroupInfo( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamInfo StreamInfo( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamLength( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamPendingInfo StreamPending( + RedisKey key, + RedisValue groupName, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamPendingMessageInfo[] StreamPendingMessages( + RedisKey key, + RedisValue groupName, + int count, + RedisValue consumerName, + RedisValue? minId, + RedisValue? maxId, + CommandFlags flags) => + throw new NotImplementedException(); + + public StreamPendingMessageInfo[] StreamPendingMessages( + RedisKey key, + RedisValue groupName, + int count, + RedisValue consumerName, + RedisValue? minId = null, + RedisValue? maxId = null, + long? minIdleTimeInMs = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamEntry[] StreamRange( + RedisKey key, + RedisValue? minId = null, + RedisValue? maxId = null, + int? count = null, + Order messageOrder = Order.Ascending, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamEntry[] StreamRead( + RedisKey key, + RedisValue position, + int? count = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisStream[] StreamRead( + StreamPosition[] streamPositions, + int? countPerStream = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamEntry[] StreamReadGroup( + RedisKey key, + RedisValue groupName, + RedisValue consumerName, + RedisValue? position, + int? count, + CommandFlags flags) => + throw new NotImplementedException(); + + public StreamEntry[] StreamReadGroup( + RedisKey key, + RedisValue groupName, + RedisValue consumerName, + RedisValue? position = null, + int? count = null, + bool noAck = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisStream[] StreamReadGroup( + StreamPosition[] streamPositions, + RedisValue groupName, + RedisValue consumerName, + int? countPerStream, + CommandFlags flags) => + throw new NotImplementedException(); + + public RedisStream[] StreamReadGroup( + StreamPosition[] streamPositions, + RedisValue groupName, + RedisValue consumerName, + int? countPerStream = null, + bool noAck = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamTrim( + RedisKey key, + int maxLength, + bool useApproximateMaxLength, + CommandFlags flags) => + throw new NotImplementedException(); + + public long StreamTrim( + RedisKey key, + long maxLength, + bool useApproximateMaxLength = false, + long? limit = null, + StreamTrimMode mode = StreamTrimMode.KeepReferences, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamTrimByMinId( + RedisKey key, + RedisValue minId, + bool useApproximateMaxLength = false, + long? limit = null, + StreamTrimMode mode = StreamTrimMode.KeepReferences, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StringAppend( + RedisKey key, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StringBitCount( + RedisKey key, + long start, + long end, + CommandFlags flags) => + throw new NotImplementedException(); + + public long StringBitCount(RedisKey key, long start = 0, long end = -1, + StringIndexType indexType = StringIndexType.Byte, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StringBitPosition(RedisKey key, bool bit, long start, long end, CommandFlags flags) => + throw new NotImplementedException(); + + public long StringBitPosition(RedisKey key, bool bit, long start = 0, long end = -1, + StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StringDecrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public double StringDecrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StringGet(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] StringGet(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Lease? StringGetLease(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool StringGetBit(RedisKey key, long offset, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StringGetRange(RedisKey key, long start, long end, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StringGetSet(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StringGetSetExpiry(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StringGetSetExpiry(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StringGetDelete(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValueWithExpiry StringGetWithExpiry(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StringIncrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public double StringIncrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public string? StringLongestCommonSubsequence(RedisKey first, RedisKey second, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public long StringLongestCommonSubsequenceLength(RedisKey first, RedisKey second, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public LCSMatchResult StringLongestCommonSubsequenceWithMatches(RedisKey first, RedisKey second, long minLength = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when) => + throw new NotImplementedException(); + + public bool StringSet( + RedisKey key, + RedisValue value, + TimeSpan? expiry, + When when, + CommandFlags flags) => + throw new NotImplementedException(); + + public bool StringSet( + RedisKey key, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool StringSet(KeyValuePair[] values, When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisValue + StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => + throw new NotImplementedException(); + + public RedisValue StringSetAndGet( + RedisKey key, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool StringSetBit(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StringSetRange(RedisKey key, long offset, RedisValue value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); +} diff --git a/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj b/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj index a8b89c0f4..c15007429 100644 --- a/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj +++ b/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj @@ -7,14 +7,19 @@ enable $(NoWarn);CS1591 2025 - $([System.DateTime]::Now.Year) Marc Gravell + readme.md + + + - - NullableHacks.cs - - - SkipLocalsInit.cs - + + + + + + + diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.Server.cs b/src/RESPite.StackExchange.Redis/RedisCommands.Server.cs new file mode 100644 index 000000000..65e5fca9f --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RedisCommands.Server.cs @@ -0,0 +1,19 @@ +using System.Runtime.CompilerServices; + +namespace RESPite.StackExchange.Redis; + +internal static partial class RedisCommands +{ + // this is just a "type pun" - it should be an invisible/magic pointer cast to the JIT + public static ref readonly Servers Servers(this in RespContext context) + => ref Unsafe.As(ref Unsafe.AsRef(in context)); +} +internal readonly struct Servers(in RespContext context) +{ + public readonly RespContext Context = context; // important: this is the only field +} +internal static partial class ServerCommands +{ + [RespCommand] + internal static partial void Ping(this in RespContext ctx); +} diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.Strings.cs b/src/RESPite.StackExchange.Redis/RedisCommands.Strings.cs new file mode 100644 index 000000000..fb865efcb --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RedisCommands.Strings.cs @@ -0,0 +1,19 @@ +using System.Runtime.CompilerServices; + +namespace RESPite.StackExchange.Redis; + +internal static partial class RedisCommands +{ + // this is just a "type pun" - it should be an invisible/magic pointer cast to the JIT + public static ref readonly Strings Strings(this in RespContext context) + => ref Unsafe.As(ref Unsafe.AsRef(in context)); +} + +internal readonly struct Strings(in RespContext context) +{ + public readonly RespContext Context = context; // important: this is the only field +} + +internal static partial class StringCommands +{ +} diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.cs new file mode 100644 index 000000000..603b7d6e7 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RedisCommands.cs @@ -0,0 +1,7 @@ +namespace RESPite.StackExchange.Redis; + +internal static partial class RedisCommands +{ + public static ref readonly RespContext Self(this in RespContext context) + => ref context; // this just proves that the above are well-defined in terms of escape analysis +} diff --git a/src/RESPite.StackExchange.Redis/RespMultiplexer.cs b/src/RESPite.StackExchange.Redis/RespMultiplexer.cs new file mode 100644 index 000000000..898beba06 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RespMultiplexer.cs @@ -0,0 +1,331 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; +using StackExchange.Redis; +using StackExchange.Redis.Maintenance; +using StackExchange.Redis.Profiling; + +namespace RESPite.StackExchange.Redis; + +public sealed class RespMultiplexer : IConnectionMultiplexer, IRespContextProxy +{ + /// + public override string ToString() => GetType().Name; + + public RespMultiplexer() + { + _routedConnection = _defaultConnection = RespContext.Null.Connection; // until we've connected + } + + private int _defaultDatabase; + + // the routed connection performs message-inspection based routing; on a single node + // instance that isn't necessary, so the default-connection abstracts over that: + // in a single-node instance, the default-connection will be the single interactive connection + // otherwise, the default-connection will be the routed connection + private RespConnection _routedConnection, _defaultConnection; + internal ref readonly RespContext Context => ref _defaultConnection.Context; + ref readonly RespContext IRespContextProxy.Context => ref _defaultConnection.Context; + RespMultiplexer IRespContextProxy.Multiplexer => this; + + private readonly CancellationTokenSource _lifetime = new(); + private ConfigurationOptions? _options; + internal RespConfiguration Configuration { get; private set; } = RespConfiguration.Default; + private Node[] _nodes = []; + internal CancellationToken Lifetime => _lifetime.Token; + internal ConfigurationOptions Options => _options ?? ThrowNotConnected(); + + [DoesNotReturn] + private static ConfigurationOptions ThrowNotConnected() + => throw new InvalidOperationException($"The {nameof(RespMultiplexer)} has not been connected."); + + private void OnConnect(ConfigurationOptions options) + { + if (Interlocked.CompareExchange(ref _options, options, null) is not null) + { + throw new InvalidOperationException($"A {GetType().Name} can only be connected once."); + } + + // add endpoints from the new options + const int DefaultPort = 6379; + int count = options.EndPoints.Count; + switch (count) + { + case 0: + _nodes = [new Node(this, new IPEndPoint(IPAddress.Loopback, DefaultPort))]; + break; + case 1: + _nodes = [new Node(this, options.EndPoints[0])]; + break; + default: + var nodes = new Node[count]; + for (int i = 0; i < nodes.Length; i++) + { + nodes[i] = new Node(this, options.EndPoints[i]); + } + + _nodes = nodes; + break; + } + + _defaultDatabase = options.DefaultDatabase ?? 0; + + // setup a basic connection that comes via ourselves (this might get simplified later, in OnNodesChanged) + var ctx = RespContext.Null; // this is just the template + _defaultConnection = _routedConnection = new RoutingRespConnection(this, ctx); + } + + public void Connect(string configuration = "", TextWriter? log = null) + { + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + var config = ConfigurationOptions.Parse(configuration ?? ""); + Connect(config, log); + } + + public void Connect(ConfigurationOptions options, TextWriter? log = null) + // use sync over async; reduce code-duplication, and sync wouldn't add anything + => ConnectAsync(options, log).Wait(Lifetime); + + public Task ConnectAsync(string configuration = "", TextWriter? log = null) + { + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + var config = ConfigurationOptions.Parse(configuration ?? ""); + return ConnectAsync(config, log); + } + + public async Task ConnectAsync(ConfigurationOptions options, TextWriter? log = null) + { + OnConnect(options); + var snapshot = _nodes; + log.LogLocked($"Connecting to {snapshot.Length} nodes..."); + Task[] pending = new Task[snapshot.Length]; + for (int i = 0; i < snapshot.Length; i++) + { + pending[i] = snapshot[i].ConnectAsync(log); + } + + await Task.WhenAll(pending).ConfigureAwait(false); + int success = 0; + foreach (var task in pending) + { + // note WhenAll ensures all connected + if (task.Result) success++; + } + + // configure our primary connection + OnNodesChanged(); + + log.LogLocked($"Connected to {success} of {snapshot.Length} nodes."); + } + + public void Dispose() + { + RespConnection c1 = _routedConnection, c2 = _defaultConnection; + _defaultConnection = _routedConnection = RespContext.Null.Connection; + _lifetime.Cancel(); + c1.Dispose(); + c2.Dispose(); + _routedConnection.Dispose(); + foreach (var node in _nodes) + { + node.Dispose(); + } + } + + public async ValueTask DisposeAsync() + { + RespConnection c1 = _routedConnection, c2 = _defaultConnection; + _defaultConnection = _routedConnection = RespContext.Null.Connection; +#if NET8_0_OR_GREATER + await _lifetime.CancelAsync().ConfigureAwait(false); +#else + _lifetime.Cancel(); +#endif + await c1.DisposeAsync().ConfigureAwait(false); + await c2.DisposeAsync().ConfigureAwait(false); + await _routedConnection.DisposeAsync().ConfigureAwait(false); + foreach (var node in _nodes) + { + await node.DisposeAsync().ConfigureAwait(false); + } + } + + public string ClientName { get; private set; } = ""; + string IConnectionMultiplexer.Configuration => Options.ToString(includePassword: false); + public int TimeoutMilliseconds => (int)Configuration.SyncTimeout.TotalMilliseconds; + public long OperationCount => 0; + + public bool PreserveAsyncOrder + { + get => false; + [Obsolete( + "Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue) + + " - this will be removed in 3.0.", + false)] + set { } + } + + public bool IsConnected + { + get + { + foreach (var node in _nodes) + { + if (node.IsConnected) return true; + } + + return false; + } + } + + public bool IsConnecting + { + get + { + foreach (var node in _nodes) + { + if (node.IsConnecting) return true; + } + + return false; + } + } + + public bool IncludeDetailInExceptions { get; set; } + public int StormLogThreshold { get; set; } + + public void RegisterProfiler(Func profilingSessionProvider) => + throw new NotImplementedException(); + + public ServerCounters GetCounters() => throw new NotImplementedException(); + + public event EventHandler? ConnectionFailed; + public event EventHandler? ConnectionRestored; + internal EventHandler? DirectConnectionFailed => ConnectionFailed; + internal EventHandler? DirectConnectionRestored => ConnectionRestored; + + public event EventHandler? ErrorMessage; + public event EventHandler? InternalError; + public event EventHandler? ConfigurationChanged; + public event EventHandler? ConfigurationChangedBroadcast; + public event EventHandler? ServerMaintenanceEvent; + + internal void OnErrorMessage(RedisErrorEventArgs e) => ErrorMessage?.Invoke(this, e); + internal void OnInternalError(InternalErrorEventArgs e) => InternalError?.Invoke(this, e); + internal void OnConfigurationChanged(EndPointEventArgs e) => ConfigurationChanged?.Invoke(this, e); + + internal void OnConfigurationChangedBroadcast(EndPointEventArgs e) => + ConfigurationChangedBroadcast?.Invoke(this, e); + + internal void OnServerMaintenanceEvent(ServerMaintenanceEvent e) => ServerMaintenanceEvent?.Invoke(this, e); + + public EndPoint[] GetEndPoints(bool configuredOnly = false) => configuredOnly + ? Options.EndPoints.ToArray() + : Array.ConvertAll(_nodes, x => x.EndPoint); + + public bool TryWait(Task task) => task.Wait(Configuration.SyncTimeout); + + public void Wait(Task task) + { + bool timeout; + try + { + timeout = !task.Wait(Configuration.SyncTimeout); + } + catch (AggregateException ex) when (ex.InnerExceptions.Count == 1) + { + throw ex.InnerException ?? ex; + } + + if (timeout) ThrowTimeout(); + } + + private static void ThrowTimeout() => throw new TimeoutException(); + + public T Wait(Task task) + { + Wait((Task)task); + return task.Result; + } + + public void WaitAll(params Task[] tasks) => throw new NotImplementedException(); + + public event EventHandler? HashSlotMoved; + internal void OnHashSlotMoved(HashSlotMovedEventArgs e) => HashSlotMoved?.Invoke(this, e); + public int HashSlot(RedisKey key) => throw new NotImplementedException(); + + public ISubscriber GetSubscriber(object? asyncState = null) => throw new NotImplementedException(); + + public IDatabase GetDatabase(int db = -1, object? asyncState = null) + { + if (db < 0) db = _defaultDatabase; + if (db < LowDatabaseCount) return _lowDatabases[db] ??= new ProxiedDatabase(this, db); + return new ProxiedDatabase(this, db); + } + + private const int LowDatabaseCount = 16; + private readonly IDatabase?[] _lowDatabases = new IDatabase?[LowDatabaseCount]; + + public IServer GetServer(string host, int port, object? asyncState = null) => throw new NotImplementedException(); + + public IServer GetServer(string hostAndPort, object? asyncState = null) => throw new NotImplementedException(); + + public IServer GetServer(IPAddress host, int port) + { + var nodes = _nodes; + foreach (var node in nodes) + { + if (node.EndPoint is IPEndPoint ep && ep.Address.Equals(host) && ep.Port == port) + { + return node.AsServer(); + } + } + + ThrowArgumentException(); + return null!; + } + + private void OnNodesChanged() + { + var nodes = _nodes; + _defaultConnection = nodes.Length switch + { + 0 => RespContext.Null.Connection, + 1 when nodes[0] is { IsConnected: true } node => node.InteractiveConnection, + _ => _routedConnection, + }; + } + + private static void ThrowArgumentException() => throw new ArgumentException(); + + public IServer GetServer(EndPoint endpoint, object? asyncState = null) => throw new NotImplementedException(); + + public IServer[] GetServers() => throw new NotImplementedException(); + + public Task ConfigureAsync(TextWriter? log = null) => throw new NotImplementedException(); + + public bool Configure(TextWriter? log = null) => throw new NotImplementedException(); + + public string GetStatus() => throw new NotImplementedException(); + + public void GetStatus(TextWriter log) => throw new NotImplementedException(); + + public void Close(bool allowCommandsToComplete = true) => throw new NotImplementedException(); + + public Task CloseAsync(bool allowCommandsToComplete = true) => throw new NotImplementedException(); + + public string? GetStormLog() => throw new NotImplementedException(); + + public void ResetStormLog() => throw new NotImplementedException(); + + public long PublishReconfigure(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public int GetHashSlot(RedisKey key) => throw new NotImplementedException(); + + public void ExportConfiguration(Stream destination, ExportOptions options = ExportOptions.All) => + throw new NotImplementedException(); + + public void AddLibraryNameSuffix(string suffix) => throw new NotImplementedException(); +} diff --git a/src/RESPite.StackExchange.Redis/RoutingRespConnection.cs b/src/RESPite.StackExchange.Redis/RoutingRespConnection.cs new file mode 100644 index 000000000..00e4dcb10 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RoutingRespConnection.cs @@ -0,0 +1,15 @@ +namespace RESPite.StackExchange.Redis; + +internal sealed class RoutingRespConnection(RespMultiplexer multiplexer, in RespContext tail) + : RespConnection(in tail, multiplexer.Configuration) +{ + public override event EventHandler? ConnectionError; + internal override int OutstandingOperations => 0; + + internal void OnConnectionError(RespConnectionErrorEventArgs e) => ConnectionError?.Invoke(this, e); + + public override void Write(in RespOperation message) + { + throw new NotImplementedException(); + } +} diff --git a/src/RESPite.StackExchange.Redis/Utils.cs b/src/RESPite.StackExchange.Redis/Utils.cs new file mode 100644 index 000000000..86f187cad --- /dev/null +++ b/src/RESPite.StackExchange.Redis/Utils.cs @@ -0,0 +1,34 @@ +using System.Net; + +namespace RESPite.StackExchange.Redis; + +internal static class Utils +{ + internal static void LogLocked(this TextWriter? writer, string message) + { + if (writer is null) return; + lock (writer) + { + writer.WriteLine(message); + } + } + +#if NET10_0_OR_GREATER + internal static void LogLocked( + this TextWriter? writer, + ref System.Runtime.CompilerServices.DefaultInterpolatedStringHandler message) + { + if (writer is null) + { + message.Clear(); + } + else + { + lock (writer) + { + writer.WriteLine(message.ToStringAndClear()); + } + } + } +#endif +} diff --git a/src/RESPite.StackExchange.Redis/readme.md b/src/RESPite.StackExchange.Redis/readme.md new file mode 100644 index 000000000..4b193bf5d --- /dev/null +++ b/src/RESPite.StackExchange.Redis/readme.md @@ -0,0 +1,5 @@ +# RESPite.StackExchange.Redis + +This libary is a bridge between StackExchange.Redis and RESPite. It provides the `IConnectionMultiplexer`, +`IDatabase`, `IServer` APIs, but implemented using the `RespConnection` and `RespContext` primitives from +RESPite. This is the intended direction for StackExchange.Redis vFuture. \ No newline at end of file diff --git a/src/RESPite/RESPite.csproj b/src/RESPite/RESPite.csproj index c7526160c..24fd9e210 100644 --- a/src/RESPite/RESPite.csproj +++ b/src/RESPite/RESPite.csproj @@ -18,6 +18,7 @@ + diff --git a/src/RESPite/RespConnection.cs b/src/RESPite/RespConnection.cs index 4483bbd53..ddde8942d 100644 --- a/src/RESPite/RespConnection.cs +++ b/src/RESPite/RespConnection.cs @@ -10,7 +10,8 @@ namespace RESPite; public abstract class RespConnection : IDisposable, IAsyncDisposable { - public sealed class RespConnectionErrorEventArgs(Exception exception, [CallerMemberName] string operation = "") : EventArgs + public sealed class RespConnectionErrorEventArgs(Exception exception, [CallerMemberName] string operation = "") + : EventArgs { public Exception Exception { get; } = exception; public string Operation { get; } = operation; @@ -23,6 +24,7 @@ public sealed class RespConnectionErrorEventArgs(Exception exception, [CallerMem public ref readonly RespContext Context => ref _context; public RespConfiguration Configuration { get; } public abstract event EventHandler? ConnectionError; + private protected static void OnConnectionError( EventHandler? handler, Exception exception, @@ -41,6 +43,7 @@ private protected static void OnConnectionError( private static EndPoint? _defaultEndPoint; // do not expose externally; vexingly mutable private static EndPoint DefaultEndPoint => _defaultEndPoint ??= new IPEndPoint(IPAddress.Loopback, 6379); + public static RespConnection Create(Stream stream, RespConfiguration? configuration = null) => new StreamConnection(configuration ?? RespConfiguration.Default, stream); @@ -52,6 +55,37 @@ public static RespConnection Create(EndPoint? endpoint = null, RespConfiguration return Create(new NetworkStream(socket), config); } + public static async ValueTask CreateAsync( + EndPoint? endpoint = null, + RespConfiguration? config = null, + CancellationToken cancellationToken = default) + { + Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.NoDelay = true; +#if NET6_0_OR_GREATER + await socket.ConnectAsync(endpoint ?? DefaultEndPoint, cancellationToken).ConfigureAwait(false); +#else + // hack together cancellation via dispose + using (var reg = cancellationToken.Register( + static state => ((Socket)state).Dispose(), socket)) + { + try + { + await socket.ConnectAsync(endpoint).ConfigureAwait(false); + } + catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(cancellationToken); + } + catch (SocketException) when (cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(cancellationToken); + } + } +#endif + return Create(new NetworkStream(socket), config); + } + // this is the usual usage, since we want context to be preserved private protected RespConnection(in RespContext tail, RespConfiguration? configuration = null) { diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index 2ef94f330..27ff1b793 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -46,5 +46,6 @@ + \ No newline at end of file diff --git a/tests/RESPite.Tests/LogWriter.cs b/tests/RESPite.Tests/LogWriter.cs new file mode 100644 index 000000000..45ffce8b6 --- /dev/null +++ b/tests/RESPite.Tests/LogWriter.cs @@ -0,0 +1,11 @@ +using System.IO; +using System.Text; +using Xunit; + +namespace RESPite.Tests; + +internal sealed class LogWriter(ITestOutputHelper? log) : TextWriter +{ + public override Encoding Encoding => Encoding.Unicode; + public override void WriteLine(string? value) => log?.WriteLine(value ?? ""); +} diff --git a/tests/RESPite.Tests/RESPite.Tests.csproj b/tests/RESPite.Tests/RESPite.Tests.csproj index 935187312..90f681c9b 100644 --- a/tests/RESPite.Tests/RESPite.Tests.csproj +++ b/tests/RESPite.Tests/RESPite.Tests.csproj @@ -16,6 +16,7 @@ + diff --git a/tests/RESPite.Tests/RespMultiplexerTests.cs b/tests/RESPite.Tests/RespMultiplexerTests.cs new file mode 100644 index 000000000..5c14662b1 --- /dev/null +++ b/tests/RESPite.Tests/RespMultiplexerTests.cs @@ -0,0 +1,24 @@ +using System.Linq; +using System.Threading.Tasks; +using RESPite.StackExchange.Redis; +using Xunit; + +namespace RESPite.Tests; + +public class RespMultiplexerTests(ITestOutputHelper log) +{ + private readonly LogWriter logWriter = new(log); + + [Fact] + public async Task CanConnect() + { + await using var muxer = new RespMultiplexer(); + await muxer.ConnectAsync(log: logWriter); + Assert.True(muxer.IsConnected); + + var server = muxer.GetServer(muxer.GetEndPoints().Single()); + Assert.IsType(server); // we expect this to *not* use routing + server.Ping(); + await server.PingAsync(); + } +} From 579efc909faef41bf964424d411e832596c8835e Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 4 Sep 2025 17:13:50 +0100 Subject: [PATCH 050/108] last of the chops --- .../ProxiedDatabase.cs | 88 +++++++++++++++---- 1 file changed, 69 insertions(+), 19 deletions(-) diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs index 2d2b10ecc..811b976a6 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs @@ -2818,10 +2818,15 @@ public RedisValue[] SortedSetRandomMembers( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public SortedSetEntry[] SortedSetRandomMembersWithScores(RedisKey key, long count, + public SortedSetEntry[] SortedSetRandomMembersWithScores( + RedisKey key, + long count, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue[] SortedSetRangeByRank(RedisKey key, long start = 0, long stop = -1, + public RedisValue[] SortedSetRangeByRank( + RedisKey key, + long start = 0, + long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -3012,7 +3017,8 @@ public StreamTrimResult StreamAcknowledgeAndDelete( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public StreamTrimResult[] StreamAcknowledgeAndDelete(RedisKey key, + public StreamTrimResult[] StreamAcknowledgeAndDelete( + RedisKey key, RedisValue groupName, StreamTrimMode mode, RedisValue[] messageIds, @@ -3288,33 +3294,61 @@ public long StringBitCount( CommandFlags flags) => throw new NotImplementedException(); - public long StringBitCount(RedisKey key, long start = 0, long end = -1, + public long StringBitCount( + RedisKey key, + long start = 0, + long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default, + public long StringBitOperation( + Bitwise operation, + RedisKey destination, + RedisKey first, + RedisKey second = default, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey[] keys, + public long StringBitOperation( + Bitwise operation, + RedisKey destination, + RedisKey[] keys, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long StringBitPosition(RedisKey key, bool bit, long start, long end, CommandFlags flags) => + public long StringBitPosition( + RedisKey key, + bool bit, + long start, + long end, + CommandFlags flags) => throw new NotImplementedException(); - public long StringBitPosition(RedisKey key, bool bit, long start = 0, long end = -1, - StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => + public long StringBitPosition( + RedisKey key, + bool bit, + long start = 0, + long end = -1, + StringIndexType indexType = StringIndexType.Byte, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long StringDecrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => + public long StringDecrement( + RedisKey key, + long value = 1, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public double StringDecrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => + public double StringDecrement( + RedisKey key, + double value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue StringGet(RedisKey key, CommandFlags flags = CommandFlags.None) => + public RedisValue StringGet( + RedisKey key, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public RedisValue[] StringGet(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => @@ -3353,13 +3387,20 @@ public double StringIncrement(RedisKey key, double value, CommandFlags flags = C public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public string? StringLongestCommonSubsequence(RedisKey first, RedisKey second, + public string? StringLongestCommonSubsequence( + RedisKey first, + RedisKey second, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long StringLongestCommonSubsequenceLength(RedisKey first, RedisKey second, + public long StringLongestCommonSubsequenceLength( + RedisKey first, + RedisKey second, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public LCSMatchResult StringLongestCommonSubsequenceWithMatches(RedisKey first, RedisKey second, long minLength = 0, + public LCSMatchResult StringLongestCommonSubsequenceWithMatches( + RedisKey first, + RedisKey second, + long minLength = 0, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -3383,11 +3424,17 @@ public bool StringSet( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public bool StringSet(KeyValuePair[] values, When when = When.Always, + public bool StringSet( + KeyValuePair[] values, + When when = When.Always, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue - StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => + public RedisValue StringSetAndGet( + RedisKey key, + RedisValue value, + TimeSpan? expiry, + When when, + CommandFlags flags) => throw new NotImplementedException(); public RedisValue StringSetAndGet( @@ -3402,6 +3449,9 @@ public RedisValue StringSetAndGet( public bool StringSetBit(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue StringSetRange(RedisKey key, long offset, RedisValue value, + public RedisValue StringSetRange( + RedisKey key, + long offset, + RedisValue value, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); } From fd0f492b8847ec7827cf2a71d934cae40062eaf9 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 5 Sep 2025 07:08:21 +0100 Subject: [PATCH 051/108] IDE nits --- src/RESPite.Benchmark/BenchmarkBase.cs | 83 +++++++------- src/RESPite.Benchmark/NewCoreBenchmark.cs | 101 +++++++++--------- src/RESPite.Benchmark/OldCoreBenchmark.cs | 86 +++++++-------- src/RESPite.Redis/RedisKeys.cs | 1 + tests/RESPite.Tests/BasicIntegrationTests.cs | 2 - tests/RESPite.Tests/BatchTests.cs | 1 - tests/RESPite.Tests/BlockBufferTests.cs | 1 - tests/RESPite.Tests/IntegrationTestBase.cs | 1 - tests/RESPite.Tests/OperationUnitTests.cs | 2 - .../RedisStringsIntegrationTests.cs | 1 - tests/RESPite.Tests/RespWriterTests.cs | 3 +- tests/RESPite.Tests/TestServer.cs | 35 +++--- 12 files changed, 157 insertions(+), 160 deletions(-) diff --git a/src/RESPite.Benchmark/BenchmarkBase.cs b/src/RESPite.Benchmark/BenchmarkBase.cs index 025fb696f..86a391a9a 100644 --- a/src/RESPite.Benchmark/BenchmarkBase.cs +++ b/src/RESPite.Benchmark/BenchmarkBase.cs @@ -13,13 +13,13 @@ namespace RESPite.Benchmark; public abstract class BenchmarkBase : IDisposable { protected const string - _getSetKey = "key:__rand_int__", - _counterKey = "counter:__rand_int__", - _listKey = "mylist", - _setKey = "myset", - _hashKey = "myhash", - _sortedSetKey = "myzset", - _streamKey = "mystream"; + GetSetKey = "key:__rand_int__", + CounterKey = "counter:__rand_int__", + ListKey = "mylist", + SetKey = "myset", + HashKey = "myhash", + SortedSetKey = "myzset", + StreamKey = "mystream"; public PipelineStrategy PipelineMode { get; } = PipelineStrategy.Batch; // the default, for parity with how redis-benchmark works @@ -32,14 +32,14 @@ public enum PipelineStrategy Batch, /// - /// Use a queue to pipeline operations - when we hit the pipeline depth, we pop one, push one, await the popped + /// Use a queue to pipeline operations - when we hit the pipeline depth, we pop one, push one, await the popped. /// Queue, } private readonly HashSet _tests = new(StringComparer.OrdinalIgnoreCase); protected bool RunTest(string name) => _tests.Count == 0 || _tests.Contains(name); - public virtual void Dispose() { } + public virtual void Dispose() => GC.SuppressFinalize(this); public int Port { get; } = 6379; public int PipelineDepth { get; } = 1; public bool Multiplexed { get; } @@ -51,9 +51,9 @@ public virtual void Dispose() { } public int TotalOperations => OperationsPerClient * ClientCount; - protected readonly byte[] _payload; + protected readonly byte[] Payload; - public BenchmarkBase(string[] args) + protected BenchmarkBase(string[] args) { int operations = 100_000; @@ -115,14 +115,14 @@ public BenchmarkBase(string[] args) OperationsPerClient = operations / ClientCount; - _payload = "abc"u8.ToArray(); + Payload = "abc"u8.ToArray(); } public abstract Task RunAll(); public async Task RunBasicLoopAsync() { - await DeleteAsync(_counterKey).ConfigureAwait(false); + await DeleteAsync(CounterKey).ConfigureAwait(false); if (ClientCount <= 1) { @@ -158,18 +158,18 @@ public async Task CleanupAsync() try { var client = GetClient(0); - await DeleteAsync(client, _getSetKey).ConfigureAwait(false); - await DeleteAsync(client, _counterKey).ConfigureAwait(false); - await DeleteAsync(client, _listKey).ConfigureAwait(false); - await DeleteAsync(client, _setKey).ConfigureAwait(false); - await DeleteAsync(client, _hashKey).ConfigureAwait(false); - await DeleteAsync(client, _sortedSetKey).ConfigureAwait(false); - await DeleteAsync(client, _streamKey).ConfigureAwait(false); + await DeleteAsync(client, GetSetKey).ConfigureAwait(false); + await DeleteAsync(client, CounterKey).ConfigureAwait(false); + await DeleteAsync(client, ListKey).ConfigureAwait(false); + await DeleteAsync(client, SetKey).ConfigureAwait(false); + await DeleteAsync(client, HashKey).ConfigureAwait(false); + await DeleteAsync(client, SortedSetKey).ConfigureAwait(false); + await DeleteAsync(client, StreamKey).ConfigureAwait(false); await OnCleanupAsync(client).ConfigureAwait(false); } catch (Exception ex) { - Console.Error.WriteLine($"Cleanup: {ex.Message}"); + await Console.Error.WriteLineAsync($"Cleanup: {ex.Message}"); } } @@ -245,7 +245,7 @@ private async Task PipelineUntyped( } catch (Exception ex) { - Console.Error.WriteLine($"{operation.Method.Name} failed after {i} operations"); + await Console.Error.WriteLineAsync($"{operation.Method.Name} failed after {i} operations"); Program.WriteException(ex); } @@ -320,7 +320,7 @@ private async Task PipelineTyped(TClient client, Func( key, action, client => action(client).AsUntypedValueTask(), - client => PipelineTyped(client, action), + client => PipelineTyped(client, action), init, format); + // ReSharper disable once UnusedMember.Global protected Task RunAsync( string? key, Func action, @@ -361,7 +362,7 @@ protected Task RunAsync( string format = "") => RunAsyncCore(key, action, action, client => PipelineUntyped(client, action), init, format); - protected async Task RunAsyncCore( + private async Task RunAsyncCore( string? key, Delegate underlyingAction, Func test, @@ -410,14 +411,7 @@ protected async Task RunAsyncCore( Console.Write(", cancel"); } - if (PipelineDepth > 1) - { - Console.Write($", {PipelineMode}: {PipelineDepth:#,##0}"); - } - else - { - Console.Write(", sequential"); - } + Console.Write(PipelineDepth > 1 ? $", {PipelineMode}: {PipelineDepth:#,##0}" : ", sequential"); Console.WriteLine(")"); } @@ -436,7 +430,7 @@ protected async Task RunAsyncCore( } catch (Exception ex) { - Console.Error.WriteLine($"\t{ex.Message}"); + await Console.Error.WriteLineAsync($"\t{ex.Message}"); didNotRun = true; return; } @@ -465,7 +459,7 @@ protected async Task RunAsyncCore( client = CreateBatch(client); } - pending[index++] = Task.Run(() => pipeline(WithCancellation(client, cancellationToken))); + pending[index++] = Task.Run(() => pipeline(WithCancellation(client, cancellationToken)), cancellationToken); } await Task.WhenAll(pending).ConfigureAwait(false); @@ -491,7 +485,7 @@ protected async Task RunAsyncCore( format = "Typical result: {0}"; } - T result = await pending[pending.Length - 1]; + T result = await pending[^1]; Console.WriteLine(format, result); } } @@ -607,13 +601,18 @@ protected async Task RunAsyncCore( static string FormatBytes(long bytes) { - const long K = 1024, M = K * K, G = M * K, T = G * K; + // ReSharper disable InconsistentNaming + const long k = 1024, M = k * k, G = M * k, T = G * k; - if (bytes < K) return $"{bytes:#,##0} B"; - if (bytes < M) return $"{bytes / (double)K:#,##0.00} KiB"; - if (bytes < G) return $"{bytes / (double)M:#,##0.00} MiB"; - if (bytes < T) return $"{bytes / (double)G:#,##0.00} GiB"; - return $"{bytes / (double)T:#,##0.00} TiB"; // I think we can stop there... + // ReSharper restore InconsistentNaming + return bytes switch + { + < k => $"{bytes:#,##0} B", + < M => $"{bytes / (double)k:#,##0.00} KiB", + < G => $"{bytes / (double)M:#,##0.00} MiB", + < T => $"{bytes / (double)G:#,##0.00} GiB", + _ => $"{bytes / (double)T:#,##0.00} TiB", + }; } } #endif diff --git a/src/RESPite.Benchmark/NewCoreBenchmark.cs b/src/RESPite.Benchmark/NewCoreBenchmark.cs index e626664b6..b443fe925 100644 --- a/src/RESPite.Benchmark/NewCoreBenchmark.cs +++ b/src/RESPite.Benchmark/NewCoreBenchmark.cs @@ -31,12 +31,12 @@ public NewCoreBenchmark(string[] args) : base(args) _clients = new RespContext[ClientCount]; _connectionPool = new(count: Multiplexed ? 1 : ClientCount); - _connectionPool.ConnectionError += (sender, args) => Program.WriteException(args.Exception, args.Operation); + _connectionPool.ConnectionError += (_, e) => Program.WriteException(e.Exception, e.Operation); _pairs = new (string, byte[])[10]; for (var i = 0; i < 10; i++) { - _pairs[i] = ($"{"key:__rand_int__"}{i}", _payload); + _pairs[i] = ($"key:__rand_int__{i}", Payload); } if (Multiplexed) @@ -86,26 +86,26 @@ public override async Task RunAll() // await RunAsync(PingInline).ConfigureAwait(false); await RunAsync(null, PingBulk).ConfigureAwait(false); - await RunAsync(_getSetKey, Set).ConfigureAwait(false); - await RunAsync(_getSetKey, Get, GetInit).ConfigureAwait(false); - await RunAsync(_counterKey, Incr).ConfigureAwait(false); - await RunAsync(_listKey, LPush).ConfigureAwait(false); - await RunAsync(_listKey, RPush).ConfigureAwait(false); - await RunAsync(_listKey, LPop, LPopInit).ConfigureAwait(false); - await RunAsync(_listKey, RPop, LPopInit).ConfigureAwait(false); - await RunAsync(_setKey, SAdd).ConfigureAwait(false); - await RunAsync(_hashKey, HSet).ConfigureAwait(false); - await RunAsync(_setKey, SPop, SPopInit).ConfigureAwait(false); - await RunAsync(_sortedSetKey, ZAdd).ConfigureAwait(false); - await RunAsync(_sortedSetKey, ZPopMin, ZPopMinInit).ConfigureAwait(false); + await RunAsync(GetSetKey, Set).ConfigureAwait(false); + await RunAsync(GetSetKey, Get, GetInit).ConfigureAwait(false); + await RunAsync(CounterKey, Incr).ConfigureAwait(false); + await RunAsync(ListKey, LPush).ConfigureAwait(false); + await RunAsync(ListKey, RPush).ConfigureAwait(false); + await RunAsync(ListKey, LPop, LPopInit).ConfigureAwait(false); + await RunAsync(ListKey, RPop, LPopInit).ConfigureAwait(false); + await RunAsync(SetKey, SAdd).ConfigureAwait(false); + await RunAsync(HashKey, HSet).ConfigureAwait(false); + await RunAsync(SetKey, SPop, SPopInit).ConfigureAwait(false); + await RunAsync(SortedSetKey, ZAdd).ConfigureAwait(false); + await RunAsync(SortedSetKey, ZPopMin, ZPopMinInit).ConfigureAwait(false); await RunAsync(null, MSet).ConfigureAwait(false); - await RunAsync(_streamKey, XAdd).ConfigureAwait(false); + await RunAsync(StreamKey, XAdd).ConfigureAwait(false); // leave until last, they're slower - await RunAsync(_listKey, LRange100, LRangeInit).ConfigureAwait(false); - await RunAsync(_listKey, LRange300, LRangeInit).ConfigureAwait(false); - await RunAsync(_listKey, LRange500, LRangeInit).ConfigureAwait(false); - await RunAsync(_listKey, LRange600, LRangeInit).ConfigureAwait(false); + await RunAsync(ListKey, LRange100, LRangeInit).ConfigureAwait(false); + await RunAsync(ListKey, LRange300, LRangeInit).ConfigureAwait(false); + await RunAsync(ListKey, LRange500, LRangeInit).ConfigureAwait(false); + await RunAsync(ListKey, LRange600, LRangeInit).ConfigureAwait(false); await CleanupAsync().ConfigureAwait(false); } @@ -131,60 +131,61 @@ protected override void PrepareBatch(RespContext client, int count) } [DisplayName("PING_INLINE")] - private ValueTask PingInline(RespContext ctx) => ctx.PingInlineAsync(_payload); + // ReSharper disable once UnusedMember.Local + private ValueTask PingInline(RespContext ctx) => ctx.PingInlineAsync(Payload); [DisplayName("PING_BULK")] - private ValueTask PingBulk(RespContext ctx) => ctx.PingAsync(_payload); + private ValueTask PingBulk(RespContext ctx) => ctx.PingAsync(Payload); [DisplayName("INCR")] - private ValueTask Incr(RespContext ctx) => ctx.IncrAsync(_counterKey); + private ValueTask Incr(RespContext ctx) => ctx.IncrAsync(CounterKey); [DisplayName("GET")] - private ValueTask Get(RespContext ctx) => ctx.GetAsync(_getSetKey); + private ValueTask Get(RespContext ctx) => ctx.GetAsync(GetSetKey); - private ValueTask GetInit(RespContext ctx) => ctx.SetAsync(_getSetKey, _payload).AsUntypedValueTask(); + private ValueTask GetInit(RespContext ctx) => ctx.SetAsync(GetSetKey, Payload).AsUntypedValueTask(); [DisplayName("SET")] - private ValueTask Set(RespContext ctx) => ctx.SetAsync(_getSetKey, _payload); + private ValueTask Set(RespContext ctx) => ctx.SetAsync(GetSetKey, Payload); [DisplayName("LPUSH")] - private ValueTask LPush(RespContext ctx) => ctx.LPushAsync(_listKey, _payload); + private ValueTask LPush(RespContext ctx) => ctx.LPushAsync(ListKey, Payload); [DisplayName("RPUSH")] - private ValueTask RPush(RespContext ctx) => ctx.RPushAsync(_listKey, _payload); + private ValueTask RPush(RespContext ctx) => ctx.RPushAsync(ListKey, Payload); [DisplayName("LRANGE_100")] - private ValueTask LRange100(RespContext ctx) => ctx.LRangeAsync(_listKey, 0, 99); + private ValueTask LRange100(RespContext ctx) => ctx.LRangeAsync(ListKey, 0, 99); [DisplayName("LRANGE_300")] - private ValueTask LRange300(RespContext ctx) => ctx.LRangeAsync(_listKey, 0, 299); + private ValueTask LRange300(RespContext ctx) => ctx.LRangeAsync(ListKey, 0, 299); [DisplayName("LRANGE_500")] - private ValueTask LRange500(RespContext ctx) => ctx.LRangeAsync(_listKey, 0, 499); + private ValueTask LRange500(RespContext ctx) => ctx.LRangeAsync(ListKey, 0, 499); [DisplayName("LRANGE_600")] - private ValueTask LRange600(RespContext ctx) => ctx.LRangeAsync(_listKey, 0, 599); + private ValueTask LRange600(RespContext ctx) => ctx.LRangeAsync(ListKey, 0, 599); [DisplayName("LPOP")] - private ValueTask LPop(RespContext ctx) => ctx.LPopAsync(_listKey); + private ValueTask LPop(RespContext ctx) => ctx.LPopAsync(ListKey); [DisplayName("RPOP")] - private ValueTask RPop(RespContext ctx) => ctx.RPopAsync(_listKey); + private ValueTask RPop(RespContext ctx) => ctx.RPopAsync(ListKey); private ValueTask LPopInit(RespContext ctx) => - ctx.LPushAsync(_listKey, _payload, TotalOperations).AsUntypedValueTask(); + ctx.LPushAsync(ListKey, Payload, TotalOperations).AsUntypedValueTask(); [DisplayName("SADD")] - private ValueTask SAdd(RespContext ctx) => ctx.SAddAsync(_setKey, "element:__rand_int__"); + private ValueTask SAdd(RespContext ctx) => ctx.SAddAsync(SetKey, "element:__rand_int__"); [DisplayName("HSET")] - private ValueTask HSet(RespContext ctx) => ctx.HSetAsync(_hashKey, "element:__rand_int__", _payload); + private ValueTask HSet(RespContext ctx) => ctx.HSetAsync(HashKey, "element:__rand_int__", Payload); [DisplayName("ZADD")] - private ValueTask ZAdd(RespContext ctx) => ctx.ZAddAsync(_sortedSetKey, 0, "element:__rand_int__"); + private ValueTask ZAdd(RespContext ctx) => ctx.ZAddAsync(SortedSetKey, 0, "element:__rand_int__"); [DisplayName("ZPOPMIN")] - private ValueTask ZPopMin(RespContext ctx) => ctx.ZPopMinAsync(_sortedSetKey); + private ValueTask ZPopMin(RespContext ctx) => ctx.ZPopMinAsync(SortedSetKey); private async ValueTask ZPopMinInit(RespContext ctx) { @@ -192,20 +193,20 @@ private async ValueTask ZPopMinInit(RespContext ctx) var rand = new Random(); for (int i = 0; i < ops; i++) { - await ctx.ZAddAsync(_sortedSetKey, (rand.NextDouble() * 2000) - 1000, "element:__rand_int__") + await ctx.ZAddAsync(SortedSetKey, (rand.NextDouble() * 2000) - 1000, "element:__rand_int__") .ConfigureAwait(false); } } [DisplayName("SPOP")] - private ValueTask SPop(RespContext ctx) => ctx.SPopAsync(_setKey); + private ValueTask SPop(RespContext ctx) => ctx.SPopAsync(SetKey); private async ValueTask SPopInit(RespContext ctx) { int ops = TotalOperations; for (int i = 0; i < ops; i++) { - await ctx.SAddAsync(_setKey, "element:__rand_int__").ConfigureAwait(false); + await ctx.SAddAsync(SetKey, "element:__rand_int__").ConfigureAwait(false); } } @@ -213,11 +214,11 @@ private async ValueTask SPopInit(RespContext ctx) private ValueTask MSet(RespContext ctx) => ctx.MSetAsync(_pairs); private ValueTask LRangeInit(RespContext ctx) => - ctx.LPushAsync(_listKey, _payload, TotalOperations).AsUntypedValueTask(); + ctx.LPushAsync(ListKey, Payload, TotalOperations).AsUntypedValueTask(); [DisplayName("XADD")] private ValueTask XAdd(RespContext ctx) => - ctx.XAddAsync(_streamKey, "*", "myfield", _payload); + ctx.XAddAsync(StreamKey, "*", "myfield", Payload); protected override async Task RunBasicLoopAsync(int clientId) { @@ -226,7 +227,7 @@ protected override async Task RunBasicLoopAsync(int clientId) var client = GetClient(clientId); var depth = PipelineDepth; int tickCount = 0; // this is just so we don't query DateTime. - long previousValue = (await client.GetInt32Async(_counterKey).ConfigureAwait(false)) ?? 0, + long previousValue = (await client.GetInt32Async(CounterKey).ConfigureAwait(false)) ?? 0, currentValue = previousValue; var watch = Stopwatch.StartNew(); long previousMillis = watch.ElapsedMilliseconds; @@ -271,7 +272,7 @@ bool Tick() { while (true) { - currentValue = await client.IncrAsync(_counterKey).ConfigureAwait(false); + currentValue = await client.IncrAsync(CounterKey).ConfigureAwait(false); if (++tickCount >= 1000 && Tick()) break; // only check whether to output every N iterations } @@ -285,12 +286,12 @@ bool Tick() { for (int i = 0; i < depth; i++) { - pending[i] = ctx.IncrAsync(_counterKey); + pending[i] = ctx.IncrAsync(CounterKey); } await batch.FlushAsync().ConfigureAwait(false); batch.EnsureCapacity(depth); // batches don't assume re-use - for (int i = 0; i < depth; i++) + for (var i = 0; i < depth; i++) { currentValue = await pending[i].ConfigureAwait(false); } @@ -319,11 +320,12 @@ internal static partial class RedisCommands [RespCommand] internal static partial int LPush(this in RespContext ctx, string key, byte[] payload); - [RespCommand(Formatter = "LPushFormatter.Instance")] + [RespCommand(Formatter = LPushFormatter.Name)] internal static partial int LPush(this in RespContext ctx, string key, byte[] payload, int count); private sealed class LPushFormatter : IRespFormatter<(string Key, byte[] Payload, int Count)> { + public const string Name = $"{nameof(LPushFormatter)}.{nameof(Instance)}"; private LPushFormatter() { } public static readonly LPushFormatter Instance = new(); @@ -387,7 +389,7 @@ internal static partial RespParsers.ResponseSummary XAdd( [RespCommand] internal static partial RespParsers.ResponseSummary Get(this in RespContext ctx, string key); - [RespCommand(Formatter = "PairsFormatter.Instance")] // custom command formatter + [RespCommand(Formatter = PairsFormatter.Name)] // custom command formatter internal static partial bool MSet(this in RespContext ctx, (string, byte[])[] pairs); internal static RespParsers.ResponseSummary PingInline(this in RespContext ctx, byte[] payload) @@ -413,6 +415,7 @@ public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in private sealed class PairsFormatter : IRespFormatter<(string Key, byte[] Value)[]> { + public const string Name = $"{nameof(PairsFormatter)}.{nameof(Instance)}"; public static readonly PairsFormatter Instance = new PairsFormatter(); public void Format( diff --git a/src/RESPite.Benchmark/OldCoreBenchmark.cs b/src/RESPite.Benchmark/OldCoreBenchmark.cs index 90fd86540..0bf650b88 100644 --- a/src/RESPite.Benchmark/OldCoreBenchmark.cs +++ b/src/RESPite.Benchmark/OldCoreBenchmark.cs @@ -23,7 +23,7 @@ public OldCoreBenchmark(string[] args) : base(args) for (var i = 0; i < 10; i++) { - _pairs[i] = new($"{"key:__rand_int__"}{i}", _payload); + _pairs[i] = new($"{"key:__rand_int__"}{i}", Payload); } } @@ -51,26 +51,26 @@ public override async Task RunAll() // await RunAsync(PingInline).ConfigureAwait(false); await RunAsync(null, PingBulk).ConfigureAwait(false); - await RunAsync(_getSetKey, Set).ConfigureAwait(false); - await RunAsync(_getSetKey, Get, GetInit).ConfigureAwait(false); - await RunAsync(_counterKey, Incr).ConfigureAwait(false); - await RunAsync(_listKey, LPush).ConfigureAwait(false); - await RunAsync(_listKey, RPush).ConfigureAwait(false); - await RunAsync(_listKey, LPop, LPopInit).ConfigureAwait(false); - await RunAsync(_listKey, RPop, LPopInit).ConfigureAwait(false); - await RunAsync(_setKey, SAdd).ConfigureAwait(false); - await RunAsync(_hashKey, HSet).ConfigureAwait(false); - await RunAsync(_setKey, SPop, SPopInit).ConfigureAwait(false); - await RunAsync(_sortedSetKey, ZAdd).ConfigureAwait(false); - await RunAsync(_sortedSetKey, ZPopMin, ZPopMinInit).ConfigureAwait(false); + await RunAsync(GetSetKey, Set).ConfigureAwait(false); + await RunAsync(GetSetKey, Get, GetInit).ConfigureAwait(false); + await RunAsync(CounterKey, Incr).ConfigureAwait(false); + await RunAsync(ListKey, LPush).ConfigureAwait(false); + await RunAsync(ListKey, RPush).ConfigureAwait(false); + await RunAsync(ListKey, LPop, LPopInit).ConfigureAwait(false); + await RunAsync(ListKey, RPop, LPopInit).ConfigureAwait(false); + await RunAsync(SetKey, SAdd).ConfigureAwait(false); + await RunAsync(HashKey, HSet).ConfigureAwait(false); + await RunAsync(SetKey, SPop, SPopInit).ConfigureAwait(false); + await RunAsync(SortedSetKey, ZAdd).ConfigureAwait(false); + await RunAsync(SortedSetKey, ZPopMin, ZPopMinInit).ConfigureAwait(false); await RunAsync(null, MSet).ConfigureAwait(false); - await RunAsync(_streamKey, XAdd).ConfigureAwait(false); + await RunAsync(StreamKey, XAdd).ConfigureAwait(false); // leave until last, they're slower - await RunAsync(_listKey, LRange100, LRangeInit).ConfigureAwait(false); - await RunAsync(_listKey, LRange300, LRangeInit).ConfigureAwait(false); - await RunAsync(_listKey, LRange500, LRangeInit).ConfigureAwait(false); - await RunAsync(_listKey, LRange600, LRangeInit).ConfigureAwait(false); + await RunAsync(ListKey, LRange100, LRangeInit).ConfigureAwait(false); + await RunAsync(ListKey, LRange300, LRangeInit).ConfigureAwait(false); + await RunAsync(ListKey, LRange500, LRangeInit).ConfigureAwait(false); + await RunAsync(ListKey, LRange600, LRangeInit).ConfigureAwait(false); await CleanupAsync().ConfigureAwait(false); } @@ -94,7 +94,7 @@ protected override async Task RunBasicLoopAsync(int clientId) var client = (IDatabase)GetClient(clientId); // need IDatabase for CreateBatch var depth = PipelineDepth; int tickCount = 0; // this is just so we don't query DateTime. - var tmp = await client.StringGetAsync(_counterKey).ConfigureAwait(false); + var tmp = await client.StringGetAsync(CounterKey).ConfigureAwait(false); long previousValue = tmp.IsNull ? 0 : (long)tmp, currentValue = previousValue; var watch = Stopwatch.StartNew(); long previousMillis = watch.ElapsedMilliseconds; @@ -139,7 +139,7 @@ bool Tick() { while (true) { - currentValue = await client.StringIncrementAsync(_counterKey).ConfigureAwait(false); + currentValue = await client.StringIncrementAsync(CounterKey).ConfigureAwait(false); if (++tickCount >= 1000 && Tick()) break; // only check whether to output every N iterations } @@ -152,7 +152,7 @@ bool Tick() { for (int i = 0; i < depth; i++) { - pending[i] = batch.StringIncrementAsync(_counterKey); + pending[i] = batch.StringIncrementAsync(CounterKey); } batch.Execute(); @@ -172,15 +172,15 @@ bool Tick() private async ValueTask GetAndMeasureString(IDatabaseAsync client) { - using var lease = await client.StringGetLeaseAsync(_getSetKey).ConfigureAwait(false); + using var lease = await client.StringGetLeaseAsync(GetSetKey).ConfigureAwait(false); return lease?.Length ?? -1; } [DisplayName("SET")] - private ValueTask Set(IDatabaseAsync client) => client.StringSetAsync(_getSetKey, _payload).AsValueTask(); + private ValueTask Set(IDatabaseAsync client) => client.StringSetAsync(GetSetKey, Payload).AsValueTask(); private ValueTask GetInit(IDatabaseAsync client) => - client.StringSetAsync(_getSetKey, _payload).AsUntypedValueTask(); + client.StringSetAsync(GetSetKey, Payload).AsUntypedValueTask(); private ValueTask PingInline(IDatabaseAsync client) => client.PingAsync().AsValueTask(); @@ -188,43 +188,43 @@ private ValueTask GetInit(IDatabaseAsync client) => private ValueTask PingBulk(IDatabaseAsync client) => client.PingAsync().AsValueTask(); [DisplayName("INCR")] - private ValueTask Incr(IDatabaseAsync client) => client.StringIncrementAsync(_counterKey).AsValueTask(); + private ValueTask Incr(IDatabaseAsync client) => client.StringIncrementAsync(CounterKey).AsValueTask(); [DisplayName("HSET")] private ValueTask HSet(IDatabaseAsync client) => - client.HashSetAsync(_hashKey, "element:__rand_int__", _payload).AsValueTask(); + client.HashSetAsync(HashKey, "element:__rand_int__", Payload).AsValueTask(); [DisplayName("SADD")] private ValueTask SAdd(IDatabaseAsync client) => - client.SetAddAsync(_setKey, "element:__rand_int__").AsValueTask(); + client.SetAddAsync(SetKey, "element:__rand_int__").AsValueTask(); [DisplayName("LPUSH")] - private ValueTask LPush(IDatabaseAsync client) => client.ListLeftPushAsync(_listKey, _payload).AsValueTask(); + private ValueTask LPush(IDatabaseAsync client) => client.ListLeftPushAsync(ListKey, Payload).AsValueTask(); [DisplayName("RPUSH")] - private ValueTask RPush(IDatabaseAsync client) => client.ListRightPushAsync(_listKey, _payload).AsValueTask(); + private ValueTask RPush(IDatabaseAsync client) => client.ListRightPushAsync(ListKey, Payload).AsValueTask(); [DisplayName("LPOP")] - private ValueTask LPop(IDatabaseAsync client) => client.ListLeftPopAsync(_listKey).AsValueTask(); + private ValueTask LPop(IDatabaseAsync client) => client.ListLeftPopAsync(ListKey).AsValueTask(); [DisplayName("RPOP")] - private ValueTask RPop(IDatabaseAsync client) => client.ListRightPopAsync(_listKey).AsValueTask(); + private ValueTask RPop(IDatabaseAsync client) => client.ListRightPopAsync(ListKey).AsValueTask(); private ValueTask LPopInit(IDatabaseAsync client) => - client.ListLeftPushAsync(_listKey, _payload).AsUntypedValueTask(); + client.ListLeftPushAsync(ListKey, Payload).AsUntypedValueTask(); [DisplayName("SPOP")] - private ValueTask SPop(IDatabaseAsync client) => client.SetPopAsync(_setKey).AsValueTask(); + private ValueTask SPop(IDatabaseAsync client) => client.SetPopAsync(SetKey).AsValueTask(); private ValueTask SPopInit(IDatabaseAsync client) => - client.SetAddAsync(_setKey, "element:__rand_int__").AsUntypedValueTask(); + client.SetAddAsync(SetKey, "element:__rand_int__").AsUntypedValueTask(); [DisplayName("ZADD")] private ValueTask ZAdd(IDatabaseAsync client) => - client.SortedSetAddAsync(_sortedSetKey, "element:__rand_int__", 0).AsValueTask(); + client.SortedSetAddAsync(SortedSetKey, "element:__rand_int__", 0).AsValueTask(); [DisplayName("ZPOPMIN")] - private ValueTask ZPopMin(IDatabaseAsync client) => CountAsync(client.SortedSetPopAsync(_sortedSetKey, 1)); + private ValueTask ZPopMin(IDatabaseAsync client) => CountAsync(client.SortedSetPopAsync(SortedSetKey, 1)); private async ValueTask ZPopMinInit(IDatabaseAsync client) { @@ -232,7 +232,7 @@ private async ValueTask ZPopMinInit(IDatabaseAsync client) var rand = new Random(); for (int i = 0; i < ops; i++) { - await client.SortedSetAddAsync(_sortedSetKey, "element:__rand_int__", (rand.NextDouble() * 2000) - 1000) + await client.SortedSetAddAsync(SortedSetKey, "element:__rand_int__", (rand.NextDouble() * 2000) - 1000) .ConfigureAwait(false); } } @@ -242,20 +242,20 @@ await client.SortedSetAddAsync(_sortedSetKey, "element:__rand_int__", (rand.Next [DisplayName("XADD")] private ValueTask XAdd(IDatabaseAsync client) => - client.StreamAddAsync(_streamKey, "myfield", _payload).AsValueTask(); + client.StreamAddAsync(StreamKey, "myfield", Payload).AsValueTask(); [DisplayName("LRANGE_100")] - private ValueTask LRange100(IDatabaseAsync client) => CountAsync(client.ListRangeAsync(_listKey, 0, 99)); + private ValueTask LRange100(IDatabaseAsync client) => CountAsync(client.ListRangeAsync(ListKey, 0, 99)); [DisplayName("LRANGE_300")] - private ValueTask LRange300(IDatabaseAsync client) => CountAsync(client.ListRangeAsync(_listKey, 0, 299)); + private ValueTask LRange300(IDatabaseAsync client) => CountAsync(client.ListRangeAsync(ListKey, 0, 299)); [DisplayName("LRANGE_500")] - private ValueTask LRange500(IDatabaseAsync client) => CountAsync(client.ListRangeAsync(_listKey, 0, 499)); + private ValueTask LRange500(IDatabaseAsync client) => CountAsync(client.ListRangeAsync(ListKey, 0, 499)); [DisplayName("LRANGE_600")] private ValueTask LRange600(IDatabaseAsync client) => - CountAsync(client.ListRangeAsync(_listKey, 0, 599)); + CountAsync(client.ListRangeAsync(ListKey, 0, 599)); private static ValueTask CountAsync(Task task) => task.ContinueWith( t => t.Result.Length, TaskContinuationOptions.ExecuteSynchronously).AsValueTask(); @@ -265,7 +265,7 @@ private async ValueTask LRangeInit(IDatabaseAsync client) var ops = TotalOperations; for (int i = 0; i < ops; i++) { - await client.ListLeftPushAsync(_listKey, _payload); + await client.ListLeftPushAsync(ListKey, Payload); } } } diff --git a/src/RESPite.Redis/RedisKeys.cs b/src/RESPite.Redis/RedisKeys.cs index db956425e..1e9d4531e 100644 --- a/src/RESPite.Redis/RedisKeys.cs +++ b/src/RESPite.Redis/RedisKeys.cs @@ -5,6 +5,7 @@ namespace RESPite.Redis; // note that members may also be added as extensions if necessary public readonly partial struct RedisKeys(in RespContext context) { + // ReSharper disable once UnusedMember.Local private readonly RespContext _context = context; [RespCommand] diff --git a/tests/RESPite.Tests/BasicIntegrationTests.cs b/tests/RESPite.Tests/BasicIntegrationTests.cs index 5b3db34e2..312699b54 100644 --- a/tests/RESPite.Tests/BasicIntegrationTests.cs +++ b/tests/RESPite.Tests/BasicIntegrationTests.cs @@ -1,10 +1,8 @@ using System; using System.Threading; using System.Threading.Tasks; -using RESPite; using RESPite.Connections; using RESPite.Messages; -using RESPite.Redis; using RESPite.Redis.Alt; // needed for AsStrings() etc using Xunit; diff --git a/tests/RESPite.Tests/BatchTests.cs b/tests/RESPite.Tests/BatchTests.cs index 0f7d9ec3d..17e5f515f 100644 --- a/tests/RESPite.Tests/BatchTests.cs +++ b/tests/RESPite.Tests/BatchTests.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using RESPite; using Xunit; namespace RESPite.Tests; diff --git a/tests/RESPite.Tests/BlockBufferTests.cs b/tests/RESPite.Tests/BlockBufferTests.cs index cac8deafd..923a4dc15 100644 --- a/tests/RESPite.Tests/BlockBufferTests.cs +++ b/tests/RESPite.Tests/BlockBufferTests.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; -using RESPite; using RESPite.Internal; using Xunit; diff --git a/tests/RESPite.Tests/IntegrationTestBase.cs b/tests/RESPite.Tests/IntegrationTestBase.cs index 117f447b2..22e2b1fb7 100644 --- a/tests/RESPite.Tests/IntegrationTestBase.cs +++ b/tests/RESPite.Tests/IntegrationTestBase.cs @@ -1,5 +1,4 @@ using System.Runtime.CompilerServices; -using RESPite; using RESPite.Redis.Alt; using Xunit; diff --git a/tests/RESPite.Tests/OperationUnitTests.cs b/tests/RESPite.Tests/OperationUnitTests.cs index 45068a8bb..9160c92ac 100644 --- a/tests/RESPite.Tests/OperationUnitTests.cs +++ b/tests/RESPite.Tests/OperationUnitTests.cs @@ -4,9 +4,7 @@ using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Sources; -using RESPite; using Xunit; -using Xunit.Internal; namespace RESPite.Tests; diff --git a/tests/RESPite.Tests/RedisStringsIntegrationTests.cs b/tests/RESPite.Tests/RedisStringsIntegrationTests.cs index fb0594ac1..e4ad8b8d9 100644 --- a/tests/RESPite.Tests/RedisStringsIntegrationTests.cs +++ b/tests/RESPite.Tests/RedisStringsIntegrationTests.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using RESPite.Redis; using RESPite.Redis.Alt; // needed for AsStrings() etc using Xunit; using FactAttribute = StackExchange.Redis.Tests.FactAttribute; diff --git a/tests/RESPite.Tests/RespWriterTests.cs b/tests/RESPite.Tests/RespWriterTests.cs index 9a114dede..6462ee991 100644 --- a/tests/RESPite.Tests/RespWriterTests.cs +++ b/tests/RESPite.Tests/RespWriterTests.cs @@ -1,5 +1,4 @@ -using System.Buffers; -using RESPite.Messages; +using RESPite.Messages; using Xunit; namespace RESPite.Tests; diff --git a/tests/RESPite.Tests/TestServer.cs b/tests/RESPite.Tests/TestServer.cs index c19fcfb45..5b3a3f1ce 100644 --- a/tests/RESPite.Tests/TestServer.cs +++ b/tests/RESPite.Tests/TestServer.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using RESPite; +using RESPite.Connections.Internal; using RESPite.Internal; using Xunit; @@ -25,7 +25,9 @@ public TestServer(RespConfiguration? configuration = null) public void Dispose() { + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract _stream?.Dispose(); + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract Connection?.Dispose(); } @@ -98,7 +100,7 @@ public static ValueTask Execute( ReadOnlySpan request, ReadOnlySpan response, T expected) - => AwaitAndValidate(Execute(operation, request, response), expected); + => AwaitAndValidate(Execute(operation, request, response), expected); // intended for use with [InlineData("...")] scenarios public static ValueTask Execute( @@ -106,7 +108,7 @@ public static ValueTask Execute( string request, string response, T expected) - => AwaitAndValidate(Execute(operation, request, response), expected); + => AwaitAndValidate(Execute(operation, request, response), expected); public static ValueTask Execute( Func operation, @@ -182,9 +184,9 @@ private sealed class TestRespServerStream : Stream public override void Close() { _closed = true; - lock (InboundLock) + lock (inboundLock) { - Monitor.PulseAll(InboundLock); + Monitor.PulseAll(inboundLock); } } @@ -193,9 +195,9 @@ protected override void Dispose(bool disposing) _disposed = true; if (disposing) { - lock (InboundLock) + lock (inboundLock) { - Monitor.PulseAll(InboundLock); + Monitor.PulseAll(inboundLock); } } } @@ -223,26 +225,26 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel public void Respond(ReadOnlySpan serverToClient) { - lock (InboundLock) + lock (inboundLock) { if (!(_disposed | _disposed)) { _inbound.Write(serverToClient); } - Monitor.PulseAll(InboundLock); + Monitor.PulseAll(inboundLock); } } private int ReadCore(Span destination) { ThrowIfDisposed(); - lock (InboundLock) + lock (inboundLock) { while (_inbound.CommittedIsEmpty) { if (_closed) return 0; - Monitor.Wait(InboundLock); + Monitor.Wait(inboundLock); ThrowIfDisposed(); } @@ -265,14 +267,15 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken } #endif - private readonly object OutboundLock = new object(), InboundLock = new object(); + // ReSharper disable once ChangeFieldTypeToSystemThreadingLock - TFM dependent + private readonly object outboundLock = new(), inboundLock = new(); private CycleBuffer _outbound = CycleBuffer.Create(MemoryPool.Shared), _inbound = CycleBuffer.Create(MemoryPool.Shared); private void WriteCore(ReadOnlySpan source) { - lock (OutboundLock) + lock (outboundLock) { _outbound.Write(source); } @@ -290,7 +293,7 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati #if NET public override void Write(ReadOnlySpan buffer) => WriteCore(buffer); - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); WriteCore(buffer.Span); @@ -302,7 +305,7 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo public void AssertAllSent() { bool empty; - lock (OutboundLock) + lock (outboundLock) { empty = _outbound.CommittedIsEmpty; } @@ -315,7 +318,7 @@ public void AssertAllSent() /// public void AssertSent(ReadOnlySpan clientToServer) { - lock (OutboundLock) + lock (outboundLock) { var available = _outbound.GetCommittedLength(); Assert.True( From 3ac8b824d7f943ed2cc15b271ccdd527f0148d7a Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 5 Sep 2025 07:31:43 +0100 Subject: [PATCH 052/108] clean build --- src/Directory.Build.props | 4 ++++ src/RESPite/RESPite.csproj | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 8e5481092..3d2acbaaf 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -14,4 +14,8 @@ + + + $(MSBuildWarningsAsMessages);MSB3277 + diff --git a/src/RESPite/RESPite.csproj b/src/RESPite/RESPite.csproj index 24fd9e210..ed46defc7 100644 --- a/src/RESPite/RESPite.csproj +++ b/src/RESPite/RESPite.csproj @@ -5,22 +5,22 @@ net461;netstandard2.0;net472;net6.0;net8.0;net9.0 enable enable - $(NoWarn);CS1591 + false 2025 - $([System.DateTime]::Now.Year) Marc Gravell readme.md - + - - + + - + From 4d252408fba6058b0d60bfaa94671fdf7f9fd44d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 5 Sep 2025 08:41:36 +0100 Subject: [PATCH 053/108] eng improvements --- .../RespCommandGenerator.cs | 125 ++++++++++++------ .../StackExchange.Redis.Build.csproj | 4 +- 2 files changed, 87 insertions(+), 42 deletions(-) diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index 261d91659..e6c40316c 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -1,5 +1,4 @@ -using System.Buffers; -using System.Collections.Immutable; +using System.Collections.Immutable; using System.Diagnostics; using System.Reflection; using System.Text; @@ -43,6 +42,38 @@ private bool Predicate(SyntaxNode node, CancellationToken cancellationToken) private static string GetFullName(ITypeSymbol type) => type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + private enum RESPite + { + RespContext, + RespCommandAttribute, + RespKeyAttribute, + } + + private static bool IsRESPite(ITypeSymbol? symbol, RESPite type) + { + static string NameOf(RESPite type) => type switch + { + RESPite.RespContext => nameof(RESPite.RespContext), + RESPite.RespCommandAttribute => nameof(RESPite.RespCommandAttribute), + RESPite.RespKeyAttribute => nameof(RESPite.RespKeyAttribute), + _ => type.ToString(), + }; + + if (symbol is INamedTypeSymbol named && named.Name == NameOf(type)) + { + // looking likely; check namespace + if (named.ContainingNamespace is { Name: "RESPite", ContainingNamespace.IsGlobalNamespace: true }) + { + return true; + } + + // if the type doesn't resolve: we're going to need to trust it + if (named.TypeKind == TypeKind.Error) return true; + } + + return false; + } + private static string GetName(ITypeSymbol type) { if (type.ContainingType is null) return type.Name; @@ -63,18 +94,31 @@ private static string GetName(ITypeSymbol type) return sb.ToString(); } + [Conditional("DEBUG")] + private static void AddNotes(ref string notes, string note) + { + if (string.IsNullOrWhiteSpace(notes)) + { + notes = note; + } + else + { + notes += "; " + note; + } + } + private (string Namespace, string TypeName, string ReturnType, string MethodName, string Command, ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> Parameters, string TypeModifiers, string - MethodModifiers, string Context, string? Formatter, string? Parser) Transform( + MethodModifiers, string Context, string? Formatter, string? Parser, string DebugNotes) Transform( GeneratorSyntaxContext ctx, CancellationToken cancellationToken) { // extract the name and value (defaults to name, but can be overridden via attribute) and the location if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not IMethodSymbol method) return default; - if (!(method.IsPartialDefinition && method.PartialImplementationPart is null)) return default; + if (!(method is { IsPartialDefinition: true, PartialImplementationPart: null })) return default; - string returnType; + string returnType, debugNote = ""; if (method.ReturnsVoid) { returnType = ""; @@ -104,11 +148,7 @@ private static string GetName(ITypeSymbol type) string? formatter = null, parser = null; foreach (var attrib in method.GetAttributes()) { - if (attrib.AttributeClass is - { - Name: "RespCommandAttribute", - ContainingNamespace: { Name: "RESPite", ContainingNamespace.IsGlobalNamespace: true } - }) + if (IsRESPite(attrib.AttributeClass, RESPite.RespCommandAttribute)) { if (attrib.ConstructorArguments.Length == 1) { @@ -143,7 +183,7 @@ private static string GetName(ITypeSymbol type) foreach (var param in method.Parameters) { - if (IsRespContext(param.Type)) + if (IsRESPite(param.Type, RESPite.RespContext)) { context = param.Name; break; @@ -152,12 +192,17 @@ private static string GetName(ITypeSymbol type) if (context is null) { + AddNotes(ref debugNote, $"checking {method.ContainingType.Name} for fields"); foreach (var member in method.ContainingType.GetMembers()) { - if (member is IFieldSymbol { IsStatic: false } field && IsRespContext(field.Type)) + if (member is IFieldSymbol { IsStatic: false } field) { - context = field.Name; - break; + if (IsRESPite(field.Type, RESPite.RespContext)) + { + AddNotes(ref debugNote, $"{field.Name} WAS match - {field.Type.Name}"); + context = field.Name; + break; + } } } } @@ -171,7 +216,7 @@ private static string GetName(ITypeSymbol type) if (ctor.IsStatic) continue; foreach (var param in ctor.Parameters) { - if (IsRespContext(param.Type)) + if (IsRESPite(param.Type, RESPite.RespContext)) { context = param.Name; break; @@ -194,12 +239,14 @@ private static string GetName(ITypeSymbol type) } } } + if (context is null) { // look for indirect from field foreach (var member in method.ContainingType.GetMembers()) { - if (member is IFieldSymbol { IsStatic: false } field && IsIndirectRespContext(field.Type, out var memberName)) + if (member is IFieldSymbol { IsStatic: false } field && + IsIndirectRespContext(field.Type, out var memberName)) { context = $"{field.Name}.{memberName}"; break; @@ -215,21 +262,23 @@ static bool IsIndirectRespContext(ITypeSymbol type, out string memberName) foreach (var member in type.GetMembers()) { if (member is IFieldSymbol { IsStatic: false } field - && IsRespContext(field.Type)) + && IsRESPite(field.Type, RESPite.RespContext)) { memberName = field.Name; return true; } } + foreach (var member in type.GetMembers()) { if (member is IPropertySymbol { IsStatic: false } prop - && IsRespContext(prop.Type) && prop.GetMethod is not null) + && IsRESPite(prop.Type, RESPite.RespContext) && prop.GetMethod is not null) { memberName = prop.Name; return true; } } + memberName = ""; return false; } @@ -240,7 +289,7 @@ static bool IsIndirectRespContext(ITypeSymbol type, out string memberName) foreach (var member in method.ContainingType.GetMembers()) { if (member is IPropertySymbol { IsStatic: false } prop - && IsRespContext(prop.Type) && prop.GetMethod is not null) + && IsRESPite(prop.Type, RESPite.RespContext) && prop.GetMethod is not null) { context = prop.Name; break; @@ -248,7 +297,7 @@ static bool IsIndirectRespContext(ITypeSymbol type, out string memberName) } } - static bool Ignore(ITypeSymbol symbol) => IsRespContext(symbol); // CT etc? + static bool Ignore(ITypeSymbol symbol) => IsRESPite(symbol, RESPite.RespContext); // CT etc? foreach (var param in method.Parameters) { @@ -276,16 +325,10 @@ static bool IsIndirectRespContext(ITypeSymbol type, out string memberName) parameters.Add((GetFullName(param.Type), param.Name, modifiers, flags)); } - static bool IsRespContext(ITypeSymbol type) - => type is INamedTypeSymbol - { - Name: "RespContext", - ContainingNamespace: { Name: "RESPite", ContainingNamespace.IsGlobalNamespace: true } - }; - var syntax = (MethodDeclarationSyntax)ctx.Node; return (ns, parentType, returnType, method.Name, value, parameters.ToImmutable(), - TypeModifiers(method.ContainingType), syntax.Modifiers.ToString(), context ?? "", formatter, parser); + TypeModifiers(method.ContainingType), syntax.Modifiers.ToString(), context ?? "", formatter, parser, + debugNote); static string TypeModifiers(ITypeSymbol type) { @@ -317,15 +360,9 @@ private bool IsKey(IParameterSymbol param) foreach (var attrib in param.GetAttributes()) { - if (attrib.AttributeClass is - { - Name: "KeyAttribute", - ContainingNamespace: { Name: "RESPite", ContainingNamespace.IsGlobalNamespace: true } - }) - { - return true; - } + if (IsRESPite(attrib.AttributeClass, RESPite.RespKeyAttribute)) return true; } + return false; } @@ -347,7 +384,7 @@ private void Generate( ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> Parameters, string TypeModifiers, string - MethodModifiers, string Context, string? Formatter, string? Parser)> methods) + MethodModifiers, string Context, string? Formatter, string? Parser, string DebugNotes)> methods) { if (methods.IsDefaultOrEmpty) return; @@ -431,6 +468,12 @@ private void Generate( foreach (var method in grp) { + if (method.DebugNotes is { Length: > 0 }) + { + NewLine().Append("/* ").Append(method.MethodName).Append(": ") + .Append(method.DebugNotes).Append(" */"); + } + bool isSharedFormatter = false; string? formatter = method.Formatter ?? InbuiltFormatter(method.Parameters); @@ -526,6 +569,7 @@ void WriteMethod(bool asAsync) { sb.Append(", ").Append(method.ReturnType); } + sb.Append(">(").Append(csValue).Append("u8").Append(", "); WriteTuple(method.Parameters, sb, TupleMode.Values); sb.Append(", ").Append(formatter).Append(", ").Append(parser).Append(")"); @@ -563,7 +607,8 @@ void WriteMethod(bool asAsync) var names = tuple.Value.Shared ? TupleMode.SyntheticNames : TupleMode.NamedTuple; NewLine(); - sb = NewLine().Append("sealed file class ").Append(name).Append(" : global::RESPite.Messages.IRespFormatter<"); + sb = NewLine().Append("sealed file class ").Append(name) + .Append(" : global::RESPite.Messages.IRespFormatter<"); WriteTuple(parameters, sb, names); sb.Append('>'); NewLine().Append("{"); @@ -572,7 +617,8 @@ void WriteMethod(bool asAsync) NewLine(); sb = NewLine() - .Append("public void Format(scoped ReadOnlySpan command, ref global::RESPite.Messages.RespWriter writer, in "); + .Append( + "public void Format(scoped ReadOnlySpan command, ref global::RESPite.Messages.RespWriter writer, in "); WriteTuple(parameters, sb, names); sb.Append(" request)"); NewLine().Append("{"); @@ -771,6 +817,7 @@ private static string RemovePartial(string modifiers) [Flags] private enum ParameterFlags { + // ReSharper disable once UnusedMember.Local None = 0, Parameter = 1 << 0, Data = 1 << 1, diff --git a/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj index 09b8b16da..b27dfeffd 100644 --- a/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj +++ b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj @@ -1,9 +1,7 @@  - - netstandard2.0;net8.0;net9.0 + netstandard2.0 enable enable true From 1bfcbbd21f5e0a55e52857c26aef0e88e12381b5 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 5 Sep 2025 08:52:39 +0100 Subject: [PATCH 054/108] eng: fix indirect context lookup --- .../RespCommandGenerator.cs | 14 ++++---------- .../RedisCommands.Server.cs | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index e6c40316c..b4337a907 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -180,11 +180,12 @@ private static void AddNotes(ref string notes, string note) // get context from the available fields string? context = null; - + IParameterSymbol? contextParam = null; foreach (var param in method.Parameters) { if (IsRESPite(param.Type, RESPite.RespContext)) { + contextParam = param; context = param.Name; break; } @@ -234,6 +235,7 @@ private static void AddNotes(ref string notes, string note) { if (IsIndirectRespContext(param.Type, out var memberName)) { + contextParam = param; context = $"{param.Name}.{memberName}"; break; } @@ -297,13 +299,11 @@ static bool IsIndirectRespContext(ITypeSymbol type, out string memberName) } } - static bool Ignore(ITypeSymbol symbol) => IsRESPite(symbol, RESPite.RespContext); // CT etc? - foreach (var param in method.Parameters) { var flags = ParameterFlags.Parameter; if (IsKey(param)) flags |= ParameterFlags.Key; - if (!Ignore(param.Type)) + if (contextParam is null || !SymbolEqualityComparer.Default.Equals(param, contextParam)) { flags |= ParameterFlags.Data; } @@ -399,7 +399,6 @@ private void Generate( bool Shared)> formatters = new(FormatterComparer.Default); - static bool IsThis(string modifier) => modifier.StartsWith("this "); foreach (var method in methods) { @@ -676,11 +675,6 @@ static void WriteTuple( if (count < 2) { var p = FirstDataParameter(parameters); - if (IsThis(p.Modifiers)) - { - p = parameters[1]; - } - sb.Append(mode == TupleMode.Values ? p.Name : p.Type); return; } diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.Server.cs b/src/RESPite.StackExchange.Redis/RedisCommands.Server.cs index 65e5fca9f..cdb98c566 100644 --- a/src/RESPite.StackExchange.Redis/RedisCommands.Server.cs +++ b/src/RESPite.StackExchange.Redis/RedisCommands.Server.cs @@ -15,5 +15,5 @@ internal readonly struct Servers(in RespContext context) internal static partial class ServerCommands { [RespCommand] - internal static partial void Ping(this in RespContext ctx); + internal static partial void Ping(this in Servers ctx); } From c140def27f59231e06e0dca25a5a9f1ab58f9579 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 5 Sep 2025 10:18:42 +0100 Subject: [PATCH 055/108] intermediate --- .../ProxiedDatabase.Connection.cs | 44 + .../ProxiedDatabase.Geo.cs | 99 + .../ProxiedDatabase.Hash.cs | 253 ++ .../ProxiedDatabase.Key.cs | 154 + .../ProxiedDatabase.Remaining.cs | 401 ++ .../ProxiedDatabase.SortedSet.cs | 247 ++ .../ProxiedDatabase.Stream.cs | 226 ++ .../ProxiedDatabase.String.cs | 208 + .../ProxiedDatabase.cs | 3434 +---------------- .../RESPite.StackExchange.Redis.csproj | 5 + 10 files changed, 1639 insertions(+), 3432 deletions(-) create mode 100644 src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs create mode 100644 src/RESPite.StackExchange.Redis/ProxiedDatabase.Geo.cs create mode 100644 src/RESPite.StackExchange.Redis/ProxiedDatabase.Hash.cs create mode 100644 src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs create mode 100644 src/RESPite.StackExchange.Redis/ProxiedDatabase.Remaining.cs create mode 100644 src/RESPite.StackExchange.Redis/ProxiedDatabase.SortedSet.cs create mode 100644 src/RESPite.StackExchange.Redis/ProxiedDatabase.Stream.cs create mode 100644 src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs new file mode 100644 index 000000000..b839437ca --- /dev/null +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs @@ -0,0 +1,44 @@ +using System.Net; +using System.Threading.Tasks; +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal sealed partial class ProxiedDatabase +{ + // Connection and core methods + public bool IsConnected(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task PingAsync(CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public TimeSpan Ping(CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task IdentifyEndpointAsync(RedisKey key = default, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public EndPoint? IdentifyEndpoint(RedisKey key = default, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IBatch CreateBatch(object? asyncState = null) => + throw new NotImplementedException(); + + public ITransaction CreateTransaction(object? asyncState = null) => + throw new NotImplementedException(); + + // Key migration + public Task KeyMigrateAsync(RedisKey key, EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, MigrateOptions migrateOptions = MigrateOptions.None, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public void KeyMigrate(RedisKey key, EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, MigrateOptions migrateOptions = MigrateOptions.None, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // Debug + public Task DebugObjectAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue DebugObject(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); +} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Geo.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Geo.cs new file mode 100644 index 000000000..477355371 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Geo.cs @@ -0,0 +1,99 @@ +using System.Threading.Tasks; +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal sealed partial class ProxiedDatabase +{ + // Async Geo methods + public Task GeoAddAsync(RedisKey key, double longitude, double latitude, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoAddAsync(RedisKey key, GeoEntry value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoAddAsync(RedisKey key, GeoEntry[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoDistanceAsync(RedisKey key, RedisValue member1, RedisValue member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoHashAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoHashAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoPositionAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoPositionAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoRadiusAsync(RedisKey key, RedisValue member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoRadiusAsync(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoSearchAsync(RedisKey key, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoSearchAsync(RedisKey key, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoSearchAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoSearchAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // Synchronous Geo methods + public bool GeoAdd(RedisKey key, double longitude, double latitude, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool GeoAdd(RedisKey key, GeoEntry value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long GeoAdd(RedisKey key, GeoEntry[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool GeoRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public double? GeoDistance(RedisKey key, RedisValue member1, RedisValue member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public string?[] GeoHash(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public string? GeoHash(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public GeoPosition?[] GeoPosition(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public GeoPosition? GeoPosition(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public GeoRadiusResult[] GeoRadius(RedisKey key, RedisValue member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public GeoRadiusResult[] GeoRadius(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public GeoRadiusResult[] GeoSearch(RedisKey key, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public GeoRadiusResult[] GeoSearch(RedisKey key, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long GeoSearchAndStore(RedisKey sourceKey, RedisKey destinationKey, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long GeoSearchAndStore(RedisKey sourceKey, RedisKey destinationKey, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); +} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Hash.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Hash.cs new file mode 100644 index 000000000..0dbf494b0 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Hash.cs @@ -0,0 +1,253 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal sealed partial class ProxiedDatabase +{ + // Async Hash methods + public Task HashDecrementAsync(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashDecrementAsync(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldGetExpireDateTimeAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldPersistAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldGetTimeToLiveAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashGetAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task?> HashGetLeaseAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashGetAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task?> HashFieldGetLeaseAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue field, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashGetAllAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashIncrementAsync(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashIncrementAsync(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashKeysAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashRandomFieldAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashRandomFieldsAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashRandomFieldsWithValuesAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IAsyncEnumerable HashScanAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IAsyncEnumerable HashScanNoValuesAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashSetAsync(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashSetAsync(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashStringLengthAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HashValuesAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // Synchronous Hash methods + public long HashDecrement(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public double HashDecrement(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool HashDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long HashDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool HashExists(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long[] HashFieldGetExpireDateTime(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public PersistResult[] HashFieldPersist(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long[] HashFieldGetTimeToLive(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue HashGet(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Lease? HashGetLease(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] HashGet(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue HashFieldGetAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Lease? HashFieldGetLeaseAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] HashFieldGetAndDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public HashEntry[] HashGetAll(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long HashIncrement(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public double HashIncrement(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] HashKeys(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long HashLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue HashRandomField(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] HashRandomFields(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public HashEntry[] HashRandomFieldsWithValues(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IEnumerable HashScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) => + throw new NotImplementedException(); + + public IEnumerable HashScan(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IEnumerable HashScanNoValues(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public void HashSet(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool HashSet(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long HashStringLength(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] HashValues(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); +} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs new file mode 100644 index 000000000..497e1db27 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs @@ -0,0 +1,154 @@ +using System.Net; +using System.Threading.Tasks; +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal sealed partial class ProxiedDatabase +{ + // Async Key methods + public Task KeyCopyAsync(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyDumpAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyEncodingAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyExistsAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags) => + throw new NotImplementedException(); + + public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags) => + throw new NotImplementedException(); + + public Task KeyExpireAsync(RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyExpireTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyFrequencyAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyIdleTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyMoveAsync(RedisKey key, int database, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyPersistAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyRandomAsync(CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyRefCountAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyRenameAsync(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyRestoreAsync(RedisKey key, byte[] value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyTimeToLiveAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyTouchAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyTouchAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task KeyTypeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // Synchronous Key methods + public bool KeyCopy(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long KeyDelete(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public byte[]? KeyDump(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public string? KeyEncoding(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool KeyExists(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long KeyExists(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags) => + throw new NotImplementedException(); + + public bool KeyExpire(RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags) => + throw new NotImplementedException(); + + public bool KeyExpire(RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public DateTime? KeyExpireTime(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long? KeyFrequency(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public TimeSpan? KeyIdleTime(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool KeyMove(RedisKey key, int database, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool KeyPersist(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisKey KeyRandom(CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long? KeyRefCount(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool KeyRename(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public void KeyRestore(RedisKey key, byte[] value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public TimeSpan? KeyTimeToLive(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool KeyTouch(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long KeyTouch(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisType KeyType(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); +} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Remaining.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Remaining.cs new file mode 100644 index 000000000..4a373e5bc --- /dev/null +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Remaining.cs @@ -0,0 +1,401 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal sealed partial class ProxiedDatabase +{ + // HyperLogLog methods + public Task HyperLogLogAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HyperLogLogAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HyperLogLogLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HyperLogLogLengthAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HyperLogLogMergeAsync(RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HyperLogLogMergeAsync(RedisKey destination, RedisKey[] sourceKeys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool HyperLogLogAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool HyperLogLogAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long HyperLogLogLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long HyperLogLogLength(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public void HyperLogLogMerge(RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public void HyperLogLogMerge(RedisKey destination, RedisKey[] sourceKeys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // List methods + public Task ListGetByIndexAsync(RedisKey key, long index, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListInsertAfterAsync(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListInsertBeforeAsync(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLeftPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLeftPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListPositionAsync(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListPositionsAsync(RedisKey key, RedisValue element, long count, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLeftPushAsync(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLeftPushAsync(RedisKey key, RedisValue[] values, When when, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLeftPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListMoveAsync(RedisKey sourceKey, RedisKey destinationKey, ListSide sourceSide, ListSide destinationSide, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRangeAsync(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRemoveAsync(RedisKey key, RedisValue value, long count = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPopLeftPushAsync(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPushAsync(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPushAsync(RedisKey key, RedisValue[] values, When when, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListSetByIndexAsync(RedisKey key, long index, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListTrimAsync(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue ListGetByIndex(RedisKey key, long index, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListInsertAfter(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListInsertBefore(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public ListPopResult ListLeftPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListPosition(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long[] ListPositions(RedisKey key, RedisValue element, long count, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListLeftPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListLeftPush(RedisKey key, RedisValue[] values, When when, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListLeftPush(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue ListMove(RedisKey sourceKey, RedisKey destinationKey, ListSide sourceSide, ListSide destinationSide, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] ListRange(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListRemove(RedisKey key, RedisValue value, long count = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public ListPopResult ListRightPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue ListRightPopLeftPush(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListRightPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListRightPush(RedisKey key, RedisValue[] values, When when, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListRightPush(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public void ListSetByIndex(RedisKey key, long index, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public void ListTrim(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // Lock methods + public Task LockExtendAsync(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task LockQueryAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task LockReleaseAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task LockTakeAsync(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool LockExtend(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue LockQuery(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool LockRelease(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool LockTake(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // Script/Execute/Publish methods + public Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ExecuteAsync(string command, params object[] args) => + throw new NotImplementedException(); + + public Task ExecuteAsync(string command, ICollection? args, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ScriptEvaluateAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ScriptEvaluateAsync(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ScriptEvaluateAsync(LoadedLuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ScriptEvaluateReadOnlyAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ScriptEvaluateReadOnlyAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult Execute(string command, params object[] args) => + throw new NotImplementedException(); + + public RedisResult Execute(string command, ICollection? args, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult ScriptEvaluate(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult ScriptEvaluate(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult ScriptEvaluate(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult ScriptEvaluate(LoadedLuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult ScriptEvaluateReadOnly(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult ScriptEvaluateReadOnly(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // Set methods + public Task SetAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetCombineAsync(SetOperation operation, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetCombineAsync(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetContainsAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetMembersAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetMoveAsync(RedisKey source, RedisKey destination, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetRemoveAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetRemoveAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IAsyncEnumerable SetScanAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SetAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SetCombine(SetOperation operation, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SetCombine(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SetContains(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool[] SetContains(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SetMembers(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SetMove(RedisKey source, RedisKey destination, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue SetPop(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SetPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue SetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SetRemove(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetRemove(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IEnumerable SetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) => + throw new NotImplementedException(); + + public IEnumerable SetScan(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // Sort methods + public Task SortAsync(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortAndStoreAsync(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortAndStore(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); +} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.SortedSet.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.SortedSet.cs new file mode 100644 index 000000000..9201a621d --- /dev/null +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.SortedSet.cs @@ -0,0 +1,247 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal sealed partial class ProxiedDatabase +{ + // Async SortedSet methods + public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, When when, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, When when, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetCombineAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetCombineWithScoresAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetDecrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetLengthAsync(RedisKey key, double min = double.NegativeInfinity, double max = double.PositiveInfinity, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetLengthByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRandomMembersWithScoresAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeByRankAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, RedisValue start, RedisValue stop, SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long? take = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeByRankWithScoresAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeByScoreAsync(RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeByScoreWithScoresAsync(RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeByValueAsync(RedisKey key, RedisValue min = default, RedisValue max = default, Exclude exclude = Exclude.None, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude, Order order, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRankAsync(RedisKey key, RedisValue member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRemoveAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRemoveRangeByRankAsync(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRemoveRangeByScoreAsync(RedisKey key, double start, double stop, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRemoveRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IAsyncEnumerable SortedSetScanAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetScoreAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetScoresAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetPopAsync(RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetPopAsync(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetPopAsync(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetUpdateAsync(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetUpdateAsync(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // Synchronous SortedSet methods + public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SortedSetAdd(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, When when, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SortedSetCombine(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public SortedSetEntry[] SortedSetCombineWithScores(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public double SortedSetDecrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public double SortedSetIncrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetLength(RedisKey key, double min = double.NegativeInfinity, double max = double.PositiveInfinity, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetLengthByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue SortedSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SortedSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public SortedSetEntry[] SortedSetRandomMembersWithScores(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SortedSetRangeByRank(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetRangeAndStore(RedisKey sourceKey, RedisKey destinationKey, RedisValue start, RedisValue stop, SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long? take = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public SortedSetEntry[] SortedSetRangeByRankWithScores(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SortedSetRangeByScore(RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public SortedSetEntry[] SortedSetRangeByScoreWithScores(RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SortedSetRangeByValue(RedisKey key, RedisValue min = default, RedisValue max = default, Exclude exclude = Exclude.None, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SortedSetRangeByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude, Order order, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long? SortedSetRank(RedisKey key, RedisValue member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SortedSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetRemove(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetRemoveRangeByRank(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetRemoveRangeByScore(RedisKey key, double start, double stop, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IEnumerable SortedSetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) => + throw new NotImplementedException(); + + public IEnumerable SortedSetScan(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public double? SortedSetScore(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public double?[] SortedSetScores(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public SortedSetEntry? SortedSetPop(RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public SortedSetEntry[] SortedSetPop(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public SortedSetPopResult SortedSetPop(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SortedSetUpdate(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetUpdate(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); +} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Stream.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Stream.cs new file mode 100644 index 000000000..607f28a30 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Stream.cs @@ -0,0 +1,226 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal sealed partial class ProxiedDatabase +{ + // Async Stream methods + public Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAcknowledgeAndDeleteAsync(RedisKey key, RedisValue groupName, StreamTrimMode trimMode, RedisValue messageId, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAcknowledgeAndDeleteAsync(RedisKey key, RedisValue groupName, StreamTrimMode trimMode, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAutoClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAutoClaimIdsOnlyAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamClaimIdsOnlyAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamConsumerGroupSetPositionAsync(RedisKey key, RedisValue groupName, RedisValue position, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamConsumerInfoAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamCreateConsumerGroupAsync(RedisKey key, RedisValue groupName, RedisValue? position = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamCreateConsumerGroupAsync(RedisKey key, RedisValue groupName, RedisValue? position = null, bool createStream = true, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, StreamTrimMode trimMode, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamDeleteConsumerAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamDeleteConsumerGroupAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamGroupInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamPendingAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, long? idleTime = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamRangeAsync(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamReadAsync(RedisKey key, RedisValue position, int? count = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamReadAsync(StreamPosition[] streamPositions, int? countPerStream = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamTrimAsync(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamTrimAsync(RedisKey key, long maxLength, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamTrimByMinIdAsync(RedisKey key, RedisValue minId, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // Synchronous Stream methods + public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamTrimResult StreamAcknowledgeAndDelete(RedisKey key, RedisValue groupName, StreamTrimMode trimMode, RedisValue messageId, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamTrimResult[] StreamAcknowledgeAndDelete(RedisKey key, RedisValue groupName, StreamTrimMode trimMode, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamAutoClaimResult StreamAutoClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamAutoClaimIdsOnlyResult StreamAutoClaimIdsOnly(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamEntry[] StreamClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] StreamClaimIdsOnly(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool StreamConsumerGroupSetPosition(RedisKey key, RedisValue groupName, RedisValue position, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamConsumerInfo[] StreamConsumerInfo(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool StreamCreateConsumerGroup(RedisKey key, RedisValue groupName, RedisValue? position = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool StreamCreateConsumerGroup(RedisKey key, RedisValue groupName, RedisValue? position = null, bool createStream = true, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamDelete(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamTrimResult[] StreamDelete(RedisKey key, RedisValue[] messageIds, StreamTrimMode trimMode, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamDeleteConsumer(RedisKey key, RedisValue groupName, RedisValue consumerName, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool StreamDeleteConsumerGroup(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamGroupInfo[] StreamGroupInfo(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamInfo StreamInfo(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamPendingInfo StreamPending(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, long? idleTime = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamEntry[] StreamRange(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamEntry[] StreamRead(RedisKey key, RedisValue position, int? count = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisStream[] StreamRead(StreamPosition[] streamPositions, int? countPerStream = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamTrim(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamTrim(RedisKey key, long maxLength, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamTrimByMinId(RedisKey key, RedisValue minId, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); +} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs new file mode 100644 index 000000000..43f57f2a5 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs @@ -0,0 +1,208 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal sealed partial class ProxiedDatabase +{ + // Async String methods + public Task StringAppendAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringBitCountAsync(RedisKey key, long start, long end, CommandFlags flags) => + throw new NotImplementedException(); + + public Task StringBitCountAsync(RedisKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringBitPositionAsync(RedisKey key, bool bit, long start, long end, CommandFlags flags) => + throw new NotImplementedException(); + + public Task StringBitPositionAsync(RedisKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringDecrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringDecrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringGetAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringGetAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task?> StringGetLeaseAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringGetBitAsync(RedisKey key, long offset, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringGetRangeAsync(RedisKey key, long start, long end, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringGetSetAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringGetSetExpiryAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringGetSetExpiryAsync(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringGetDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringGetWithExpiryAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringIncrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringIncrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringLongestCommonSubsequenceAsync(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringLongestCommonSubsequenceLengthAsync(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringLongestCommonSubsequenceWithMatchesAsync(RedisKey first, RedisKey second, long minLength = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when) => + throw new NotImplementedException(); + + public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => + throw new NotImplementedException(); + + public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringSetAsync(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => + throw new NotImplementedException(); + + public Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringSetBitAsync(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StringSetRangeAsync(RedisKey key, long offset, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // Synchronous String methods + public long StringAppend(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StringBitCount(RedisKey key, long start, long end, CommandFlags flags) => + throw new NotImplementedException(); + + public long StringBitCount(RedisKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StringBitPosition(RedisKey key, bool bit, long start, long end, CommandFlags flags) => + throw new NotImplementedException(); + + public long StringBitPosition(RedisKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StringDecrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public double StringDecrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StringGet(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] StringGet(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Lease? StringGetLease(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool StringGetBit(RedisKey key, long offset, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StringGetRange(RedisKey key, long start, long end, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StringGetSet(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StringGetSetExpiry(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StringGetSetExpiry(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StringGetDelete(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValueWithExpiry StringGetWithExpiry(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StringIncrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public double StringIncrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public string? StringLongestCommonSubsequence(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StringLongestCommonSubsequenceLength(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public LCSMatchResult StringLongestCommonSubsequenceWithMatches(RedisKey first, RedisKey second, long minLength = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when) => + throw new NotImplementedException(); + + public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => + throw new NotImplementedException(); + + public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool StringSet(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => + throw new NotImplementedException(); + + public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool StringSetBit(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StringSetRange(RedisKey key, long offset, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); +} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs index 811b976a6..245270449 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs @@ -1,5 +1,4 @@ -using System.Net; -using StackExchange.Redis; +using StackExchange.Redis; namespace RESPite.StackExchange.Redis; @@ -8,7 +7,7 @@ namespace RESPite.StackExchange.Redis; /// could be direct to a known server or routed - the is responsible for /// that determination. /// -internal class ProxiedDatabase(IRespContextProxy proxy, int db) : IDatabase +internal sealed partial class ProxiedDatabase(IRespContextProxy proxy, int db) : IDatabase { // Question: cache this, or rebuild each time? the latter handles shutdown better. // internal readonly RespContext Context = proxy.Context.WithDatabase(db); @@ -17,7 +16,6 @@ internal class ProxiedDatabase(IRespContextProxy proxy, int db) : IDatabase public int Database => db; public IConnectionMultiplexer Multiplexer => proxy.Multiplexer; - public Task PingAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public bool TryWait(Task task) => proxy.Multiplexer.TryWait(task); @@ -26,3432 +24,4 @@ internal class ProxiedDatabase(IRespContextProxy proxy, int db) : IDatabase public T Wait(Task task) => proxy.Multiplexer.Wait(task); public void WaitAll(params Task[] tasks) => proxy.Multiplexer.WaitAll(tasks); - - public TimeSpan Ping(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public bool IsConnected( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyMigrateAsync( - RedisKey key, - EndPoint toServer, - int toDatabase = 0, - int timeoutMilliseconds = 0, - MigrateOptions migrateOptions = MigrateOptions.None, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task DebugObjectAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoAddAsync( - RedisKey key, - double longitude, - double latitude, - RedisValue member, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoAddAsync( - RedisKey key, - GeoEntry value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoAddAsync( - RedisKey key, - GeoEntry[] values, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoRemoveAsync( - RedisKey key, - RedisValue member, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoDistanceAsync( - RedisKey key, - RedisValue member1, - RedisValue member2, - GeoUnit unit = GeoUnit.Meters, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoHashAsync( - RedisKey key, - RedisValue[] members, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoHashAsync( - RedisKey key, - RedisValue member, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoPositionAsync( - RedisKey key, - RedisValue[] members, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task GeoPositionAsync( - RedisKey key, - RedisValue member, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoRadiusAsync( - RedisKey key, - RedisValue member, - double radius, - GeoUnit unit = GeoUnit.Meters, - int count = -1, - Order? order = null, - GeoRadiusOptions options = GeoRadiusOptions.Default, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoRadiusAsync( - RedisKey key, - double longitude, - double latitude, - double radius, - GeoUnit unit = GeoUnit.Meters, - int count = -1, - Order? order = null, - GeoRadiusOptions options = GeoRadiusOptions.Default, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoSearchAsync( - RedisKey key, - RedisValue member, - GeoSearchShape shape, - int count = -1, - bool demandClosest = true, - Order? order = null, - GeoRadiusOptions options = GeoRadiusOptions.Default, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoSearchAsync( - RedisKey key, - double longitude, - double latitude, - GeoSearchShape shape, - int count = -1, - bool demandClosest = true, - Order? order = null, - GeoRadiusOptions options = GeoRadiusOptions.Default, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoSearchAndStoreAsync( - RedisKey sourceKey, - RedisKey destinationKey, - RedisValue member, - GeoSearchShape shape, - int count = -1, - bool demandClosest = true, - Order? order = null, - bool storeDistances = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoSearchAndStoreAsync( - RedisKey sourceKey, - RedisKey destinationKey, - double longitude, - double latitude, - GeoSearchShape shape, - int count = -1, - bool demandClosest = true, - Order? order = null, - bool storeDistances = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashDecrementAsync( - RedisKey key, - RedisValue hashField, - long value = 1, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task HashDecrementAsync( - RedisKey key, - RedisValue hashField, - double value, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task HashDeleteAsync( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashDeleteAsync( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashExistsAsync( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashFieldGetAndDeleteAsync( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task?> HashFieldGetLeaseAndDeleteAsync( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task HashFieldGetAndDeleteAsync( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task HashFieldGetAndSetExpiryAsync( - RedisKey key, - RedisValue hashField, - TimeSpan? expiry = null, - bool persist = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashFieldGetAndSetExpiryAsync( - RedisKey key, - RedisValue hashField, - DateTime expiry, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task?> HashFieldGetLeaseAndSetExpiryAsync( - RedisKey key, - RedisValue hashField, - TimeSpan? expiry = null, - bool persist = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task?> HashFieldGetLeaseAndSetExpiryAsync( - RedisKey key, - RedisValue hashField, - DateTime expiry, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashFieldGetAndSetExpiryAsync( - RedisKey key, - RedisValue[] hashFields, - TimeSpan? expiry = null, - bool persist = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashFieldGetAndSetExpiryAsync( - RedisKey key, - RedisValue[] hashFields, - DateTime expiry, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashFieldSetAndSetExpiryAsync( - RedisKey key, - RedisValue field, - RedisValue value, - TimeSpan? expiry = null, - bool keepTtl = false, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashFieldSetAndSetExpiryAsync( - RedisKey key, - RedisValue field, - RedisValue value, - DateTime expiry, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashFieldSetAndSetExpiryAsync( - RedisKey key, - HashEntry[] hashFields, - TimeSpan? expiry = null, - bool keepTtl = false, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashFieldSetAndSetExpiryAsync( - RedisKey key, - HashEntry[] hashFields, - DateTime expiry, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashFieldExpireAsync( - RedisKey key, - RedisValue[] hashFields, - TimeSpan expiry, - ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashFieldExpireAsync( - RedisKey key, - RedisValue[] hashFields, - DateTime expiry, - ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashFieldGetExpireDateTimeAsync( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task HashFieldPersistAsync( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task HashFieldGetTimeToLiveAsync( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task HashGetAsync( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task?> HashGetLeaseAsync( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task HashGetAsync( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task HashGetAllAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashIncrementAsync( - RedisKey key, - RedisValue hashField, - long value = 1, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task HashIncrementAsync( - RedisKey key, - RedisValue hashField, - double value, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task HashKeysAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashLengthAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashRandomFieldAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashRandomFieldsAsync( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashRandomFieldsWithValuesAsync( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public IAsyncEnumerable HashScanAsync( - RedisKey key, - RedisValue pattern = default, - int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, - long cursor = RedisBase.CursorUtils.Origin, - int pageOffset = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public IAsyncEnumerable HashScanNoValuesAsync( - RedisKey key, - RedisValue pattern = default, - int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, - long cursor = RedisBase.CursorUtils.Origin, - int pageOffset = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashSetAsync( - RedisKey key, - HashEntry[] hashFields, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashSetAsync( - RedisKey key, - RedisValue hashField, - RedisValue value, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashStringLengthAsync( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashValuesAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HyperLogLogAddAsync( - RedisKey key, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HyperLogLogAddAsync( - RedisKey key, - RedisValue[] values, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HyperLogLogLengthAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HyperLogLogLengthAsync( - RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HyperLogLogMergeAsync( - RedisKey destination, - RedisKey first, - RedisKey second, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HyperLogLogMergeAsync( - RedisKey destination, - RedisKey[] sourceKeys, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task IdentifyEndpointAsync( - RedisKey key = default, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyCopyAsync( - RedisKey sourceKey, - RedisKey destinationKey, - int destinationDatabase = -1, - bool replace = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyDeleteAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyDeleteAsync( - RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyDumpAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyEncodingAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyExistsAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyExistsAsync( - RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyExpireAsync( - RedisKey key, - TimeSpan? expiry, - CommandFlags flags) => - throw new NotImplementedException(); - - public Task KeyExpireAsync( - RedisKey key, - TimeSpan? expiry, - ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyExpireAsync( - RedisKey key, - DateTime? expiry, - CommandFlags flags) => - throw new NotImplementedException(); - - public Task KeyExpireAsync( - RedisKey key, - DateTime? expiry, - ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyExpireTimeAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyFrequencyAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyIdleTimeAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyMoveAsync( - RedisKey key, - int database, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyPersistAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyRandomAsync( - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task KeyRefCountAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyRenameAsync( - RedisKey key, - RedisKey newKey, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task KeyRestoreAsync( - RedisKey key, - byte[] value, - TimeSpan? expiry = null, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task KeyTimeToLiveAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyTouchAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyTouchAsync( - RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task KeyTypeAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListGetByIndexAsync( - RedisKey key, - long index, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListInsertAfterAsync( - RedisKey key, - RedisValue pivot, - RedisValue value, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task ListInsertBeforeAsync( - RedisKey key, - RedisValue pivot, - RedisValue value, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task ListLeftPopAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListLeftPopAsync( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListLeftPopAsync( - RedisKey[] keys, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListPositionAsync( - RedisKey key, - RedisValue element, - long rank = 1, - long maxLength = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListPositionsAsync( - RedisKey key, - RedisValue element, - long count, - long rank = 1, - long maxLength = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListLeftPushAsync( - RedisKey key, - RedisValue value, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task ListLeftPushAsync( - RedisKey key, - RedisValue[] values, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListLeftPushAsync( - RedisKey key, - RedisValue[] values, - CommandFlags flags) => - throw new NotImplementedException(); - - public Task ListLengthAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListMoveAsync( - RedisKey sourceKey, - RedisKey destinationKey, - ListSide sourceSide, - ListSide destinationSide, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListRangeAsync( - RedisKey key, - long start = 0, - long stop = -1, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task ListRemoveAsync( - RedisKey key, - RedisValue value, - long count = 0, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task ListRightPopAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListRightPopAsync( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListRightPopAsync( - RedisKey[] keys, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListRightPopLeftPushAsync( - RedisKey source, - RedisKey destination, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task ListRightPushAsync( - RedisKey key, - RedisValue value, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListRightPushAsync( - RedisKey key, - RedisValue[] values, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListRightPushAsync( - RedisKey key, - RedisValue[] values, - CommandFlags flags) => - throw new NotImplementedException(); - - public Task ListSetByIndexAsync( - RedisKey key, - long index, - RedisValue value, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task ListTrimAsync( - RedisKey key, - long start, - long stop, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task LockExtendAsync( - RedisKey key, - RedisValue value, - TimeSpan expiry, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task LockQueryAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task LockReleaseAsync( - RedisKey key, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task LockTakeAsync( - RedisKey key, - RedisValue value, - TimeSpan expiry, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task PublishAsync( - RedisChannel channel, - RedisValue message, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ExecuteAsync(string command, params object[] args) => throw new NotImplementedException(); - - public Task ExecuteAsync( - string command, - ICollection? args, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task ScriptEvaluateAsync( - string script, - RedisKey[]? keys = null, - RedisValue[]? values = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ScriptEvaluateAsync( - byte[] hash, - RedisKey[]? keys = null, - RedisValue[]? values = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ScriptEvaluateAsync( - LuaScript script, - object? parameters = null, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task ScriptEvaluateAsync( - LoadedLuaScript script, - object? parameters = null, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task ScriptEvaluateReadOnlyAsync( - string script, - RedisKey[]? keys = null, - RedisValue[]? values = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ScriptEvaluateReadOnlyAsync( - byte[] hash, - RedisKey[]? keys = null, - RedisValue[]? values = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetAddAsync( - RedisKey key, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetAddAsync( - RedisKey key, - RedisValue[] values, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetCombineAsync( - SetOperation operation, - RedisKey first, - RedisKey second, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task SetCombineAsync( - SetOperation operation, - RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task SetCombineAndStoreAsync( - SetOperation operation, - RedisKey destination, - RedisKey first, - RedisKey second, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetCombineAndStoreAsync( - SetOperation operation, - RedisKey destination, - RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetContainsAsync( - RedisKey key, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetContainsAsync( - RedisKey key, - RedisValue[] values, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetIntersectionLengthAsync( - RedisKey[] keys, - long limit = 0, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task SetLengthAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetMembersAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetMoveAsync( - RedisKey source, - RedisKey destination, - RedisValue value, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task SetPopAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetPopAsync( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetRandomMemberAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetRandomMembersAsync( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetRemoveAsync( - RedisKey key, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetRemoveAsync( - RedisKey key, - RedisValue[] values, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public IAsyncEnumerable SetScanAsync( - RedisKey key, - RedisValue pattern = default, - int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, - long cursor = RedisBase.CursorUtils.Origin, - int pageOffset = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortAsync( - RedisKey key, - long skip = 0, - long take = -1, - Order order = Order.Ascending, - SortType sortType = SortType.Numeric, - RedisValue by = default, - RedisValue[]? get = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortAndStoreAsync( - RedisKey destination, - RedisKey key, - long skip = 0, - long take = -1, - Order order = Order.Ascending, - SortType sortType = SortType.Numeric, - RedisValue by = default, - RedisValue[]? get = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetAddAsync( - RedisKey key, - RedisValue member, - double score, - CommandFlags flags) => - throw new NotImplementedException(); - - public Task SortedSetAddAsync( - RedisKey key, - RedisValue member, - double score, - When when, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetAddAsync( - RedisKey key, - RedisValue member, - double score, - SortedSetWhen when = SortedSetWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetAddAsync( - RedisKey key, - SortedSetEntry[] values, - CommandFlags flags) => - throw new NotImplementedException(); - - public Task SortedSetAddAsync( - RedisKey key, - SortedSetEntry[] values, - When when, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task SortedSetAddAsync( - RedisKey key, - SortedSetEntry[] values, - SortedSetWhen when = SortedSetWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetCombineAsync( - SetOperation operation, - RedisKey[] keys, - double[]? weights = null, - Aggregate aggregate = Aggregate.Sum, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetCombineWithScoresAsync( - SetOperation operation, - RedisKey[] keys, - double[]? weights = null, - Aggregate aggregate = Aggregate.Sum, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetCombineAndStoreAsync( - SetOperation operation, - RedisKey destination, - RedisKey first, - RedisKey second, - Aggregate aggregate = Aggregate.Sum, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetCombineAndStoreAsync( - SetOperation operation, - RedisKey destination, - RedisKey[] keys, - double[]? weights = null, - Aggregate aggregate = Aggregate.Sum, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetDecrementAsync( - RedisKey key, - RedisValue member, - double value, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task SortedSetIncrementAsync( - RedisKey key, - RedisValue member, - double value, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task SortedSetIntersectionLengthAsync( - RedisKey[] keys, - long limit = 0, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task SortedSetLengthAsync( - RedisKey key, - double min = double.NegativeInfinity, - double max = double.PositiveInfinity, - Exclude exclude = Exclude.None, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetLengthByValueAsync( - RedisKey key, - RedisValue min, - RedisValue max, - Exclude exclude = Exclude.None, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRandomMemberAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRandomMembersAsync( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task SortedSetRandomMembersWithScoresAsync( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task SortedSetRangeByRankAsync( - RedisKey key, - long start = 0, - long stop = -1, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeAndStoreAsync( - RedisKey sourceKey, - RedisKey destinationKey, - RedisValue start, - RedisValue stop, - SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, - Exclude exclude = Exclude.None, - Order order = Order.Ascending, - long skip = 0, - long? take = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeByRankWithScoresAsync( - RedisKey key, - long start = 0, - long stop = -1, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeByScoreAsync( - RedisKey key, - double start = double.NegativeInfinity, - double stop = double.PositiveInfinity, - Exclude exclude = Exclude.None, - Order order = Order.Ascending, - long skip = 0, - long take = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeByScoreWithScoresAsync( - RedisKey key, - double start = double.NegativeInfinity, - double stop = double.PositiveInfinity, - Exclude exclude = Exclude.None, - Order order = Order.Ascending, - long skip = 0, - long take = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeByValueAsync( - RedisKey key, - RedisValue min, - RedisValue max, - Exclude exclude, - long skip, - long take = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeByValueAsync( - RedisKey key, - RedisValue min = default, - RedisValue max = default, - Exclude exclude = Exclude.None, - Order order = Order.Ascending, - long skip = 0, - long take = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRankAsync( - RedisKey key, - RedisValue member, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRemoveAsync( - RedisKey key, - RedisValue member, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRemoveAsync( - RedisKey key, - RedisValue[] members, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRemoveRangeByRankAsync( - RedisKey key, - long start, - long stop, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task SortedSetRemoveRangeByScoreAsync( - RedisKey key, - double start, - double stop, - Exclude exclude = Exclude.None, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRemoveRangeByValueAsync( - RedisKey key, - RedisValue min, - RedisValue max, - Exclude exclude = Exclude.None, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public IAsyncEnumerable SortedSetScanAsync( - RedisKey key, - RedisValue pattern = default, - int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, - long cursor = RedisBase.CursorUtils.Origin, - int pageOffset = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetScoreAsync( - RedisKey key, - RedisValue member, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetScoresAsync( - RedisKey key, - RedisValue[] members, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task SortedSetUpdateAsync( - RedisKey key, - RedisValue member, - double score, - SortedSetWhen when = SortedSetWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetUpdateAsync( - RedisKey key, - SortedSetEntry[] values, - SortedSetWhen when = SortedSetWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetPopAsync( - RedisKey key, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task SortedSetPopAsync( - RedisKey key, - long count, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task SortedSetPopAsync( - RedisKey[] keys, - long count, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamAcknowledgeAsync( - RedisKey key, - RedisValue groupName, - RedisValue messageId, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamAcknowledgeAsync( - RedisKey key, - RedisValue groupName, - RedisValue[] messageIds, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamAcknowledgeAndDeleteAsync( - RedisKey key, - RedisValue groupName, - StreamTrimMode mode, - RedisValue messageId, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamAcknowledgeAndDeleteAsync( - RedisKey key, - RedisValue groupName, - StreamTrimMode mode, - RedisValue[] messageIds, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamAddAsync( - RedisKey key, - RedisValue streamField, - RedisValue streamValue, - RedisValue? messageId, - int? maxLength, - bool useApproximateMaxLength, - CommandFlags flags) => - throw new NotImplementedException(); - - public Task StreamAddAsync( - RedisKey key, - NameValueEntry[] streamPairs, - RedisValue? messageId, - int? maxLength, - bool useApproximateMaxLength, - CommandFlags flags) => - throw new NotImplementedException(); - - public Task StreamAddAsync( - RedisKey key, - RedisValue streamField, - RedisValue streamValue, - RedisValue? messageId = null, - long? maxLength = null, - bool useApproximateMaxLength = false, - long? limit = null, - StreamTrimMode trimMode = StreamTrimMode.KeepReferences, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamAddAsync( - RedisKey key, - NameValueEntry[] streamPairs, - RedisValue? messageId = null, - long? maxLength = null, - bool useApproximateMaxLength = false, - long? limit = null, - StreamTrimMode trimMode = StreamTrimMode.KeepReferences, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamAutoClaimAsync( - RedisKey key, - RedisValue consumerGroup, - RedisValue claimingConsumer, - long minIdleTimeInMs, - RedisValue startAtId, - int? count = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamAutoClaimIdsOnlyAsync( - RedisKey key, - RedisValue consumerGroup, - RedisValue claimingConsumer, - long minIdleTimeInMs, - RedisValue startAtId, - int? count = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamClaimAsync( - RedisKey key, - RedisValue consumerGroup, - RedisValue claimingConsumer, - long minIdleTimeInMs, - RedisValue[] messageIds, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamClaimIdsOnlyAsync( - RedisKey key, - RedisValue consumerGroup, - RedisValue claimingConsumer, - long minIdleTimeInMs, - RedisValue[] messageIds, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamConsumerGroupSetPositionAsync( - RedisKey key, - RedisValue groupName, - RedisValue position, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamConsumerInfoAsync( - RedisKey key, - RedisValue groupName, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task StreamCreateConsumerGroupAsync( - RedisKey key, - RedisValue groupName, - RedisValue? position, - CommandFlags flags) => throw new NotImplementedException(); - - public Task StreamCreateConsumerGroupAsync( - RedisKey key, - RedisValue groupName, - RedisValue? position = null, - bool createStream = true, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamDeleteAsync( - RedisKey key, - RedisValue[] messageIds, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamDeleteAsync( - RedisKey key, - RedisValue[] messageIds, - StreamTrimMode mode, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamDeleteConsumerAsync( - RedisKey key, - RedisValue groupName, - RedisValue consumerName, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamDeleteConsumerGroupAsync( - RedisKey key, - RedisValue groupName, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task StreamGroupInfoAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamInfoAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamLengthAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamPendingAsync( - RedisKey key, - RedisValue groupName, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task StreamPendingMessagesAsync( - RedisKey key, - RedisValue groupName, - int count, - RedisValue consumerName, - RedisValue? minId, - RedisValue? maxId, - CommandFlags flags) => - throw new NotImplementedException(); - - public Task StreamPendingMessagesAsync( - RedisKey key, - RedisValue groupName, - int count, - RedisValue consumerName, - RedisValue? minId = null, - RedisValue? maxId = null, - long? minIdleTimeInMs = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamRangeAsync( - RedisKey key, - RedisValue? minId = null, - RedisValue? maxId = null, - int? count = null, - Order messageOrder = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamReadAsync( - RedisKey key, - RedisValue position, - int? count = null, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task StreamReadAsync( - StreamPosition[] streamPositions, - int? countPerStream = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamReadGroupAsync( - RedisKey key, - RedisValue groupName, - RedisValue consumerName, - RedisValue? position, - int? count, - CommandFlags flags) => - throw new NotImplementedException(); - - public Task StreamReadGroupAsync( - RedisKey key, - RedisValue groupName, - RedisValue consumerName, - RedisValue? position = null, - int? count = null, - bool noAck = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamReadGroupAsync( - StreamPosition[] streamPositions, - RedisValue groupName, - RedisValue consumerName, - int? countPerStream, - CommandFlags flags) => - throw new NotImplementedException(); - - public Task StreamReadGroupAsync( - StreamPosition[] streamPositions, - RedisValue groupName, - RedisValue consumerName, - int? countPerStream = null, - bool noAck = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamTrimAsync( - RedisKey key, - int maxLength, - bool useApproximateMaxLength, - CommandFlags flags) => - throw new NotImplementedException(); - - public Task StreamTrimAsync( - RedisKey key, - long maxLength, - bool useApproximateMaxLength = false, - long? limit = null, - StreamTrimMode mode = StreamTrimMode.KeepReferences, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamTrimByMinIdAsync( - RedisKey key, - RedisValue minId, - bool useApproximateMaxLength = false, - long? limit = null, - StreamTrimMode mode = StreamTrimMode.KeepReferences, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringAppendAsync( - RedisKey key, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringBitCountAsync( - RedisKey key, - long start, - long end, - CommandFlags flags) => - throw new NotImplementedException(); - - public Task StringBitCountAsync( - RedisKey key, - long start = 0, - long end = -1, - StringIndexType indexType = StringIndexType.Byte, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringBitOperationAsync( - Bitwise operation, - RedisKey destination, - RedisKey first, - RedisKey second = default, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringBitOperationAsync( - Bitwise operation, - RedisKey destination, - RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringBitPositionAsync( - RedisKey key, - bool bit, - long start, - long end, - CommandFlags flags) => - throw new NotImplementedException(); - - public Task StringBitPositionAsync( - RedisKey key, - bool bit, - long start = 0, - long end = -1, - StringIndexType indexType = StringIndexType.Byte, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringDecrementAsync( - RedisKey key, - long value = 1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringDecrementAsync( - RedisKey key, - double value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringGetAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringGetAsync( - RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task?> StringGetLeaseAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringGetBitAsync( - RedisKey key, - long offset, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringGetRangeAsync( - RedisKey key, - long start, - long end, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task StringGetSetAsync( - RedisKey key, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringGetSetExpiryAsync( - RedisKey key, - TimeSpan? expiry, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task StringGetSetExpiryAsync( - RedisKey key, - DateTime expiry, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task StringGetDeleteAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringGetWithExpiryAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringIncrementAsync( - RedisKey key, - long value = 1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringIncrementAsync( - RedisKey key, - double value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringLengthAsync( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringLongestCommonSubsequenceAsync( - RedisKey first, - RedisKey second, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task StringLongestCommonSubsequenceLengthAsync( - RedisKey first, - RedisKey second, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task StringLongestCommonSubsequenceWithMatchesAsync( - RedisKey first, - RedisKey second, - long minLength = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringSetAsync( - RedisKey key, - RedisValue value, - TimeSpan? expiry, - When when) => - throw new NotImplementedException(); - - public Task StringSetAsync( - RedisKey key, - RedisValue value, - TimeSpan? expiry, - When when, - CommandFlags flags) => - throw new NotImplementedException(); - - public Task StringSetAsync( - RedisKey key, - RedisValue value, - TimeSpan? expiry = null, - bool keepTtl = false, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringSetAsync( - KeyValuePair[] values, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task StringSetAndGetAsync( - RedisKey key, - RedisValue value, - TimeSpan? expiry, - When when, - CommandFlags flags) => throw new NotImplementedException(); - - public Task StringSetAndGetAsync( - RedisKey key, - RedisValue value, - TimeSpan? expiry = null, - bool keepTtl = false, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringSetBitAsync( - RedisKey key, - long offset, - bool bit, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringSetRangeAsync( - RedisKey key, - long offset, - RedisValue value, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public IBatch CreateBatch(object? asyncState = null) => throw new NotImplementedException(); - - public ITransaction CreateTransaction(object? asyncState = null) => throw new NotImplementedException(); - - public void KeyMigrate( - RedisKey key, - EndPoint toServer, - int toDatabase = 0, - int timeoutMilliseconds = 0, - MigrateOptions migrateOptions = MigrateOptions.None, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue DebugObject( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool GeoAdd( - RedisKey key, - double longitude, - double latitude, - RedisValue member, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public bool GeoAdd( - RedisKey key, - GeoEntry value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long GeoAdd( - RedisKey key, - GeoEntry[] values, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool GeoRemove( - RedisKey key, - RedisValue member, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public double? GeoDistance( - RedisKey key, - RedisValue member1, - RedisValue member2, - GeoUnit unit = GeoUnit.Meters, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public string?[] GeoHash( - RedisKey key, - RedisValue[] members, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public string? GeoHash( - RedisKey key, - RedisValue member, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public GeoPosition?[] GeoPosition( - RedisKey key, - RedisValue[] members, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public GeoPosition? GeoPosition( - RedisKey key, - RedisValue member, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public GeoRadiusResult[] GeoRadius( - RedisKey key, - RedisValue member, - double radius, - GeoUnit unit = GeoUnit.Meters, - int count = -1, - Order? order = null, - GeoRadiusOptions options = GeoRadiusOptions.Default, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public GeoRadiusResult[] GeoRadius( - RedisKey key, - double longitude, - double latitude, - double radius, - GeoUnit unit = GeoUnit.Meters, - int count = -1, - Order? order = null, - GeoRadiusOptions options = GeoRadiusOptions.Default, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public GeoRadiusResult[] GeoSearch( - RedisKey key, - RedisValue member, - GeoSearchShape shape, - int count = -1, - bool demandClosest = true, - Order? order = null, - GeoRadiusOptions options = GeoRadiusOptions.Default, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public GeoRadiusResult[] GeoSearch( - RedisKey key, - double longitude, - double latitude, - GeoSearchShape shape, - int count = -1, - bool demandClosest = true, - Order? order = null, - GeoRadiusOptions options = GeoRadiusOptions.Default, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long GeoSearchAndStore( - RedisKey sourceKey, - RedisKey destinationKey, - RedisValue member, - GeoSearchShape shape, - int count = -1, - bool demandClosest = true, - Order? order = null, - bool storeDistances = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long GeoSearchAndStore( - RedisKey sourceKey, - RedisKey destinationKey, - double longitude, - double latitude, - GeoSearchShape shape, - int count = -1, - bool demandClosest = true, - Order? order = null, - bool storeDistances = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long HashDecrement( - RedisKey key, - RedisValue hashField, - long value = 1, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public double HashDecrement( - RedisKey key, - RedisValue hashField, - double value, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public bool HashDelete( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long HashDelete( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool HashExists( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public ExpireResult[] HashFieldExpire( - RedisKey key, - RedisValue[] hashFields, - TimeSpan expiry, - ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public ExpireResult[] HashFieldExpire( - RedisKey key, - RedisValue[] hashFields, - DateTime expiry, - ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long[] HashFieldGetExpireDateTime( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public PersistResult[] HashFieldPersist( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public long[] HashFieldGetTimeToLive( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue HashGet( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Lease? HashGetLease( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] HashGet( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue HashFieldGetAndDelete( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Lease? HashFieldGetLeaseAndDelete( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public RedisValue[] HashFieldGetAndDelete( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public RedisValue HashFieldGetAndSetExpiry( - RedisKey key, - RedisValue hashField, - TimeSpan? expiry = null, - bool persist = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue HashFieldGetAndSetExpiry( - RedisKey key, - RedisValue hashField, - DateTime expiry, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Lease? HashFieldGetLeaseAndSetExpiry( - RedisKey key, - RedisValue hashField, - TimeSpan? expiry = null, - bool persist = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Lease? HashFieldGetLeaseAndSetExpiry( - RedisKey key, - RedisValue hashField, - DateTime expiry, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] HashFieldGetAndSetExpiry( - RedisKey key, - RedisValue[] hashFields, - TimeSpan? expiry = null, - bool persist = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] HashFieldGetAndSetExpiry( - RedisKey key, - RedisValue[] hashFields, - DateTime expiry, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue HashFieldSetAndSetExpiry( - RedisKey key, - RedisValue field, - RedisValue value, - TimeSpan? expiry = null, - bool keepTtl = false, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue HashFieldSetAndSetExpiry( - RedisKey key, - RedisValue field, - RedisValue value, - DateTime expiry, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue HashFieldSetAndSetExpiry( - RedisKey key, - HashEntry[] hashFields, - TimeSpan? expiry = null, - bool keepTtl = false, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue HashFieldSetAndSetExpiry( - RedisKey key, - HashEntry[] hashFields, - DateTime expiry, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public HashEntry[] HashGetAll( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long HashIncrement( - RedisKey key, - RedisValue hashField, - long value = 1, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public double HashIncrement( - RedisKey key, - RedisValue hashField, - double value, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public RedisValue[] HashKeys( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long HashLength( - RedisKey key, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public RedisValue HashRandomField( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] HashRandomFields( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public HashEntry[] HashRandomFieldsWithValues( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public IEnumerable HashScan( - RedisKey key, - RedisValue pattern, - int pageSize, - CommandFlags flags) => - throw new NotImplementedException(); - - public IEnumerable HashScan( - RedisKey key, - RedisValue pattern = default, - int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, - long cursor = RedisBase.CursorUtils.Origin, - int pageOffset = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public IEnumerable HashScanNoValues( - RedisKey key, - RedisValue pattern = default, - int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, - long cursor = RedisBase.CursorUtils.Origin, - int pageOffset = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public void HashSet( - RedisKey key, - HashEntry[] hashFields, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool HashSet( - RedisKey key, - RedisValue hashField, - RedisValue value, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long HashStringLength( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] HashValues( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool HyperLogLogAdd( - RedisKey key, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool HyperLogLogAdd( - RedisKey key, - RedisValue[] values, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long HyperLogLogLength( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long HyperLogLogLength( - RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public void HyperLogLogMerge( - RedisKey destination, - RedisKey first, - RedisKey second, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public void HyperLogLogMerge( - RedisKey destination, - RedisKey[] sourceKeys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public EndPoint? IdentifyEndpoint( - RedisKey key = default, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool KeyCopy( - RedisKey sourceKey, - RedisKey destinationKey, - int destinationDatabase = -1, - bool replace = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool KeyDelete( - RedisKey key, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public long KeyDelete( - RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public byte[]? KeyDump( - RedisKey key, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public string? KeyEncoding( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool KeyExists( - RedisKey key, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public long KeyExists( - RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool KeyExpire( - RedisKey key, - TimeSpan? expiry, - CommandFlags flags) => throw new NotImplementedException(); - - public bool KeyExpire( - RedisKey key, - TimeSpan? expiry, - ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool KeyExpire( - RedisKey key, - DateTime? expiry, - CommandFlags flags) => throw new NotImplementedException(); - - public bool KeyExpire( - RedisKey key, - DateTime? expiry, - ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public DateTime? KeyExpireTime( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long? KeyFrequency( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public TimeSpan? KeyIdleTime( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool KeyMove( - RedisKey key, - int database, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool KeyPersist( - RedisKey key, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public RedisKey KeyRandom( - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public long? KeyRefCount( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool KeyRename( - RedisKey key, - RedisKey newKey, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public void KeyRestore( - RedisKey key, - byte[] value, - TimeSpan? expiry = null, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public TimeSpan? KeyTimeToLive( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool KeyTouch( - RedisKey key, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public long KeyTouch( - RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisType KeyType( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue ListGetByIndex( - RedisKey key, - long index, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long ListInsertAfter( - RedisKey key, - RedisValue pivot, - RedisValue value, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public long ListInsertBefore( - RedisKey key, - RedisValue pivot, - RedisValue value, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public RedisValue ListLeftPop( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] ListLeftPop( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public ListPopResult ListLeftPop( - RedisKey[] keys, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long ListPosition( - RedisKey key, - RedisValue element, - long rank = 1, - long maxLength = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long[] ListPositions( - RedisKey key, - RedisValue element, - long count, - long rank = 1, - long maxLength = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long ListLeftPush( - RedisKey key, - RedisValue value, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public long ListLeftPush( - RedisKey key, - RedisValue[] values, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public long ListLeftPush( - RedisKey key, - RedisValue[] values, - CommandFlags flags) => - throw new NotImplementedException(); - - public long ListLength( - RedisKey key, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public RedisValue ListMove( - RedisKey sourceKey, - RedisKey destinationKey, - ListSide sourceSide, - ListSide destinationSide, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] - ListRange(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long ListRemove( - RedisKey key, - RedisValue value, - long count = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue ListRightPop( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] ListRightPop( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public ListPopResult ListRightPop( - RedisKey[] keys, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue ListRightPopLeftPush( - RedisKey source, - RedisKey destination, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public long ListRightPush( - RedisKey key, - RedisValue value, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public long ListRightPush( - RedisKey key, - RedisValue[] values, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public long ListRightPush( - RedisKey key, - RedisValue[] values, - CommandFlags flags) => - throw new NotImplementedException(); - - public void ListSetByIndex( - RedisKey key, - long index, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public void ListTrim( - RedisKey key, - long start, - long stop, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool LockExtend( - RedisKey key, - RedisValue value, - TimeSpan expiry, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue LockQuery( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool LockRelease( - RedisKey key, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool LockTake( - RedisKey key, - RedisValue value, - TimeSpan expiry, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long Publish( - RedisChannel channel, - RedisValue message, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisResult Execute(string command, params object[] args) => throw new NotImplementedException(); - - public RedisResult Execute( - string command, - ICollection args, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisResult ScriptEvaluate( - string script, - RedisKey[]? keys = null, - RedisValue[]? values = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisResult ScriptEvaluate( - byte[] hash, - RedisKey[]? keys = null, - RedisValue[]? values = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisResult ScriptEvaluate( - LuaScript script, - object? parameters = null, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public RedisResult ScriptEvaluate( - LoadedLuaScript script, - object? parameters = null, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public RedisResult ScriptEvaluateReadOnly( - string script, - RedisKey[]? keys = null, - RedisValue[]? values = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisResult ScriptEvaluateReadOnly( - byte[] hash, - RedisKey[]? keys = null, - RedisValue[]? values = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool SetAdd( - RedisKey key, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SetAdd( - RedisKey key, - RedisValue[] values, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SetCombine( - SetOperation operation, - RedisKey first, - RedisKey second, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SetCombine( - SetOperation operation, - RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SetCombineAndStore( - SetOperation operation, - RedisKey destination, - RedisKey first, - RedisKey second, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SetCombineAndStore( - SetOperation operation, - RedisKey destination, - RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool SetContains( - RedisKey key, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool[] SetContains( - RedisKey key, - RedisValue[] values, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SetIntersectionLength( - RedisKey[] keys, - long limit = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SetLength( - RedisKey key, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public RedisValue[] SetMembers( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool SetMove( - RedisKey source, - RedisKey destination, - RedisValue value, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public RedisValue SetPop( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SetPop( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue SetRandomMember( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SetRandomMembers( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool SetRemove( - RedisKey key, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SetRemove( - RedisKey key, - RedisValue[] values, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public IEnumerable SetScan( - RedisKey key, - RedisValue pattern, - int pageSize, - CommandFlags flags) => - throw new NotImplementedException(); - - public IEnumerable SetScan( - RedisKey key, - RedisValue pattern = default, - int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, - long cursor = RedisBase.CursorUtils.Origin, - int pageOffset = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] Sort( - RedisKey key, - long skip = 0, - long take = -1, - Order order = Order.Ascending, - SortType sortType = SortType.Numeric, - RedisValue by = default, - RedisValue[]? get = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SortAndStore( - RedisKey destination, - RedisKey key, - long skip = 0, - long take = -1, - Order order = Order.Ascending, - SortType sortType = SortType.Numeric, - RedisValue by = default, - RedisValue[]? get = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool SortedSetAdd( - RedisKey key, - RedisValue member, - double score, - CommandFlags flags) => - throw new NotImplementedException(); - - public bool SortedSetAdd( - RedisKey key, - RedisValue member, - double score, - When when, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public bool SortedSetAdd( - RedisKey key, - RedisValue member, - double score, - SortedSetWhen when = SortedSetWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SortedSetAdd( - RedisKey key, - SortedSetEntry[] values, - CommandFlags flags) => - throw new NotImplementedException(); - - public long SortedSetAdd( - RedisKey key, - SortedSetEntry[] values, - When when, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public long SortedSetAdd( - RedisKey key, - SortedSetEntry[] values, - SortedSetWhen when = SortedSetWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SortedSetCombine( - SetOperation operation, - RedisKey[] keys, - double[]? weights = null, - Aggregate aggregate = Aggregate.Sum, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public SortedSetEntry[] SortedSetCombineWithScores( - SetOperation operation, - RedisKey[] keys, - double[]? weights = null, - Aggregate aggregate = Aggregate.Sum, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SortedSetCombineAndStore( - SetOperation operation, - RedisKey destination, - RedisKey first, - RedisKey second, - Aggregate aggregate = Aggregate.Sum, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SortedSetCombineAndStore( - SetOperation operation, - RedisKey destination, - RedisKey[] keys, - double[]? weights = null, - Aggregate aggregate = Aggregate.Sum, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public double SortedSetDecrement( - RedisKey key, - RedisValue member, - double value, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public double SortedSetIncrement( - RedisKey key, - RedisValue member, - double value, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public long SortedSetIntersectionLength( - RedisKey[] keys, - long limit = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SortedSetLength( - RedisKey key, - double min = double.NegativeInfinity, - double max = double.PositiveInfinity, - Exclude exclude = Exclude.None, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SortedSetLengthByValue( - RedisKey key, - RedisValue min, - RedisValue max, - Exclude exclude = Exclude.None, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue SortedSetRandomMember( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SortedSetRandomMembers( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public SortedSetEntry[] SortedSetRandomMembersWithScores( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public RedisValue[] SortedSetRangeByRank( - RedisKey key, - long start = 0, - long stop = -1, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SortedSetRangeAndStore( - RedisKey sourceKey, - RedisKey destinationKey, - RedisValue start, - RedisValue stop, - SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, - Exclude exclude = Exclude.None, - Order order = Order.Ascending, - long skip = 0, - long? take = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public SortedSetEntry[] SortedSetRangeByRankWithScores( - RedisKey key, - long start = 0, - long stop = -1, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SortedSetRangeByScore( - RedisKey key, - double start = double.NegativeInfinity, - double stop = double.PositiveInfinity, - Exclude exclude = Exclude.None, - Order order = Order.Ascending, - long skip = 0, - long take = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public SortedSetEntry[] SortedSetRangeByScoreWithScores( - RedisKey key, - double start = double.NegativeInfinity, - double stop = double.PositiveInfinity, - Exclude exclude = Exclude.None, - Order order = Order.Ascending, - long skip = 0, - long take = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SortedSetRangeByValue( - RedisKey key, - RedisValue min, - RedisValue max, - Exclude exclude, - long skip, - long take = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SortedSetRangeByValue( - RedisKey key, - RedisValue min = default, - RedisValue max = default, - Exclude exclude = Exclude.None, - Order order = Order.Ascending, - long skip = 0, - long take = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long? SortedSetRank( - RedisKey key, - RedisValue member, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool SortedSetRemove( - RedisKey key, - RedisValue member, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SortedSetRemove( - RedisKey key, - RedisValue[] members, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SortedSetRemoveRangeByRank( - RedisKey key, - long start, - long stop, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public long SortedSetRemoveRangeByScore( - RedisKey key, - double start, - double stop, - Exclude exclude = Exclude.None, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SortedSetRemoveRangeByValue( - RedisKey key, - RedisValue min, - RedisValue max, - Exclude exclude = Exclude.None, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public IEnumerable - SortedSetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) => - throw new NotImplementedException(); - - public IEnumerable SortedSetScan( - RedisKey key, - RedisValue pattern = default, - int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, - long cursor = RedisBase.CursorUtils.Origin, - int pageOffset = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public double? SortedSetScore( - RedisKey key, - RedisValue member, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public double?[] SortedSetScores( - RedisKey key, - RedisValue[] members, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public SortedSetEntry? SortedSetPop( - RedisKey key, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public SortedSetEntry[] SortedSetPop( - RedisKey key, - long count, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public SortedSetPopResult SortedSetPop( - RedisKey[] keys, - long count, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool SortedSetUpdate( - RedisKey key, - RedisValue member, - double score, - SortedSetWhen when = SortedSetWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SortedSetUpdate( - RedisKey key, - SortedSetEntry[] values, - SortedSetWhen when = SortedSetWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StreamAcknowledge( - RedisKey key, - RedisValue groupName, - RedisValue messageId, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StreamAcknowledge( - RedisKey key, - RedisValue groupName, - RedisValue[] messageIds, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamTrimResult StreamAcknowledgeAndDelete( - RedisKey key, - RedisValue groupName, - StreamTrimMode mode, - RedisValue messageId, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamTrimResult[] StreamAcknowledgeAndDelete( - RedisKey key, - RedisValue groupName, - StreamTrimMode mode, - RedisValue[] messageIds, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue StreamAdd( - RedisKey key, - RedisValue streamField, - RedisValue streamValue, - RedisValue? messageId, - int? maxLength, - bool useApproximateMaxLength, - CommandFlags flags) => - throw new NotImplementedException(); - - public RedisValue StreamAdd( - RedisKey key, - NameValueEntry[] streamPairs, - RedisValue? messageId, - int? maxLength, - bool useApproximateMaxLength, - CommandFlags flags) => - throw new NotImplementedException(); - - public RedisValue StreamAdd( - RedisKey key, - RedisValue streamField, - RedisValue streamValue, - RedisValue? messageId = null, - long? maxLength = null, - bool useApproximateMaxLength = false, - long? limit = null, - StreamTrimMode trimMode = StreamTrimMode.KeepReferences, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue StreamAdd( - RedisKey key, - NameValueEntry[] streamPairs, - RedisValue? messageId = null, - long? maxLength = null, - bool useApproximateMaxLength = false, - long? limit = null, - StreamTrimMode trimMode = StreamTrimMode.KeepReferences, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamAutoClaimResult StreamAutoClaim( - RedisKey key, - RedisValue consumerGroup, - RedisValue claimingConsumer, - long minIdleTimeInMs, - RedisValue startAtId, - int? count = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamAutoClaimIdsOnlyResult StreamAutoClaimIdsOnly( - RedisKey key, - RedisValue consumerGroup, - RedisValue claimingConsumer, - long minIdleTimeInMs, - RedisValue startAtId, - int? count = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamEntry[] StreamClaim( - RedisKey key, - RedisValue consumerGroup, - RedisValue claimingConsumer, - long minIdleTimeInMs, - RedisValue[] messageIds, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] StreamClaimIdsOnly( - RedisKey key, - RedisValue consumerGroup, - RedisValue claimingConsumer, - long minIdleTimeInMs, - RedisValue[] messageIds, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool StreamConsumerGroupSetPosition( - RedisKey key, - RedisValue groupName, - RedisValue position, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamConsumerInfo[] StreamConsumerInfo( - RedisKey key, - RedisValue groupName, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public bool StreamCreateConsumerGroup( - RedisKey key, - RedisValue groupName, - RedisValue? position, - CommandFlags flags) => throw new NotImplementedException(); - - public bool StreamCreateConsumerGroup( - RedisKey key, - RedisValue groupName, - RedisValue? position = null, - bool createStream = true, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StreamDelete( - RedisKey key, - RedisValue[] messageIds, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamTrimResult[] StreamDelete( - RedisKey key, - RedisValue[] messageIds, - StreamTrimMode mode, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StreamDeleteConsumer( - RedisKey key, - RedisValue groupName, - RedisValue consumerName, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool StreamDeleteConsumerGroup( - RedisKey key, - RedisValue groupName, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamGroupInfo[] StreamGroupInfo( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamInfo StreamInfo( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StreamLength( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamPendingInfo StreamPending( - RedisKey key, - RedisValue groupName, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamPendingMessageInfo[] StreamPendingMessages( - RedisKey key, - RedisValue groupName, - int count, - RedisValue consumerName, - RedisValue? minId, - RedisValue? maxId, - CommandFlags flags) => - throw new NotImplementedException(); - - public StreamPendingMessageInfo[] StreamPendingMessages( - RedisKey key, - RedisValue groupName, - int count, - RedisValue consumerName, - RedisValue? minId = null, - RedisValue? maxId = null, - long? minIdleTimeInMs = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamEntry[] StreamRange( - RedisKey key, - RedisValue? minId = null, - RedisValue? maxId = null, - int? count = null, - Order messageOrder = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamEntry[] StreamRead( - RedisKey key, - RedisValue position, - int? count = null, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public RedisStream[] StreamRead( - StreamPosition[] streamPositions, - int? countPerStream = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamEntry[] StreamReadGroup( - RedisKey key, - RedisValue groupName, - RedisValue consumerName, - RedisValue? position, - int? count, - CommandFlags flags) => - throw new NotImplementedException(); - - public StreamEntry[] StreamReadGroup( - RedisKey key, - RedisValue groupName, - RedisValue consumerName, - RedisValue? position = null, - int? count = null, - bool noAck = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisStream[] StreamReadGroup( - StreamPosition[] streamPositions, - RedisValue groupName, - RedisValue consumerName, - int? countPerStream, - CommandFlags flags) => - throw new NotImplementedException(); - - public RedisStream[] StreamReadGroup( - StreamPosition[] streamPositions, - RedisValue groupName, - RedisValue consumerName, - int? countPerStream = null, - bool noAck = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StreamTrim( - RedisKey key, - int maxLength, - bool useApproximateMaxLength, - CommandFlags flags) => - throw new NotImplementedException(); - - public long StreamTrim( - RedisKey key, - long maxLength, - bool useApproximateMaxLength = false, - long? limit = null, - StreamTrimMode mode = StreamTrimMode.KeepReferences, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StreamTrimByMinId( - RedisKey key, - RedisValue minId, - bool useApproximateMaxLength = false, - long? limit = null, - StreamTrimMode mode = StreamTrimMode.KeepReferences, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StringAppend( - RedisKey key, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StringBitCount( - RedisKey key, - long start, - long end, - CommandFlags flags) => - throw new NotImplementedException(); - - public long StringBitCount( - RedisKey key, - long start = 0, - long end = -1, - StringIndexType indexType = StringIndexType.Byte, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StringBitOperation( - Bitwise operation, - RedisKey destination, - RedisKey first, - RedisKey second = default, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StringBitOperation( - Bitwise operation, - RedisKey destination, - RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StringBitPosition( - RedisKey key, - bool bit, - long start, - long end, - CommandFlags flags) => - throw new NotImplementedException(); - - public long StringBitPosition( - RedisKey key, - bool bit, - long start = 0, - long end = -1, - StringIndexType indexType = StringIndexType.Byte, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StringDecrement( - RedisKey key, - long value = 1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public double StringDecrement( - RedisKey key, - double value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue StringGet( - RedisKey key, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] StringGet(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Lease? StringGetLease(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool StringGetBit(RedisKey key, long offset, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue StringGetRange(RedisKey key, long start, long end, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue StringGetSet(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue StringGetSetExpiry(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue StringGetSetExpiry(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue StringGetDelete(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValueWithExpiry StringGetWithExpiry(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StringIncrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public double StringIncrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public string? StringLongestCommonSubsequence( - RedisKey first, - RedisKey second, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public long StringLongestCommonSubsequenceLength( - RedisKey first, - RedisKey second, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public LCSMatchResult StringLongestCommonSubsequenceWithMatches( - RedisKey first, - RedisKey second, - long minLength = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when) => - throw new NotImplementedException(); - - public bool StringSet( - RedisKey key, - RedisValue value, - TimeSpan? expiry, - When when, - CommandFlags flags) => - throw new NotImplementedException(); - - public bool StringSet( - RedisKey key, - RedisValue value, - TimeSpan? expiry = null, - bool keepTtl = false, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool StringSet( - KeyValuePair[] values, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public RedisValue StringSetAndGet( - RedisKey key, - RedisValue value, - TimeSpan? expiry, - When when, - CommandFlags flags) => - throw new NotImplementedException(); - - public RedisValue StringSetAndGet( - RedisKey key, - RedisValue value, - TimeSpan? expiry = null, - bool keepTtl = false, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool StringSetBit(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue StringSetRange( - RedisKey key, - long offset, - RedisValue value, - CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); } diff --git a/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj b/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj index c15007429..933f2390c 100644 --- a/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj +++ b/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj @@ -22,4 +22,9 @@ + + + ProxiedDatabase.cs + + From 06e4b1ec2827b78a12f10f74df9addd14e13d77b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 5 Sep 2025 11:34:10 +0100 Subject: [PATCH 056/108] method splitout --- .../ProxiedDatabase.Connection.cs | 21 +- .../ProxiedDatabase.Geo.cs | 219 ++++-- .../ProxiedDatabase.Hash.cs | 348 ++++++++-- .../ProxiedDatabase.HyperLogLog.cs | 58 ++ .../ProxiedDatabase.Key.cs | 66 +- .../ProxiedDatabase.List.cs | 270 ++++++++ .../ProxiedDatabase.Lock.cs | 40 ++ .../ProxiedDatabase.Remaining.cs | 401 ----------- .../ProxiedDatabase.Script.cs | 112 +++ .../ProxiedDatabase.Set.cs | 177 +++++ .../ProxiedDatabase.Sort.cs | 54 ++ .../ProxiedDatabase.SortedSet.cs | 538 ++++++++++++--- .../ProxiedDatabase.Stream.cs | 638 ++++++++++++++---- .../ProxiedDatabase.String.cs | 176 ++++- .../RespMultiplexer.cs | 41 +- src/RESPite.StackExchange.Redis/Utils.cs | 4 +- .../Connections/Internal/NullConnection.cs | 62 +- 17 files changed, 2392 insertions(+), 833 deletions(-) create mode 100644 src/RESPite.StackExchange.Redis/ProxiedDatabase.HyperLogLog.cs create mode 100644 src/RESPite.StackExchange.Redis/ProxiedDatabase.List.cs create mode 100644 src/RESPite.StackExchange.Redis/ProxiedDatabase.Lock.cs delete mode 100644 src/RESPite.StackExchange.Redis/ProxiedDatabase.Remaining.cs create mode 100644 src/RESPite.StackExchange.Redis/ProxiedDatabase.Script.cs create mode 100644 src/RESPite.StackExchange.Redis/ProxiedDatabase.Set.cs create mode 100644 src/RESPite.StackExchange.Redis/ProxiedDatabase.Sort.cs diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs index b839437ca..39550a6cd 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs @@ -1,5 +1,4 @@ using System.Net; -using System.Threading.Tasks; using StackExchange.Redis; namespace RESPite.StackExchange.Redis; @@ -29,10 +28,22 @@ public ITransaction CreateTransaction(object? asyncState = null) => throw new NotImplementedException(); // Key migration - public Task KeyMigrateAsync(RedisKey key, EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, MigrateOptions migrateOptions = MigrateOptions.None, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public void KeyMigrate(RedisKey key, EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, MigrateOptions migrateOptions = MigrateOptions.None, CommandFlags flags = CommandFlags.None) => + public Task KeyMigrateAsync( + RedisKey key, + EndPoint toServer, + int toDatabase = 0, + int timeoutMilliseconds = 0, + MigrateOptions migrateOptions = MigrateOptions.None, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public void KeyMigrate( + RedisKey key, + EndPoint toServer, + int toDatabase = 0, + int timeoutMilliseconds = 0, + MigrateOptions migrateOptions = MigrateOptions.None, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); // Debug diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Geo.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Geo.cs index 477355371..7bba05b00 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Geo.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Geo.cs @@ -1,12 +1,16 @@ -using System.Threading.Tasks; -using StackExchange.Redis; +using StackExchange.Redis; namespace RESPite.StackExchange.Redis; internal sealed partial class ProxiedDatabase { // Async Geo methods - public Task GeoAddAsync(RedisKey key, double longitude, double latitude, RedisValue member, CommandFlags flags = CommandFlags.None) => + public Task GeoAddAsync( + RedisKey key, + double longitude, + double latitude, + RedisValue member, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task GeoAddAsync(RedisKey key, GeoEntry value, CommandFlags flags = CommandFlags.None) => @@ -18,7 +22,12 @@ public Task GeoAddAsync(RedisKey key, GeoEntry[] values, CommandFlags flag public Task GeoRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task GeoDistanceAsync(RedisKey key, RedisValue member1, RedisValue member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None) => + public Task GeoDistanceAsync( + RedisKey key, + RedisValue member1, + RedisValue member2, + GeoUnit unit = GeoUnit.Meters, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task GeoHashAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => @@ -27,32 +36,96 @@ public Task GeoRemoveAsync(RedisKey key, RedisValue member, CommandFlags f public Task GeoHashAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task GeoPositionAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoPositionAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoRadiusAsync(RedisKey key, RedisValue member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoRadiusAsync(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoSearchAsync(RedisKey key, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoSearchAsync(RedisKey key, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoSearchAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task GeoSearchAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) => + public Task GeoPositionAsync( + RedisKey key, + RedisValue[] members, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoPositionAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoRadiusAsync( + RedisKey key, + RedisValue member, + double radius, + GeoUnit unit = GeoUnit.Meters, + int count = -1, + Order? order = null, + GeoRadiusOptions options = GeoRadiusOptions.Default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoRadiusAsync( + RedisKey key, + double longitude, + double latitude, + double radius, + GeoUnit unit = GeoUnit.Meters, + int count = -1, + Order? order = null, + GeoRadiusOptions options = GeoRadiusOptions.Default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoSearchAsync( + RedisKey key, + RedisValue member, + GeoSearchShape shape, + int count = -1, + bool demandClosest = true, + Order? order = null, + GeoRadiusOptions options = GeoRadiusOptions.Default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoSearchAsync( + RedisKey key, + double longitude, + double latitude, + GeoSearchShape shape, + int count = -1, + bool demandClosest = true, + Order? order = null, + GeoRadiusOptions options = GeoRadiusOptions.Default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoSearchAndStoreAsync( + RedisKey sourceKey, + RedisKey destinationKey, + RedisValue member, + GeoSearchShape shape, + int count = -1, + bool demandClosest = true, + Order? order = null, + bool storeDistances = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task GeoSearchAndStoreAsync( + RedisKey sourceKey, + RedisKey destinationKey, + double longitude, + double latitude, + GeoSearchShape shape, + int count = -1, + bool demandClosest = true, + Order? order = null, + bool storeDistances = false, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); // Synchronous Geo methods - public bool GeoAdd(RedisKey key, double longitude, double latitude, RedisValue member, CommandFlags flags = CommandFlags.None) => + public bool GeoAdd( + RedisKey key, + double longitude, + double latitude, + RedisValue member, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public bool GeoAdd(RedisKey key, GeoEntry value, CommandFlags flags = CommandFlags.None) => @@ -64,7 +137,12 @@ public long GeoAdd(RedisKey key, GeoEntry[] values, CommandFlags flags = Command public bool GeoRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public double? GeoDistance(RedisKey key, RedisValue member1, RedisValue member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None) => + public double? GeoDistance( + RedisKey key, + RedisValue member1, + RedisValue member2, + GeoUnit unit = GeoUnit.Meters, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public string?[] GeoHash(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => @@ -79,21 +157,74 @@ public bool GeoRemove(RedisKey key, RedisValue member, CommandFlags flags = Comm public GeoPosition? GeoPosition(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public GeoRadiusResult[] GeoRadius(RedisKey key, RedisValue member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public GeoRadiusResult[] GeoRadius(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public GeoRadiusResult[] GeoSearch(RedisKey key, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public GeoRadiusResult[] GeoSearch(RedisKey key, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long GeoSearchAndStore(RedisKey sourceKey, RedisKey destinationKey, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long GeoSearchAndStore(RedisKey sourceKey, RedisKey destinationKey, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) => + public GeoRadiusResult[] GeoRadius( + RedisKey key, + RedisValue member, + double radius, + GeoUnit unit = GeoUnit.Meters, + int count = -1, + Order? order = null, + GeoRadiusOptions options = GeoRadiusOptions.Default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public GeoRadiusResult[] GeoRadius( + RedisKey key, + double longitude, + double latitude, + double radius, + GeoUnit unit = GeoUnit.Meters, + int count = -1, + Order? order = null, + GeoRadiusOptions options = GeoRadiusOptions.Default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public GeoRadiusResult[] GeoSearch( + RedisKey key, + RedisValue member, + GeoSearchShape shape, + int count = -1, + bool demandClosest = true, + Order? order = null, + GeoRadiusOptions options = GeoRadiusOptions.Default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public GeoRadiusResult[] GeoSearch( + RedisKey key, + double longitude, + double latitude, + GeoSearchShape shape, + int count = -1, + bool demandClosest = true, + Order? order = null, + GeoRadiusOptions options = GeoRadiusOptions.Default, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long GeoSearchAndStore( + RedisKey sourceKey, + RedisKey destinationKey, + RedisValue member, + GeoSearchShape shape, + int count = -1, + bool demandClosest = true, + Order? order = null, + bool storeDistances = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long GeoSearchAndStore( + RedisKey sourceKey, + RedisKey destinationKey, + double longitude, + double latitude, + GeoSearchShape shape, + int count = -1, + bool demandClosest = true, + Order? order = null, + bool storeDistances = false, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); } diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Hash.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Hash.cs index 0dbf494b0..543173456 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Hash.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Hash.cs @@ -1,16 +1,22 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using StackExchange.Redis; +using StackExchange.Redis; namespace RESPite.StackExchange.Redis; internal sealed partial class ProxiedDatabase { // Async Hash methods - public Task HashDecrementAsync(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) => + public Task HashDecrementAsync( + RedisKey key, + RedisValue hashField, + long value = 1, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashDecrementAsync(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) => + public Task HashDecrementAsync( + RedisKey key, + RedisValue hashField, + double value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task HashDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => @@ -22,76 +28,169 @@ public Task HashDeleteAsync(RedisKey key, RedisValue[] hashFields, Command public Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + public Task HashFieldExpireAsync( + RedisKey key, + RedisValue[] hashFields, + TimeSpan expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + public Task HashFieldExpireAsync( + RedisKey key, + RedisValue[] hashFields, + DateTime expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldGetExpireDateTimeAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + public Task HashFieldGetExpireDateTimeAsync( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldPersistAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + public Task HashFieldPersistAsync( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldGetTimeToLiveAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + public Task HashFieldGetTimeToLiveAsync( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task HashGetAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task?> HashGetLeaseAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + public Task?> HashGetLeaseAsync( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashGetAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + public Task HashGetAsync( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + public Task HashFieldGetAndDeleteAsync( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task?> HashFieldGetLeaseAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + public Task?> HashFieldGetLeaseAndDeleteAsync( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + public Task HashFieldGetAndDeleteAsync( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + public Task HashFieldGetAndSetExpiryAsync( + RedisKey key, + RedisValue hashField, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) => + public Task HashFieldGetAndSetExpiryAsync( + RedisKey key, + RedisValue hashField, + DateTime expiry, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + public Task?> HashFieldGetLeaseAndSetExpiryAsync( + RedisKey key, + RedisValue hashField, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) => + public Task?> HashFieldGetLeaseAndSetExpiryAsync( + RedisKey key, + RedisValue hashField, + DateTime expiry, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + public Task HashFieldGetAndSetExpiryAsync( + RedisKey key, + RedisValue[] hashFields, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None) => + public Task HashFieldGetAndSetExpiryAsync( + RedisKey key, + RedisValue[] hashFields, + DateTime expiry, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public Task HashFieldSetAndSetExpiryAsync( + RedisKey key, + RedisValue field, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue field, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public Task HashFieldSetAndSetExpiryAsync( + RedisKey key, + RedisValue field, + RedisValue value, + DateTime expiry, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public Task HashFieldSetAndSetExpiryAsync( + RedisKey key, + HashEntry[] hashFields, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public Task HashFieldSetAndSetExpiryAsync( + RedisKey key, + HashEntry[] hashFields, + DateTime expiry, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task HashGetAllAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashIncrementAsync(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) => + public Task HashIncrementAsync( + RedisKey key, + RedisValue hashField, + long value = 1, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashIncrementAsync(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) => + public Task HashIncrementAsync( + RedisKey key, + RedisValue hashField, + double value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task HashKeysAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => @@ -106,32 +205,63 @@ public Task HashRandomFieldAsync(RedisKey key, CommandFlags flags = public Task HashRandomFieldsAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashRandomFieldsWithValuesAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + public Task HashRandomFieldsWithValuesAsync( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public IAsyncEnumerable HashScanAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None) => + public IAsyncEnumerable HashScanAsync( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public IAsyncEnumerable HashScanNoValuesAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None) => + public IAsyncEnumerable HashScanNoValuesAsync( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task HashSetAsync(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashSetAsync(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public Task HashSetAsync( + RedisKey key, + RedisValue hashField, + RedisValue value, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashStringLengthAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + public Task HashStringLengthAsync( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task HashValuesAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); // Synchronous Hash methods - public long HashDecrement(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) => + public long HashDecrement( + RedisKey key, + RedisValue hashField, + long value = 1, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public double HashDecrement(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) => + public double HashDecrement( + RedisKey key, + RedisValue hashField, + double value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public bool HashDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => @@ -143,19 +273,38 @@ public long HashDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags public bool HashExists(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + public ExpireResult[] HashFieldExpire( + RedisKey key, + RedisValue[] hashFields, + TimeSpan expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + public ExpireResult[] HashFieldExpire( + RedisKey key, + RedisValue[] hashFields, + DateTime expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long[] HashFieldGetExpireDateTime(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + public long[] HashFieldGetExpireDateTime( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public PersistResult[] HashFieldPersist(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + public PersistResult[] HashFieldPersist( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long[] HashFieldGetTimeToLive(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + public long[] HashFieldGetTimeToLive( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public RedisValue HashGet(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => @@ -167,52 +316,120 @@ public RedisValue HashGet(RedisKey key, RedisValue hashField, CommandFlags flags public RedisValue[] HashGet(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue HashFieldGetAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + public RedisValue HashFieldGetAndDelete( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Lease? HashFieldGetLeaseAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + public Lease? HashFieldGetLeaseAndDelete( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue[] HashFieldGetAndDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + public RedisValue[] HashFieldGetAndDelete( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + public RedisValue HashFieldGetAndSetExpiry( + RedisKey key, + RedisValue hashField, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) => + public RedisValue HashFieldGetAndSetExpiry( + RedisKey key, + RedisValue hashField, + DateTime expiry, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + public Lease? HashFieldGetLeaseAndSetExpiry( + RedisKey key, + RedisValue hashField, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) => + public Lease? HashFieldGetLeaseAndSetExpiry( + RedisKey key, + RedisValue hashField, + DateTime expiry, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + public RedisValue[] HashFieldGetAndSetExpiry( + RedisKey key, + RedisValue[] hashFields, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None) => + public RedisValue[] HashFieldGetAndSetExpiry( + RedisKey key, + RedisValue[] hashFields, + DateTime expiry, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public RedisValue HashFieldSetAndSetExpiry( + RedisKey key, + RedisValue field, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public RedisValue HashFieldSetAndSetExpiry( + RedisKey key, + RedisValue field, + RedisValue value, + DateTime expiry, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public RedisValue HashFieldSetAndSetExpiry( + RedisKey key, + HashEntry[] hashFields, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public RedisValue HashFieldSetAndSetExpiry( + RedisKey key, + HashEntry[] hashFields, + DateTime expiry, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public HashEntry[] HashGetAll(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long HashIncrement(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) => + public long HashIncrement( + RedisKey key, + RedisValue hashField, + long value = 1, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public double HashIncrement(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) => + public double HashIncrement( + RedisKey key, + RedisValue hashField, + double value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public RedisValue[] HashKeys(RedisKey key, CommandFlags flags = CommandFlags.None) => @@ -233,16 +450,33 @@ public HashEntry[] HashRandomFieldsWithValues(RedisKey key, long count, CommandF public IEnumerable HashScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) => throw new NotImplementedException(); - public IEnumerable HashScan(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None) => + public IEnumerable HashScan( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public IEnumerable HashScanNoValues(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None) => + public IEnumerable HashScanNoValues( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public void HashSet(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public bool HashSet(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public bool HashSet( + RedisKey key, + RedisValue hashField, + RedisValue value, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public long HashStringLength(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.HyperLogLog.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.HyperLogLog.cs new file mode 100644 index 000000000..2fbb8746d --- /dev/null +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.HyperLogLog.cs @@ -0,0 +1,58 @@ +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal sealed partial class ProxiedDatabase +{ + // Async HyperLogLog methods + public Task HyperLogLogAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HyperLogLogAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HyperLogLogLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HyperLogLogLengthAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HyperLogLogMergeAsync( + RedisKey destination, + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task HyperLogLogMergeAsync( + RedisKey destination, + RedisKey[] sourceKeys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // Synchronous HyperLogLog methods + public bool HyperLogLogAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool HyperLogLogAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long HyperLogLogLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long HyperLogLogLength(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public void HyperLogLogMerge( + RedisKey destination, + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public void HyperLogLogMerge( + RedisKey destination, + RedisKey[] sourceKeys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); +} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs index 497e1db27..8726e0501 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs @@ -1,13 +1,16 @@ -using System.Net; -using System.Threading.Tasks; -using StackExchange.Redis; +using StackExchange.Redis; namespace RESPite.StackExchange.Redis; internal sealed partial class ProxiedDatabase { // Async Key methods - public Task KeyCopyAsync(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None) => + public Task KeyCopyAsync( + RedisKey sourceKey, + RedisKey destinationKey, + int destinationDatabase = -1, + bool replace = false, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task KeyDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => @@ -31,13 +34,21 @@ public Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFl public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags) => throw new NotImplementedException(); - public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + public Task KeyExpireAsync( + RedisKey key, + TimeSpan? expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags) => throw new NotImplementedException(); - public Task KeyExpireAsync(RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + public Task KeyExpireAsync( + RedisKey key, + DateTime? expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task KeyExpireTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => @@ -61,10 +72,18 @@ public Task KeyRandomAsync(CommandFlags flags = CommandFlags.None) => public Task KeyRefCountAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task KeyRenameAsync(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public Task KeyRenameAsync( + RedisKey key, + RedisKey newKey, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task KeyRestoreAsync(RedisKey key, byte[] value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) => + public Task KeyRestoreAsync( + RedisKey key, + byte[] value, + TimeSpan? expiry = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task KeyTimeToLiveAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => @@ -80,7 +99,12 @@ public Task KeyTypeAsync(RedisKey key, CommandFlags flags = CommandFl throw new NotImplementedException(); // Synchronous Key methods - public bool KeyCopy(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None) => + public bool KeyCopy( + RedisKey sourceKey, + RedisKey destinationKey, + int destinationDatabase = -1, + bool replace = false, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None) => @@ -104,13 +128,21 @@ public long KeyExists(RedisKey[] keys, CommandFlags flags = CommandFlags.None) = public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags) => throw new NotImplementedException(); - public bool KeyExpire(RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + public bool KeyExpire( + RedisKey key, + TimeSpan? expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags) => throw new NotImplementedException(); - public bool KeyExpire(RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + public bool KeyExpire( + RedisKey key, + DateTime? expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public DateTime? KeyExpireTime(RedisKey key, CommandFlags flags = CommandFlags.None) => @@ -134,10 +166,18 @@ public RedisKey KeyRandom(CommandFlags flags = CommandFlags.None) => public long? KeyRefCount(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public bool KeyRename(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public bool KeyRename( + RedisKey key, + RedisKey newKey, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public void KeyRestore(RedisKey key, byte[] value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) => + public void KeyRestore( + RedisKey key, + byte[] value, + TimeSpan? expiry = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public TimeSpan? KeyTimeToLive(RedisKey key, CommandFlags flags = CommandFlags.None) => diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.List.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.List.cs new file mode 100644 index 000000000..b65fb94b6 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.List.cs @@ -0,0 +1,270 @@ +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal sealed partial class ProxiedDatabase +{ + // Async List methods + public Task ListGetByIndexAsync(RedisKey key, long index, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListInsertAfterAsync( + RedisKey key, + RedisValue pivot, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListInsertBeforeAsync( + RedisKey key, + RedisValue pivot, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLeftPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLeftPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListPositionAsync( + RedisKey key, + RedisValue element, + long rank = 1, + long maxLength = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListPositionsAsync( + RedisKey key, + RedisValue element, + long count, + long rank = 1, + long maxLength = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLeftPushAsync( + RedisKey key, + RedisValue value, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLeftPushAsync( + RedisKey key, + RedisValue[] values, + When when, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLeftPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListMoveAsync( + RedisKey sourceKey, + RedisKey destinationKey, + ListSide sourceSide, + ListSide destinationSide, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRangeAsync( + RedisKey key, + long start = 0, + long stop = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRemoveAsync( + RedisKey key, + RedisValue value, + long count = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPopLeftPushAsync( + RedisKey source, + RedisKey destination, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPushAsync( + RedisKey key, + RedisValue value, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPushAsync( + RedisKey key, + RedisValue[] values, + When when, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListRightPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListSetByIndexAsync( + RedisKey key, + long index, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ListTrimAsync( + RedisKey key, + long start, + long stop, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // Synchronous List methods + public RedisValue ListGetByIndex(RedisKey key, long index, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListInsertAfter( + RedisKey key, + RedisValue pivot, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListInsertBefore( + RedisKey key, + RedisValue pivot, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public ListPopResult ListLeftPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListPosition( + RedisKey key, + RedisValue element, + long rank = 1, + long maxLength = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long[] ListPositions( + RedisKey key, + RedisValue element, + long count, + long rank = 1, + long maxLength = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListLeftPush( + RedisKey key, + RedisValue value, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListLeftPush( + RedisKey key, + RedisValue[] values, + When when, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListLeftPush(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue ListMove( + RedisKey sourceKey, + RedisKey destinationKey, + ListSide sourceSide, + ListSide destinationSide, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] ListRange( + RedisKey key, + long start = 0, + long stop = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListRemove( + RedisKey key, + RedisValue value, + long count = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public ListPopResult ListRightPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue ListRightPopLeftPush( + RedisKey source, + RedisKey destination, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListRightPush( + RedisKey key, + RedisValue value, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListRightPush( + RedisKey key, + RedisValue[] values, + When when, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long ListRightPush(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public void ListSetByIndex( + RedisKey key, + long index, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public void ListTrim( + RedisKey key, + long start, + long stop, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); +} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Lock.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Lock.cs new file mode 100644 index 000000000..63d51ab1f --- /dev/null +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Lock.cs @@ -0,0 +1,40 @@ +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal sealed partial class ProxiedDatabase +{ + // Async Lock methods + public Task LockExtendAsync( + RedisKey key, + RedisValue value, + TimeSpan expiry, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task LockQueryAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task LockReleaseAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task LockTakeAsync( + RedisKey key, + RedisValue value, + TimeSpan expiry, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // Synchronous Lock methods + public bool LockExtend(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue LockQuery(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool LockRelease(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool LockTake(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); +} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Remaining.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Remaining.cs deleted file mode 100644 index 4a373e5bc..000000000 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Remaining.cs +++ /dev/null @@ -1,401 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using StackExchange.Redis; - -namespace RESPite.StackExchange.Redis; - -internal sealed partial class ProxiedDatabase -{ - // HyperLogLog methods - public Task HyperLogLogAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HyperLogLogAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HyperLogLogLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HyperLogLogLengthAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HyperLogLogMergeAsync(RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HyperLogLogMergeAsync(RedisKey destination, RedisKey[] sourceKeys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool HyperLogLogAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool HyperLogLogAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long HyperLogLogLength(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long HyperLogLogLength(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public void HyperLogLogMerge(RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public void HyperLogLogMerge(RedisKey destination, RedisKey[] sourceKeys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - // List methods - public Task ListGetByIndexAsync(RedisKey key, long index, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListInsertAfterAsync(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListInsertBeforeAsync(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListLeftPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListLeftPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListPositionAsync(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListPositionsAsync(RedisKey key, RedisValue element, long count, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListLeftPushAsync(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListLeftPushAsync(RedisKey key, RedisValue[] values, When when, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListLeftPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListMoveAsync(RedisKey sourceKey, RedisKey destinationKey, ListSide sourceSide, ListSide destinationSide, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListRangeAsync(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListRemoveAsync(RedisKey key, RedisValue value, long count = 0, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListRightPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListRightPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListRightPopLeftPushAsync(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListRightPushAsync(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListRightPushAsync(RedisKey key, RedisValue[] values, When when, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListRightPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListSetByIndexAsync(RedisKey key, long index, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListTrimAsync(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue ListGetByIndex(RedisKey key, long index, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long ListInsertAfter(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long ListInsertBefore(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public ListPopResult ListLeftPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long ListPosition(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long[] ListPositions(RedisKey key, RedisValue element, long count, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long ListLeftPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long ListLeftPush(RedisKey key, RedisValue[] values, When when, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long ListLeftPush(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long ListLength(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue ListMove(RedisKey sourceKey, RedisKey destinationKey, ListSide sourceSide, ListSide destinationSide, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] ListRange(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long ListRemove(RedisKey key, RedisValue value, long count = 0, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public ListPopResult ListRightPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue ListRightPopLeftPush(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long ListRightPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long ListRightPush(RedisKey key, RedisValue[] values, When when, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long ListRightPush(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public void ListSetByIndex(RedisKey key, long index, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public void ListTrim(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - // Lock methods - public Task LockExtendAsync(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task LockQueryAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task LockReleaseAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task LockTakeAsync(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool LockExtend(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue LockQuery(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool LockRelease(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool LockTake(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - // Script/Execute/Publish methods - public Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ExecuteAsync(string command, params object[] args) => - throw new NotImplementedException(); - - public Task ExecuteAsync(string command, ICollection? args, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ScriptEvaluateAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ScriptEvaluateAsync(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ScriptEvaluateAsync(LoadedLuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ScriptEvaluateReadOnlyAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ScriptEvaluateReadOnlyAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisResult Execute(string command, params object[] args) => - throw new NotImplementedException(); - - public RedisResult Execute(string command, ICollection? args, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisResult ScriptEvaluate(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisResult ScriptEvaluate(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisResult ScriptEvaluate(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisResult ScriptEvaluate(LoadedLuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisResult ScriptEvaluateReadOnly(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisResult ScriptEvaluateReadOnly(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - // Set methods - public Task SetAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetCombineAsync(SetOperation operation, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetCombineAsync(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetContainsAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetMembersAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetMoveAsync(RedisKey source, RedisKey destination, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetRemoveAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetRemoveAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public IAsyncEnumerable SetScanAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool SetAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SetAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SetCombine(SetOperation operation, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SetCombine(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool SetContains(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool[] SetContains(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SetLength(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SetMembers(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool SetMove(RedisKey source, RedisKey destination, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue SetPop(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SetPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue SetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool SetRemove(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SetRemove(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public IEnumerable SetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) => - throw new NotImplementedException(); - - public IEnumerable SetScan(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - // Sort methods - public Task SortAsync(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortAndStoreAsync(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SortAndStore(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); -} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Script.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Script.cs new file mode 100644 index 000000000..208105405 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Script.cs @@ -0,0 +1,112 @@ +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal sealed partial class ProxiedDatabase +{ + // Async Script/Execute/Publish methods + public Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ExecuteAsync(string command, params object[] args) => + throw new NotImplementedException(); + + public Task ExecuteAsync( + string command, + ICollection? args, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ScriptEvaluateAsync( + string script, + RedisKey[]? keys = null, + RedisValue[]? values = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ScriptEvaluateAsync( + byte[] hash, + RedisKey[]? keys = null, + RedisValue[]? values = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ScriptEvaluateAsync( + LuaScript script, + object? parameters = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ScriptEvaluateAsync( + LoadedLuaScript script, + object? parameters = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ScriptEvaluateReadOnlyAsync( + string script, + RedisKey[]? keys = null, + RedisValue[]? values = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ScriptEvaluateReadOnlyAsync( + byte[] hash, + RedisKey[]? keys = null, + RedisValue[]? values = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // Synchronous Script/Execute/Publish methods + public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult Execute(string command, params object[] args) => + throw new NotImplementedException(); + + public RedisResult Execute( + string command, + ICollection? args, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult ScriptEvaluate( + string script, + RedisKey[]? keys = null, + RedisValue[]? values = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult ScriptEvaluate( + byte[] hash, + RedisKey[]? keys = null, + RedisValue[]? values = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult ScriptEvaluate( + LuaScript script, + object? parameters = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult ScriptEvaluate( + LoadedLuaScript script, + object? parameters = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult ScriptEvaluateReadOnly( + string script, + RedisKey[]? keys = null, + RedisValue[]? values = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisResult ScriptEvaluateReadOnly( + byte[] hash, + RedisKey[]? keys = null, + RedisValue[]? values = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); +} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Set.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Set.cs new file mode 100644 index 000000000..3fb5423f2 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Set.cs @@ -0,0 +1,177 @@ +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal sealed partial class ProxiedDatabase +{ + // Async Set methods + public Task SetAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetCombineAsync( + SetOperation operation, + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetCombineAsync( + SetOperation operation, + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetCombineAndStoreAsync( + SetOperation operation, + RedisKey destination, + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetCombineAndStoreAsync( + SetOperation operation, + RedisKey destination, + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetContainsAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetIntersectionLengthAsync( + RedisKey[] keys, + long limit = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetMembersAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetMoveAsync( + RedisKey source, + RedisKey destination, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetRemoveAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SetRemoveAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IAsyncEnumerable SetScanAsync( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // Synchronous Set methods + public bool SetAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SetCombine( + SetOperation operation, + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SetCombine(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetCombineAndStore( + SetOperation operation, + RedisKey destination, + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetCombineAndStore( + SetOperation operation, + RedisKey destination, + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SetContains(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool[] SetContains(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SetMembers(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SetMove( + RedisKey source, + RedisKey destination, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue SetPop(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SetPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue SetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool SetRemove(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SetRemove(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public IEnumerable SetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) => + throw new NotImplementedException(); + + public IEnumerable SetScan( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); +} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Sort.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Sort.cs new file mode 100644 index 000000000..54497ca21 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Sort.cs @@ -0,0 +1,54 @@ +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal sealed partial class ProxiedDatabase +{ + // Async Sort methods + public Task SortAsync( + RedisKey key, + long skip = 0, + long take = -1, + Order order = Order.Ascending, + SortType sortType = SortType.Numeric, + RedisValue by = default, + RedisValue[]? get = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortAndStoreAsync( + RedisKey destination, + RedisKey key, + long skip = 0, + long take = -1, + Order order = Order.Ascending, + SortType sortType = SortType.Numeric, + RedisValue by = default, + RedisValue[]? get = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + // Synchronous Sort methods + public RedisValue[] Sort( + RedisKey key, + long skip = 0, + long take = -1, + Order order = Order.Ascending, + SortType sortType = SortType.Numeric, + RedisValue by = default, + RedisValue[]? get = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortAndStore( + RedisKey destination, + RedisKey key, + long skip = 0, + long take = -1, + Order order = Order.Ascending, + SortType sortType = SortType.Numeric, + RedisValue by = default, + RedisValue[]? get = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); +} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.SortedSet.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.SortedSet.cs index 9201a621d..6f4652aa7 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.SortedSet.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.SortedSet.cs @@ -1,173 +1,404 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using StackExchange.Redis; +using StackExchange.Redis; namespace RESPite.StackExchange.Redis; internal sealed partial class ProxiedDatabase { // Async SortedSet methods - public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, CommandFlags flags = CommandFlags.None) => + public Task SortedSetAddAsync( + RedisKey key, + RedisValue member, + double score, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, When when, CommandFlags flags = CommandFlags.None) => + public Task SortedSetAddAsync( + RedisKey key, + RedisValue member, + double score, + When when, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + public Task SortedSetAddAsync( + RedisKey key, + RedisValue member, + double score, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, CommandFlags flags = CommandFlags.None) => + public Task SortedSetAddAsync( + RedisKey key, + SortedSetEntry[] values, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, When when, CommandFlags flags = CommandFlags.None) => + public Task SortedSetAddAsync( + RedisKey key, + SortedSetEntry[] values, + When when, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + public Task SortedSetAddAsync( + RedisKey key, + SortedSetEntry[] values, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetCombineAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + public Task SortedSetCombineAsync( + SetOperation operation, + RedisKey[] keys, + double[]? weights = null, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetCombineWithScoresAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + public Task SortedSetCombineWithScoresAsync( + SetOperation operation, + RedisKey[] keys, + double[]? weights = null, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + public Task SortedSetCombineAndStoreAsync( + SetOperation operation, + RedisKey destination, + RedisKey first, + RedisKey second, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + public Task SortedSetCombineAndStoreAsync( + SetOperation operation, + RedisKey destination, + RedisKey[] keys, + double[]? weights = null, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetDecrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => + public Task SortedSetDecrementAsync( + RedisKey key, + RedisValue member, + double value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => + public Task SortedSetIncrementAsync( + RedisKey key, + RedisValue member, + double value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => + public Task SortedSetIntersectionLengthAsync( + RedisKey[] keys, + long limit = 0, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetLengthAsync(RedisKey key, double min = double.NegativeInfinity, double max = double.PositiveInfinity, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + public Task SortedSetLengthAsync( + RedisKey key, + double min = double.NegativeInfinity, + double max = double.PositiveInfinity, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetLengthByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + public Task SortedSetLengthByValueAsync( + RedisKey key, + RedisValue min, + RedisValue max, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task SortedSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRandomMembersWithScoresAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeByRankAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, RedisValue start, RedisValue stop, SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long? take = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeByRankWithScoresAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeByScoreAsync(RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeByScoreWithScoresAsync(RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeByValueAsync(RedisKey key, RedisValue min = default, RedisValue max = default, Exclude exclude = Exclude.None, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude, Order order, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRankAsync(RedisKey key, RedisValue member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + public Task SortedSetRandomMembersAsync( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRandomMembersWithScoresAsync( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeByRankAsync( + RedisKey key, + long start = 0, + long stop = -1, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeAndStoreAsync( + RedisKey sourceKey, + RedisKey destinationKey, + RedisValue start, + RedisValue stop, + SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long? take = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeByRankWithScoresAsync( + RedisKey key, + long start = 0, + long stop = -1, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeByScoreAsync( + RedisKey key, + double start = double.NegativeInfinity, + double stop = double.PositiveInfinity, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeByScoreWithScoresAsync( + RedisKey key, + double start = double.NegativeInfinity, + double stop = double.PositiveInfinity, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeByValueAsync( + RedisKey key, + RedisValue min = default, + RedisValue max = default, + Exclude exclude = Exclude.None, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRangeByValueAsync( + RedisKey key, + RedisValue min, + RedisValue max, + Exclude exclude, + Order order, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task SortedSetRankAsync( + RedisKey key, + RedisValue member, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task SortedSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetRemoveAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + public Task SortedSetRemoveAsync( + RedisKey key, + RedisValue[] members, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetRemoveRangeByRankAsync(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) => + public Task SortedSetRemoveRangeByRankAsync( + RedisKey key, + long start, + long stop, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetRemoveRangeByScoreAsync(RedisKey key, double start, double stop, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + public Task SortedSetRemoveRangeByScoreAsync( + RedisKey key, + double start, + double stop, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetRemoveRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + public Task SortedSetRemoveRangeByValueAsync( + RedisKey key, + RedisValue min, + RedisValue max, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public IAsyncEnumerable SortedSetScanAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None) => + public IAsyncEnumerable SortedSetScanAsync( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task SortedSetScoreAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetScoresAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + public Task SortedSetScoresAsync( + RedisKey key, + RedisValue[] members, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetPopAsync(RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + public Task SortedSetPopAsync( + RedisKey key, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetPopAsync(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + public Task SortedSetPopAsync( + RedisKey key, + long count, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetPopAsync(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + public Task SortedSetPopAsync( + RedisKey[] keys, + long count, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetUpdateAsync(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + public Task SortedSetUpdateAsync( + RedisKey key, + RedisValue member, + double score, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SortedSetUpdateAsync(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + public Task SortedSetUpdateAsync( + RedisKey key, + SortedSetEntry[] values, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); // Synchronous SortedSet methods public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when, CommandFlags flags = CommandFlags.None) => + public bool SortedSetAdd( + RedisKey key, + RedisValue member, + double score, + When when, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public bool SortedSetAdd(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + public bool SortedSetAdd( + RedisKey key, + RedisValue member, + double score, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, When when, CommandFlags flags = CommandFlags.None) => + public long SortedSetAdd( + RedisKey key, + SortedSetEntry[] values, + When when, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + public long SortedSetAdd( + RedisKey key, + SortedSetEntry[] values, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue[] SortedSetCombine(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + public RedisValue[] SortedSetCombine( + SetOperation operation, + RedisKey[] keys, + double[]? weights = null, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public SortedSetEntry[] SortedSetCombineWithScores(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + public SortedSetEntry[] SortedSetCombineWithScores( + SetOperation operation, + RedisKey[] keys, + double[]? weights = null, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + public long SortedSetCombineAndStore( + SetOperation operation, + RedisKey destination, + RedisKey first, + RedisKey second, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + public long SortedSetCombineAndStore( + SetOperation operation, + RedisKey destination, + RedisKey[] keys, + double[]? weights = null, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public double SortedSetDecrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => + public double SortedSetDecrement( + RedisKey key, + RedisValue member, + double value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public double SortedSetIncrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => + public double SortedSetIncrement( + RedisKey key, + RedisValue member, + double value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long SortedSetLength(RedisKey key, double min = double.NegativeInfinity, double max = double.PositiveInfinity, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + public long SortedSetLength( + RedisKey key, + double min = double.NegativeInfinity, + double max = double.PositiveInfinity, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long SortedSetLengthByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + public long SortedSetLengthByValue( + RedisKey key, + RedisValue min, + RedisValue max, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public RedisValue SortedSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) => @@ -176,31 +407,89 @@ public RedisValue SortedSetRandomMember(RedisKey key, CommandFlags flags = Comma public RedisValue[] SortedSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public SortedSetEntry[] SortedSetRandomMembersWithScores(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SortedSetRangeByRank(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long SortedSetRangeAndStore(RedisKey sourceKey, RedisKey destinationKey, RedisValue start, RedisValue stop, SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long? take = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public SortedSetEntry[] SortedSetRangeByRankWithScores(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SortedSetRangeByScore(RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public SortedSetEntry[] SortedSetRangeByScoreWithScores(RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SortedSetRangeByValue(RedisKey key, RedisValue min = default, RedisValue max = default, Exclude exclude = Exclude.None, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SortedSetRangeByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude, Order order, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long? SortedSetRank(RedisKey key, RedisValue member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + public SortedSetEntry[] SortedSetRandomMembersWithScores( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SortedSetRangeByRank( + RedisKey key, + long start = 0, + long stop = -1, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long SortedSetRangeAndStore( + RedisKey sourceKey, + RedisKey destinationKey, + RedisValue start, + RedisValue stop, + SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long? take = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public SortedSetEntry[] SortedSetRangeByRankWithScores( + RedisKey key, + long start = 0, + long stop = -1, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SortedSetRangeByScore( + RedisKey key, + double start = double.NegativeInfinity, + double stop = double.PositiveInfinity, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public SortedSetEntry[] SortedSetRangeByScoreWithScores( + RedisKey key, + double start = double.NegativeInfinity, + double stop = double.PositiveInfinity, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SortedSetRangeByValue( + RedisKey key, + RedisValue min = default, + RedisValue max = default, + Exclude exclude = Exclude.None, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] SortedSetRangeByValue( + RedisKey key, + RedisValue min, + RedisValue max, + Exclude exclude, + Order order, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long? SortedSetRank( + RedisKey key, + RedisValue member, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public bool SortedSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => @@ -209,19 +498,40 @@ public bool SortedSetRemove(RedisKey key, RedisValue member, CommandFlags flags public long SortedSetRemove(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long SortedSetRemoveRangeByRank(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) => + public long SortedSetRemoveRangeByRank( + RedisKey key, + long start, + long stop, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long SortedSetRemoveRangeByScore(RedisKey key, double start, double stop, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + public long SortedSetRemoveRangeByScore( + RedisKey key, + double start, + double stop, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + public long SortedSetRemoveRangeByValue( + RedisKey key, + RedisValue min, + RedisValue max, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public IEnumerable SortedSetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) => + public IEnumerable + SortedSetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) => throw new NotImplementedException(); - public IEnumerable SortedSetScan(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None) => + public IEnumerable SortedSetScan( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public double? SortedSetScore(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => @@ -230,18 +540,38 @@ public IEnumerable SortedSetScan(RedisKey key, RedisValue patter public double?[] SortedSetScores(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public SortedSetEntry? SortedSetPop(RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + public SortedSetEntry? SortedSetPop( + RedisKey key, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public SortedSetEntry[] SortedSetPop(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + public SortedSetEntry[] SortedSetPop( + RedisKey key, + long count, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public SortedSetPopResult SortedSetPop(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + public SortedSetPopResult SortedSetPop( + RedisKey[] keys, + long count, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public bool SortedSetUpdate(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + public bool SortedSetUpdate( + RedisKey key, + RedisValue member, + double score, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long SortedSetUpdate(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + public long SortedSetUpdate( + RedisKey key, + SortedSetEntry[] values, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); } diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Stream.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Stream.cs index 607f28a30..a5673ed9b 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Stream.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Stream.cs @@ -1,70 +1,172 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using StackExchange.Redis; +using StackExchange.Redis; namespace RESPite.StackExchange.Redis; internal sealed partial class ProxiedDatabase { // Async Stream methods - public Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task StreamAcknowledgeAsync( + RedisKey key, + RedisValue groupName, + RedisValue messageId, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAcknowledgeAsync( + RedisKey key, + RedisValue groupName, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAcknowledgeAndDeleteAsync( + RedisKey key, + RedisValue groupName, + StreamTrimMode trimMode, + RedisValue messageId, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAcknowledgeAndDeleteAsync( + RedisKey key, + RedisValue groupName, + StreamTrimMode trimMode, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAddAsync( + RedisKey key, + RedisValue streamField, + RedisValue streamValue, + RedisValue? messageId = null, + int? maxLength = null, + bool useApproximateMaxLength = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAddAsync( + RedisKey key, + NameValueEntry[] streamPairs, + RedisValue? messageId = null, + int? maxLength = null, + bool useApproximateMaxLength = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAddAsync( + RedisKey key, + RedisValue streamField, + RedisValue streamValue, + RedisValue? messageId = null, + long? maxLength = null, + bool useApproximateMaxLength = false, + long? limit = null, + StreamTrimMode trimMode = StreamTrimMode.KeepReferences, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAddAsync( + RedisKey key, + NameValueEntry[] streamPairs, + RedisValue? messageId = null, + long? maxLength = null, + bool useApproximateMaxLength = false, + long? limit = null, + StreamTrimMode trimMode = StreamTrimMode.KeepReferences, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAutoClaimAsync( + RedisKey key, + RedisValue consumerGroup, + RedisValue claimingConsumer, + long minIdleTimeInMs, + RedisValue startAtId, + int? count = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamAutoClaimIdsOnlyAsync( + RedisKey key, + RedisValue consumerGroup, + RedisValue claimingConsumer, + long minIdleTimeInMs, + RedisValue startAtId, + int? count = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamClaimAsync( + RedisKey key, + RedisValue consumerGroup, + RedisValue claimingConsumer, + long minIdleTimeInMs, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamClaimIdsOnlyAsync( + RedisKey key, + RedisValue consumerGroup, + RedisValue claimingConsumer, + long minIdleTimeInMs, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); - public Task StreamAcknowledgeAndDeleteAsync(RedisKey key, RedisValue groupName, StreamTrimMode trimMode, RedisValue messageId, CommandFlags flags = CommandFlags.None) => + public Task StreamConsumerGroupSetPositionAsync( + RedisKey key, + RedisValue groupName, + RedisValue position, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task StreamConsumerInfoAsync( + RedisKey key, + RedisValue groupName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task StreamAcknowledgeAndDeleteAsync(RedisKey key, RedisValue groupName, StreamTrimMode trimMode, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + + public Task StreamCreateConsumerGroupAsync( + RedisKey key, + RedisValue groupName, + RedisValue? position = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + public Task StreamCreateConsumerGroupAsync( + RedisKey key, + RedisValue groupName, + RedisValue? position = null, + bool createStream = true, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + public Task StreamDeleteAsync( + RedisKey key, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + public Task StreamDeleteAsync( + RedisKey key, + RedisValue[] messageIds, + StreamTrimMode trimMode, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StreamAutoClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) => + public Task StreamDeleteConsumerAsync( + RedisKey key, + RedisValue groupName, + RedisValue consumerName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StreamAutoClaimIdsOnlyAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamClaimIdsOnlyAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamConsumerGroupSetPositionAsync(RedisKey key, RedisValue groupName, RedisValue position, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamConsumerInfoAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamCreateConsumerGroupAsync(RedisKey key, RedisValue groupName, RedisValue? position = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamCreateConsumerGroupAsync(RedisKey key, RedisValue groupName, RedisValue? position = null, bool createStream = true, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, StreamTrimMode trimMode, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamDeleteConsumerAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StreamDeleteConsumerGroupAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => + public Task StreamDeleteConsumerGroupAsync( + RedisKey key, + RedisValue groupName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task StreamGroupInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => @@ -76,101 +178,270 @@ public Task StreamInfoAsync(RedisKey key, CommandFlags flags = Comma public Task StreamLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StreamPendingAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => + public Task StreamPendingAsync( + RedisKey key, + RedisValue groupName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None) => + + public Task StreamPendingMessagesAsync( + RedisKey key, + RedisValue groupName, + int count, + RedisValue consumerName, + RedisValue? minId = null, + RedisValue? maxId = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, long? idleTime = null, CommandFlags flags = CommandFlags.None) => + public Task StreamPendingMessagesAsync( + RedisKey key, + RedisValue groupName, + int count, + RedisValue consumerName, + RedisValue? minId = null, + RedisValue? maxId = null, + long? idleTime = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StreamRangeAsync(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None) => + public Task StreamRangeAsync( + RedisKey key, + RedisValue? minId = null, + RedisValue? maxId = null, + int? count = null, + Order messageOrder = Order.Ascending, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StreamReadAsync(RedisKey key, RedisValue position, int? count = null, CommandFlags flags = CommandFlags.None) => + public Task StreamReadAsync( + RedisKey key, + RedisValue position, + int? count = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StreamReadAsync(StreamPosition[] streamPositions, int? countPerStream = null, CommandFlags flags = CommandFlags.None) => + public Task StreamReadAsync( + StreamPosition[] streamPositions, + int? countPerStream = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, CommandFlags flags = CommandFlags.None) => + public Task StreamReadGroupAsync( + RedisKey key, + RedisValue groupName, + RedisValue consumerName, + RedisValue? position = null, + int? count = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => + public Task StreamReadGroupAsync( + RedisKey key, + RedisValue groupName, + RedisValue consumerName, + RedisValue? position = null, + int? count = null, + bool noAck = false, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, CommandFlags flags = CommandFlags.None) => + public Task StreamReadGroupAsync( + StreamPosition[] streamPositions, + RedisValue groupName, + RedisValue consumerName, + int? countPerStream = null, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => + public Task StreamReadGroupAsync( + StreamPosition[] streamPositions, + RedisValue groupName, + RedisValue consumerName, + int? countPerStream = null, + bool noAck = false, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StreamTrimAsync(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + public Task StreamTrimAsync( + RedisKey key, + int maxLength, + bool useApproximateMaxLength = false, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StreamTrimAsync(RedisKey key, long maxLength, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + public Task StreamTrimAsync( + RedisKey key, + long maxLength, + bool useApproximateMaxLength = false, + long? limit = null, + StreamTrimMode trimMode = StreamTrimMode.KeepReferences, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public Task StreamTrimByMinIdAsync(RedisKey key, RedisValue minId, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + + public Task StreamTrimByMinIdAsync( + RedisKey key, + RedisValue minId, + bool useApproximateMaxLength = false, + long? limit = null, + StreamTrimMode trimMode = StreamTrimMode.KeepReferences, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); // Synchronous Stream methods - public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamTrimResult StreamAcknowledgeAndDelete(RedisKey key, RedisValue groupName, StreamTrimMode trimMode, RedisValue messageId, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamTrimResult[] StreamAcknowledgeAndDelete(RedisKey key, RedisValue groupName, StreamTrimMode trimMode, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamAutoClaimResult StreamAutoClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamAutoClaimIdsOnlyResult StreamAutoClaimIdsOnly(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamEntry[] StreamClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] StreamClaimIdsOnly(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool StreamConsumerGroupSetPosition(RedisKey key, RedisValue groupName, RedisValue position, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamConsumerInfo[] StreamConsumerInfo(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool StreamCreateConsumerGroup(RedisKey key, RedisValue groupName, RedisValue? position = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool StreamCreateConsumerGroup(RedisKey key, RedisValue groupName, RedisValue? position = null, bool createStream = true, CommandFlags flags = CommandFlags.None) => + public long StreamAcknowledge( + RedisKey key, + RedisValue groupName, + RedisValue messageId, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamAcknowledge( + RedisKey key, + RedisValue groupName, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamTrimResult StreamAcknowledgeAndDelete( + RedisKey key, + RedisValue groupName, + StreamTrimMode trimMode, + RedisValue messageId, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamTrimResult[] StreamAcknowledgeAndDelete( + RedisKey key, + RedisValue groupName, + StreamTrimMode trimMode, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StreamAdd( + RedisKey key, + RedisValue streamField, + RedisValue streamValue, + RedisValue? messageId = null, + int? maxLength = null, + bool useApproximateMaxLength = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StreamAdd( + RedisKey key, + NameValueEntry[] streamPairs, + RedisValue? messageId = null, + int? maxLength = null, + bool useApproximateMaxLength = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StreamAdd( + RedisKey key, + RedisValue streamField, + RedisValue streamValue, + RedisValue? messageId = null, + long? maxLength = null, + bool useApproximateMaxLength = false, + long? limit = null, + StreamTrimMode trimMode = StreamTrimMode.KeepReferences, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue StreamAdd( + RedisKey key, + NameValueEntry[] streamPairs, + RedisValue? messageId = null, + long? maxLength = null, + bool useApproximateMaxLength = false, + long? limit = null, + StreamTrimMode trimMode = StreamTrimMode.KeepReferences, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamAutoClaimResult StreamAutoClaim( + RedisKey key, + RedisValue consumerGroup, + RedisValue claimingConsumer, + long minIdleTimeInMs, + RedisValue startAtId, + int? count = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamAutoClaimIdsOnlyResult StreamAutoClaimIdsOnly( + RedisKey key, + RedisValue consumerGroup, + RedisValue claimingConsumer, + long minIdleTimeInMs, + RedisValue startAtId, + int? count = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamEntry[] StreamClaim( + RedisKey key, + RedisValue consumerGroup, + RedisValue claimingConsumer, + long minIdleTimeInMs, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] StreamClaimIdsOnly( + RedisKey key, + RedisValue consumerGroup, + RedisValue claimingConsumer, + long minIdleTimeInMs, + RedisValue[] messageIds, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool StreamConsumerGroupSetPosition( + RedisKey key, + RedisValue groupName, + RedisValue position, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamConsumerInfo[] StreamConsumerInfo( + RedisKey key, + RedisValue groupName, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool StreamCreateConsumerGroup( + RedisKey key, + RedisValue groupName, + RedisValue? position = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool StreamCreateConsumerGroup( + RedisKey key, + RedisValue groupName, + RedisValue? position = null, + bool createStream = true, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public long StreamDelete(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public StreamTrimResult[] StreamDelete(RedisKey key, RedisValue[] messageIds, StreamTrimMode trimMode, CommandFlags flags = CommandFlags.None) => + public StreamTrimResult[] StreamDelete( + RedisKey key, + RedisValue[] messageIds, + StreamTrimMode trimMode, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long StreamDeleteConsumer(RedisKey key, RedisValue groupName, RedisValue consumerName, CommandFlags flags = CommandFlags.None) => + public long StreamDeleteConsumer( + RedisKey key, + RedisValue groupName, + RedisValue consumerName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public bool StreamDeleteConsumerGroup(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => @@ -185,42 +456,113 @@ public StreamInfo StreamInfo(RedisKey key, CommandFlags flags = CommandFlags.Non public long StreamLength(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public StreamPendingInfo StreamPending(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, long? idleTime = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamEntry[] StreamRange(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamEntry[] StreamRead(RedisKey key, RedisValue position, int? count = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisStream[] StreamRead(StreamPosition[] streamPositions, int? countPerStream = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => + public StreamPendingInfo StreamPending( + RedisKey key, + RedisValue groupName, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - - public long StreamTrim(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StreamTrim(RedisKey key, long maxLength, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long StreamTrimByMinId(RedisKey key, RedisValue minId, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + + public StreamPendingMessageInfo[] StreamPendingMessages( + RedisKey key, + RedisValue groupName, + int count, + RedisValue consumerName, + RedisValue? minId = null, + RedisValue? maxId = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamPendingMessageInfo[] StreamPendingMessages( + RedisKey key, + RedisValue groupName, + int count, + RedisValue consumerName, + RedisValue? minId = null, + RedisValue? maxId = null, + long? idleTime = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamEntry[] StreamRange( + RedisKey key, + RedisValue? minId = null, + RedisValue? maxId = null, + int? count = null, + Order messageOrder = Order.Ascending, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamEntry[] StreamRead( + RedisKey key, + RedisValue position, + int? count = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisStream[] StreamRead( + StreamPosition[] streamPositions, + int? countPerStream = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamEntry[] StreamReadGroup( + RedisKey key, + RedisValue groupName, + RedisValue consumerName, + RedisValue? position = null, + int? count = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public StreamEntry[] StreamReadGroup( + RedisKey key, + RedisValue groupName, + RedisValue consumerName, + RedisValue? position = null, + int? count = null, + bool noAck = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisStream[] StreamReadGroup( + StreamPosition[] streamPositions, + RedisValue groupName, + RedisValue consumerName, + int? countPerStream = null, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisStream[] StreamReadGroup( + StreamPosition[] streamPositions, + RedisValue groupName, + RedisValue consumerName, + int? countPerStream = null, + bool noAck = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamTrim( + RedisKey key, + int maxLength, + bool useApproximateMaxLength = false, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamTrim( + RedisKey key, + long maxLength, + bool useApproximateMaxLength = false, + long? limit = null, + StreamTrimMode trimMode = StreamTrimMode.KeepReferences, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long StreamTrimByMinId( + RedisKey key, + RedisValue minId, + bool useApproximateMaxLength = false, + long? limit = null, + StreamTrimMode trimMode = StreamTrimMode.KeepReferences, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); } diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs index 43f57f2a5..b662a7a27 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using StackExchange.Redis; +using StackExchange.Redis; namespace RESPite.StackExchange.Redis; @@ -13,19 +11,39 @@ public Task StringAppendAsync(RedisKey key, RedisValue value, CommandFlags public Task StringBitCountAsync(RedisKey key, long start, long end, CommandFlags flags) => throw new NotImplementedException(); - public Task StringBitCountAsync(RedisKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => + public Task StringBitCountAsync( + RedisKey key, + long start = 0, + long end = -1, + StringIndexType indexType = StringIndexType.Byte, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default, CommandFlags flags = CommandFlags.None) => + public Task StringBitOperationAsync( + Bitwise operation, + RedisKey destination, + RedisKey first, + RedisKey second = default, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + public Task StringBitOperationAsync( + Bitwise operation, + RedisKey destination, + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task StringBitPositionAsync(RedisKey key, bool bit, long start, long end, CommandFlags flags) => throw new NotImplementedException(); - public Task StringBitPositionAsync(RedisKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => + public Task StringBitPositionAsync( + RedisKey key, + bool bit, + long start = 0, + long end = -1, + StringIndexType indexType = StringIndexType.Byte, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task StringDecrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => @@ -46,16 +64,26 @@ public Task StringGetAsync(RedisKey[] keys, CommandFlags flags = C public Task StringGetBitAsync(RedisKey key, long offset, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StringGetRangeAsync(RedisKey key, long start, long end, CommandFlags flags = CommandFlags.None) => + public Task StringGetRangeAsync( + RedisKey key, + long start, + long end, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task StringGetSetAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StringGetSetExpiryAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => + public Task StringGetSetExpiryAsync( + RedisKey key, + TimeSpan? expiry, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StringGetSetExpiryAsync(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) => + public Task StringGetSetExpiryAsync( + RedisKey key, + DateTime expiry, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task StringGetDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => @@ -73,13 +101,23 @@ public Task StringIncrementAsync(RedisKey key, double value, CommandFlag public Task StringLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StringLongestCommonSubsequenceAsync(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + public Task StringLongestCommonSubsequenceAsync( + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StringLongestCommonSubsequenceLengthAsync(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + public Task StringLongestCommonSubsequenceLengthAsync( + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StringLongestCommonSubsequenceWithMatchesAsync(RedisKey first, RedisKey second, long minLength = 0, CommandFlags flags = CommandFlags.None) => + public Task StringLongestCommonSubsequenceWithMatchesAsync( + RedisKey first, + RedisKey second, + long minLength = 0, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when) => @@ -88,22 +126,46 @@ public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expir public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => throw new NotImplementedException(); - public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public Task StringSetAsync( + RedisKey key, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StringSetAsync(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public Task StringSetAsync( + KeyValuePair[] values, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => + public Task StringSetAndGetAsync( + RedisKey key, + RedisValue value, + TimeSpan? expiry, + When when, + CommandFlags flags) => throw new NotImplementedException(); - public Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public Task StringSetAndGetAsync( + RedisKey key, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task StringSetBitAsync(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StringSetRangeAsync(RedisKey key, long offset, RedisValue value, CommandFlags flags = CommandFlags.None) => + public Task StringSetRangeAsync( + RedisKey key, + long offset, + RedisValue value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); // Synchronous String methods @@ -113,19 +175,39 @@ public long StringAppend(RedisKey key, RedisValue value, CommandFlags flags = Co public long StringBitCount(RedisKey key, long start, long end, CommandFlags flags) => throw new NotImplementedException(); - public long StringBitCount(RedisKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => + public long StringBitCount( + RedisKey key, + long start = 0, + long end = -1, + StringIndexType indexType = StringIndexType.Byte, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default, CommandFlags flags = CommandFlags.None) => + public long StringBitOperation( + Bitwise operation, + RedisKey destination, + RedisKey first, + RedisKey second = default, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + public long StringBitOperation( + Bitwise operation, + RedisKey destination, + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public long StringBitPosition(RedisKey key, bool bit, long start, long end, CommandFlags flags) => throw new NotImplementedException(); - public long StringBitPosition(RedisKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => + public long StringBitPosition( + RedisKey key, + bool bit, + long start = 0, + long end = -1, + StringIndexType indexType = StringIndexType.Byte, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public long StringDecrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => @@ -173,13 +255,23 @@ public double StringIncrement(RedisKey key, double value, CommandFlags flags = C public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public string? StringLongestCommonSubsequence(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + public string? StringLongestCommonSubsequence( + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long StringLongestCommonSubsequenceLength(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + public long StringLongestCommonSubsequenceLength( + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public LCSMatchResult StringLongestCommonSubsequenceWithMatches(RedisKey first, RedisKey second, long minLength = 0, CommandFlags flags = CommandFlags.None) => + public LCSMatchResult StringLongestCommonSubsequenceWithMatches( + RedisKey first, + RedisKey second, + long minLength = 0, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when) => @@ -188,21 +280,45 @@ public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When whe public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => throw new NotImplementedException(); - public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public bool StringSet( + RedisKey key, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public bool StringSet(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public bool StringSet( + KeyValuePair[] values, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => + public RedisValue StringSetAndGet( + RedisKey key, + RedisValue value, + TimeSpan? expiry, + When when, + CommandFlags flags) => throw new NotImplementedException(); - public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + public RedisValue StringSetAndGet( + RedisKey key, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public bool StringSetBit(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue StringSetRange(RedisKey key, long offset, RedisValue value, CommandFlags flags = CommandFlags.None) => + public RedisValue StringSetRange( + RedisKey key, + long offset, + RedisValue value, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); } diff --git a/src/RESPite.StackExchange.Redis/RespMultiplexer.cs b/src/RESPite.StackExchange.Redis/RespMultiplexer.cs index 898beba06..f129c8e99 100644 --- a/src/RESPite.StackExchange.Redis/RespMultiplexer.cs +++ b/src/RESPite.StackExchange.Redis/RespMultiplexer.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Net; +using RESPite.Connections.Internal; using StackExchange.Redis; using StackExchange.Redis.Maintenance; using StackExchange.Redis.Profiling; @@ -13,7 +14,8 @@ public sealed class RespMultiplexer : IConnectionMultiplexer, IRespContextProxy public RespMultiplexer() { - _routedConnection = _defaultConnection = RespContext.Null.Connection; // until we've connected + _routedConnection = RespContext.Null.Connection; // until we've connected + _defaultContext = _routedConnection.Context; } private int _defaultDatabase; @@ -22,9 +24,10 @@ public RespMultiplexer() // instance that isn't necessary, so the default-connection abstracts over that: // in a single-node instance, the default-connection will be the single interactive connection // otherwise, the default-connection will be the routed connection - private RespConnection _routedConnection, _defaultConnection; - internal ref readonly RespContext Context => ref _defaultConnection.Context; - ref readonly RespContext IRespContextProxy.Context => ref _defaultConnection.Context; + private RespConnection _routedConnection; + private RespContext _defaultContext; + internal ref readonly RespContext Context => ref _defaultContext; + ref readonly RespContext IRespContextProxy.Context => ref _defaultContext; RespMultiplexer IRespContextProxy.Multiplexer => this; private readonly CancellationTokenSource _lifetime = new(); @@ -69,9 +72,11 @@ private void OnConnect(ConfigurationOptions options) _defaultDatabase = options.DefaultDatabase ?? 0; - // setup a basic connection that comes via ourselves (this might get simplified later, in OnNodesChanged) + // setup a basic connection that comes via ourselves var ctx = RespContext.Null; // this is just the template - _defaultConnection = _routedConnection = new RoutingRespConnection(this, ctx); + _routedConnection = new RoutingRespConnection(this, ctx); + // set the default context (this might get simplified later, in OnNodesChanged) + _defaultContext = _routedConnection.Context; } public void Connect(string configuration = "", TextWriter? log = null) @@ -119,11 +124,11 @@ public async Task ConnectAsync(ConfigurationOptions options, TextWriter? log = n public void Dispose() { - RespConnection c1 = _routedConnection, c2 = _defaultConnection; - _defaultConnection = _routedConnection = RespContext.Null.Connection; + RespConnection conn = _routedConnection; + _routedConnection = NullConnection.Disposed; + _defaultContext = _routedConnection.Context; _lifetime.Cancel(); - c1.Dispose(); - c2.Dispose(); + conn.Dispose(); _routedConnection.Dispose(); foreach (var node in _nodes) { @@ -133,15 +138,15 @@ public void Dispose() public async ValueTask DisposeAsync() { - RespConnection c1 = _routedConnection, c2 = _defaultConnection; - _defaultConnection = _routedConnection = RespContext.Null.Connection; + RespConnection conn = _routedConnection; + _routedConnection = RespContext.Null.Connection; + _defaultContext = _routedConnection.Context; #if NET8_0_OR_GREATER await _lifetime.CancelAsync().ConfigureAwait(false); #else _lifetime.Cancel(); #endif - await c1.DisposeAsync().ConfigureAwait(false); - await c2.DisposeAsync().ConfigureAwait(false); + await conn.DisposeAsync().ConfigureAwait(false); await _routedConnection.DisposeAsync().ConfigureAwait(false); foreach (var node in _nodes) { @@ -287,11 +292,11 @@ public IServer GetServer(IPAddress host, int port) private void OnNodesChanged() { var nodes = _nodes; - _defaultConnection = nodes.Length switch + _defaultContext = nodes.Length switch { - 0 => RespContext.Null.Connection, - 1 when nodes[0] is { IsConnected: true } node => node.InteractiveConnection, - _ => _routedConnection, + 0 => NullConnection.NonRoutable.Context, // nowhere to go + 1 when nodes[0] is { IsConnected: true } node => node.InteractiveConnection.Context, + _ => _routedConnection.Context, }; } diff --git a/src/RESPite.StackExchange.Redis/Utils.cs b/src/RESPite.StackExchange.Redis/Utils.cs index 86f187cad..0b24c8f5f 100644 --- a/src/RESPite.StackExchange.Redis/Utils.cs +++ b/src/RESPite.StackExchange.Redis/Utils.cs @@ -1,6 +1,4 @@ -using System.Net; - -namespace RESPite.StackExchange.Redis; +namespace RESPite.StackExchange.Redis; internal static class Utils { diff --git a/src/RESPite/Connections/Internal/NullConnection.cs b/src/RESPite/Connections/Internal/NullConnection.cs index 139e8cb82..a803e2c48 100644 --- a/src/RESPite/Connections/Internal/NullConnection.cs +++ b/src/RESPite/Connections/Internal/NullConnection.cs @@ -2,35 +2,77 @@ internal sealed class NullConnection : RespConnection { + private enum FailureMode + { + Default, + Disposed, + NonRoutable, + } + + private readonly FailureMode _failureMode; + public static NullConnection WithConfiguration(RespConfiguration configuration) => ReferenceEquals(configuration, RespConfiguration.Default) ? Default - : new(configuration); + : new(configuration, FailureMode.Default); - public static readonly NullConnection Default = new(RespConfiguration.Default); + // convenience singletons (all but Default are lazily created) + public static readonly NullConnection Default = new(RespConfiguration.Default, FailureMode.Default); + private static NullConnection? _disposed, _nonRoutable; + public static NullConnection Disposed => + _disposed ??= new(RespConfiguration.Default, FailureMode.Disposed); + public static NullConnection NonRoutable => + _nonRoutable ??= new(RespConfiguration.Default, FailureMode.NonRoutable); internal override int OutstandingOperations => 0; - private NullConnection(RespConfiguration configuration) : base(configuration) + private NullConnection(RespConfiguration configuration, FailureMode failureMode) : base(configuration) + => _failureMode = failureMode; + + private void SetError(in RespOperation message) { + message.TrySetException(_failureMode switch + { + FailureMode.Disposed => new ObjectDisposedException(nameof(RespConnection)), + FailureMode.NonRoutable => new InvalidOperationException("No connection is available for this operation."), + _ => new NotSupportedException("Null connections do not support sending messages."), + }); } - private const string SendErrorMessage = "Null connections do not support sending messages."; - public override void Write(in RespOperation message) + public override void Write(in RespOperation message) => SetError(in message); + + public override Task WriteAsync(in RespOperation message) { - message.TrySetException(new NotSupportedException(SendErrorMessage)); + SetError(message); + return Task.CompletedTask; } - public override Task WriteAsync(in RespOperation message) + internal override void Write(ReadOnlySpan messages) + { + foreach (var message in messages) + { + SetError(in message); + } + } + + internal override Task WriteAsync(ReadOnlyMemory messages) { - Write(message); + foreach (var message in messages.Span) + { + SetError(in message); + } + return Task.CompletedTask; } public override event EventHandler? ConnectionError { - add { } - remove { } + add + { + } + remove + { + } } internal override void ThrowIfUnhealthy() { } From 68845602ad6d961d9e96e6acd42580c82e3052ef Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 5 Sep 2025 12:19:33 +0100 Subject: [PATCH 057/108] get as far as connecting --- .../IRespContextProxy.cs | 10 +++ src/RESPite.StackExchange.Redis/Node.cs | 20 ++++- .../ProxiedDatabase.cs | 1 + .../RespMultiplexer.cs | 73 ++++++++++++------- tests/RESPite.Tests/RespMultiplexerTests.cs | 7 ++ 5 files changed, 82 insertions(+), 29 deletions(-) diff --git a/src/RESPite.StackExchange.Redis/IRespContextProxy.cs b/src/RESPite.StackExchange.Redis/IRespContextProxy.cs index e52433ef0..bc67e0f93 100644 --- a/src/RESPite.StackExchange.Redis/IRespContextProxy.cs +++ b/src/RESPite.StackExchange.Redis/IRespContextProxy.cs @@ -7,4 +7,14 @@ internal interface IRespContextProxy { RespMultiplexer Multiplexer { get; } ref readonly RespContext Context { get; } + RespContextProxyKind RespContextProxyKind { get; } +} + +internal enum RespContextProxyKind +{ + Unknown, + Multiplexer, + ConnectionInteractive, + ConnectionSubscription, + Batch, } diff --git a/src/RESPite.StackExchange.Redis/Node.cs b/src/RESPite.StackExchange.Redis/Node.cs index 238d5d018..fade24621 100644 --- a/src/RESPite.StackExchange.Redis/Node.cs +++ b/src/RESPite.StackExchange.Redis/Node.cs @@ -11,6 +11,7 @@ internal sealed class Node : IDisposable, IAsyncDisposable, IRespContextProxy public Version Version { get; } public EndPoint EndPoint => _interactive.EndPoint; public RespMultiplexer Multiplexer => _interactive.Multiplexer; + public Node(RespMultiplexer multiplexer, EndPoint endPoint) { _interactive = new(multiplexer, endPoint, ConnectionType.Interactive); @@ -42,9 +43,14 @@ public async ValueTask DisposeAsync() private NodeConnection? _subscription; public ref readonly RespContext Context => ref _interactive.Context; + RespContextProxyKind IRespContextProxy.RespContextProxyKind => RespContextProxyKind.ConnectionInteractive; + public RespConnection InteractiveConnection => _interactive.Connection; - public Task ConnectAsync(TextWriter? log = null, bool force = false, ConnectionType connectionType = ConnectionType.Interactive) + public Task ConnectAsync( + TextWriter? log = null, + bool force = false, + ConnectionType connectionType = ConnectionType.Interactive) { if (_isDisposed) return Task.FromResult(false); if (connectionType == ConnectionType.Interactive) @@ -83,6 +89,13 @@ public NodeConnection(RespMultiplexer multiplexer, EndPoint endPoint, Connection _label = Format.ToString(endPoint); } + RespContextProxyKind IRespContextProxy.RespContextProxyKind => _connectionType switch + { + ConnectionType.Interactive => RespContextProxyKind.ConnectionInteractive, + ConnectionType.Subscription => RespContextProxyKind.ConnectionSubscription, + _ => RespContextProxyKind.Unknown, + }; + public EndPoint EndPoint => _endPoint; private int _state = (int)NodeState.Disconnected; private readonly string _label; @@ -134,7 +147,10 @@ public async Task ConnectAsync(TextWriter? log = null, bool force = false) try { - log.LogLocked($"[{_label}] Connecting..."); + // observe outcome of CEX above (noting that if forcing, we don't do that CEX) + if (State == NodeState.Connecting) state = (int)NodeState.Connecting; + + log.LogLocked($"[{_label}] {_endPoint.GetType().Name} connecting..."); connecting = true; var connection = await RespConnection.CreateAsync( _endPoint, diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs index 245270449..5d89041d8 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs @@ -16,6 +16,7 @@ internal sealed partial class ProxiedDatabase(IRespContextProxy proxy, int db) : public int Database => db; public IConnectionMultiplexer Multiplexer => proxy.Multiplexer; + public RespContextProxyKind RespContextProxyKind => proxy.RespContextProxyKind; public bool TryWait(Task task) => proxy.Multiplexer.TryWait(task); diff --git a/src/RESPite.StackExchange.Redis/RespMultiplexer.cs b/src/RESPite.StackExchange.Redis/RespMultiplexer.cs index f129c8e99..ab198cf7d 100644 --- a/src/RESPite.StackExchange.Redis/RespMultiplexer.cs +++ b/src/RESPite.StackExchange.Redis/RespMultiplexer.cs @@ -28,6 +28,7 @@ public RespMultiplexer() private RespContext _defaultContext; internal ref readonly RespContext Context => ref _defaultContext; ref readonly RespContext IRespContextProxy.Context => ref _defaultContext; + RespContextProxyKind IRespContextProxy.RespContextProxyKind => RespContextProxyKind.Multiplexer; RespMultiplexer IRespContextProxy.Multiplexer => this; private readonly CancellationTokenSource _lifetime = new(); @@ -43,32 +44,39 @@ private static ConfigurationOptions ThrowNotConnected() private void OnConnect(ConfigurationOptions options) { + if (options is null) throw new ArgumentNullException(nameof(options)); if (Interlocked.CompareExchange(ref _options, options, null) is not null) { throw new InvalidOperationException($"A {GetType().Name} can only be connected once."); } - // add endpoints from the new options - const int DefaultPort = 6379; - int count = options.EndPoints.Count; - switch (count) + // fixup the endpoints in an isolated collection + var ep = options.EndPoints.Clone(); + if (ep.Count == 0) { - case 0: - _nodes = [new Node(this, new IPEndPoint(IPAddress.Loopback, DefaultPort))]; - break; - case 1: - _nodes = [new Node(this, options.EndPoints[0])]; - break; - default: - var nodes = new Node[count]; - for (int i = 0; i < nodes.Length; i++) + // no endpoints; add a default, deferring the port to the SSL setting + ep.Add(new IPEndPoint(IPAddress.Loopback, 0)); + } + else + { + for (int i = 0; i < ep.Count; i++) + { + if (ep[i] is DnsEndPoint { Host: "." or "localhost" } dns) { - nodes[i] = new Node(this, options.EndPoints[i]); + // unroll loopback + ep[i] = new IPEndPoint(IPAddress.Loopback, dns.Port); } + } + } + ep.SetDefaultPorts(ServerType.Standalone, ssl: options.Ssl); - _nodes = nodes; - break; + // add nodes from the endpoints + var nodes = new Node[ep.Count]; + for (int i = 0; i < nodes.Length; i++) + { + nodes[i] = new Node(this, ep[i]); } + _nodes = nodes; _defaultDatabase = options.DefaultDatabase ?? 0; @@ -93,6 +101,7 @@ public void Connect(ConfigurationOptions options, TextWriter? log = null) public Task ConnectAsync(string configuration = "", TextWriter? log = null) { // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + if (string.IsNullOrWhiteSpace(configuration)) configuration = "."; // localhost by default var config = ConfigurationOptions.Parse(configuration ?? ""); return ConnectAsync(config, log); } @@ -270,23 +279,37 @@ public IDatabase GetDatabase(int db = -1, object? asyncState = null) private const int LowDatabaseCount = 16; private readonly IDatabase?[] _lowDatabases = new IDatabase?[LowDatabaseCount]; - public IServer GetServer(string host, int port, object? asyncState = null) => throw new NotImplementedException(); + public IServer GetServer(string host, int port, object? asyncState = null) + => GetServer(Format.ParseEndPoint(host, port), asyncState); - public IServer GetServer(string hostAndPort, object? asyncState = null) => throw new NotImplementedException(); + public IServer GetServer(string hostAndPort, object? asyncState = null) => + Format.TryParseEndPoint(hostAndPort, out var ep) + ? GetServer(ep, asyncState) + : throw new ArgumentException($"The specified host and port could not be parsed: {hostAndPort}", nameof(hostAndPort)); public IServer GetServer(IPAddress host, int port) { - var nodes = _nodes; - foreach (var node in nodes) + foreach (var node in _nodes) { if (node.EndPoint is IPEndPoint ep && ep.Address.Equals(host) && ep.Port == port) { return node.AsServer(); } } + throw new ArgumentException("The specified endpoint is not defined", nameof(host)); + } - ThrowArgumentException(); - return null!; + public IServer GetServer(EndPoint endpoint, object? asyncState = null) + { + foreach (var node in _nodes) + { + if (node.EndPoint.Equals(endpoint)) + { + return node.AsServer(); + } + } + + throw new ArgumentException("The specified endpoint is not defined", nameof(endpoint)); } private void OnNodesChanged() @@ -300,11 +323,7 @@ private void OnNodesChanged() }; } - private static void ThrowArgumentException() => throw new ArgumentException(); - - public IServer GetServer(EndPoint endpoint, object? asyncState = null) => throw new NotImplementedException(); - - public IServer[] GetServers() => throw new NotImplementedException(); + public IServer[] GetServers() => Array.ConvertAll(_nodes, static x => x.AsServer()); public Task ConfigureAsync(TextWriter? log = null) => throw new NotImplementedException(); diff --git a/tests/RESPite.Tests/RespMultiplexerTests.cs b/tests/RESPite.Tests/RespMultiplexerTests.cs index 5c14662b1..d7c7e021f 100644 --- a/tests/RESPite.Tests/RespMultiplexerTests.cs +++ b/tests/RESPite.Tests/RespMultiplexerTests.cs @@ -20,5 +20,12 @@ public async Task CanConnect() Assert.IsType(server); // we expect this to *not* use routing server.Ping(); await server.PingAsync(); + + var db = muxer.GetDatabase(); + var proxied = Assert.IsType(db); + // since this is a single-node instance, we expect the proxied database to use the interactive connection + Assert.Equal(RespContextProxyKind.ConnectionInteractive, proxied.RespContextProxyKind); + db.Ping(); + await db.PingAsync(); } } From 5c1b0e343eb083b31946f4510b5eb4709925b49e Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 5 Sep 2025 15:36:42 +0100 Subject: [PATCH 058/108] start lighting up RedisValue/RedisKey APIs --- .../RespCommandGenerator.cs | 117 +++++++++++++++-- .../ProxiedDatabase.Connection.cs | 20 +-- .../ProxiedDatabase.cs | 16 ++- .../RespFormatters.cs | 80 ++++++++++++ .../RespParsers.cs | 35 +++++ src/RESPite/Messages/RespReader.cs | 122 +++++++++++++----- src/RESPite/Messages/RespWriter.cs | 29 +++++ src/RESPite/RespContext.cs | 96 +++++++++++++- src/RESPite/RespContextExtensions.cs | 30 +++++ 9 files changed, 486 insertions(+), 59 deletions(-) create mode 100644 src/RESPite.StackExchange.Redis/RespFormatters.cs create mode 100644 src/RESPite.StackExchange.Redis/RespParsers.cs diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index b4337a907..8ed5c2434 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -74,6 +74,41 @@ private static bool IsRESPite(ITypeSymbol? symbol, RESPite type) return false; } + private enum SERedis + { + CommandFlags, + } + + private static bool IsSERedis(ITypeSymbol? symbol, SERedis type) + { + static string NameOf(SERedis type) => type switch + { + SERedis.CommandFlags => nameof(SERedis.CommandFlags), + _ => type.ToString(), + }; + + if (symbol is INamedTypeSymbol named && named.Name == NameOf(type)) + { + // looking likely; check namespace + if (named.ContainingNamespace is + { + Name: "Redis", ContainingNamespace: + { + Name: "StackExchange", + ContainingNamespace.IsGlobalNamespace: true, + } + }) + { + return true; + } + + // if the type doesn't resolve: we're going to need to trust it + if (named.TypeKind == TypeKind.Error) return true; + } + + return false; + } + private static string GetName(ITypeSymbol type) { if (type.ContainingType is null) return type.Name; @@ -303,7 +338,21 @@ static bool IsIndirectRespContext(ITypeSymbol type, out string memberName) { var flags = ParameterFlags.Parameter; if (IsKey(param)) flags |= ParameterFlags.Key; - if (contextParam is null || !SymbolEqualityComparer.Default.Equals(param, contextParam)) + if (IsSERedis(param.Type, SERedis.CommandFlags)) + { + flags |= ParameterFlags.CommandFlags; + // magic pattern; we *demand* a method called Context that takes the flags + context = $"Context({param.Name})"; + } + else if (IsRESPite(param.Type, RESPite.RespContext)) + { + // ignore it, but no extra flag + } + else if (contextParam is not null && SymbolEqualityComparer.Default.Equals(param, contextParam)) + { + // ignore it, but no extra flag + } + else { flags |= ParameterFlags.Data; } @@ -496,7 +545,7 @@ void WriteMethod(bool asAsync) .Append(' '); if (asAsync) { - sb.Append("ValueTask"); + sb.Append(HasAnyFlag(method.Parameters, ParameterFlags.CommandFlags) ? "Task" : "ValueTask"); if (!string.IsNullOrWhiteSpace(method.ReturnType)) { sb.Append('<').Append(method.ReturnType).Append('>'); @@ -553,7 +602,12 @@ void WriteMethod(bool asAsync) sb.Append('<').Append(method.ReturnType).Append('>'); } - sb.Append("(").Append(parser).Append(");"); + sb.Append("(").Append(parser).Append(")"); + if (asAsync && HasAnyFlag(method.Parameters, ParameterFlags.CommandFlags)) + { + sb.Append(".AsTask()"); + } + sb.Append(';'); } if (useDirectCall) // avoid the intermediate step when possible @@ -574,11 +628,21 @@ void WriteMethod(bool asAsync) sb.Append(", ").Append(formatter).Append(", ").Append(parser).Append(")"); if (asAsync) { - sb.Append(".AsValueTask()"); + sb.Append(HasAnyFlag(method.Parameters, ParameterFlags.CommandFlags) ? ".AsTask()" : ".AsValueTask()"); } else { - sb.Append(".Wait(").Append(method.Context).Append(".SyncTimeout)"); + sb.Append(".Wait("); + if (HasAnyFlag(method.Parameters, ParameterFlags.CommandFlags)) + { + // to avoid calling Context(flags) twice, we assume that this member will exist + sb.Append("SyncTimeout"); + } + else + { + sb.Append(method.Context).Append(".SyncTimeout"); + } + sb.Append(")"); } sb.Append(";"); @@ -627,7 +691,18 @@ void WriteMethod(bool asAsync) sb.Append(");"); if (count == 1) { - NewLine().Append("writer.WriteBulkString(request);"); + var p = FirstDataParameter(parameters); + sb = NewLine().Append("writer."); + if (p.Type is "global::StackExchange.Redis.RedisValue" or "global::StackExchange.Redis.RedisKey") + { + sb.Append("Write"); + } + else + { + sb.Append((p.Flags & ParameterFlags.Key) == 0 ? "WriteBulkString" : "WriteKey"); + } + + sb.Append("(request);"); } else { @@ -636,9 +711,18 @@ void WriteMethod(bool asAsync) { if ((parameter.Flags & ParameterFlags.DataParameter) == ParameterFlags.DataParameter) { - sb = NewLine().Append("writer.") - .Append((parameter.Flags & ParameterFlags.Key) == 0 ? "WriteBulkString" : "WriteKey") - .Append("(request."); + sb = NewLine().Append("writer."); + if (parameter.Type is "global::StackExchange.Redis.RedisValue" + or "global::StackExchange.Redis.RedisKey") + { + sb.Append("Write"); + } + else + { + sb.Append((parameter.Flags & ParameterFlags.Key) == 0 ? "WriteBulkString" : "WriteKey"); + } + + sb.Append("(request."); if (names == TupleMode.SyntheticNames) { sb.Append("Arg").Append(index); @@ -713,6 +797,16 @@ static void WriteTuple( } } + private static bool HasAnyFlag(ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> parameters, ParameterFlags any) + { + foreach (var p in parameters) + { + if ((p.Flags & any) != 0) return true; + } + + return false; + } + private static string? InbuiltFormatter( ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> parameters) { @@ -769,6 +863,8 @@ private static int DataParameterCount( "float" => RespFormattersPrefix + "Single", "double" => RespFormattersPrefix + "Double", "" => RespFormattersPrefix + "Empty", + "global::StackExchange.Redis.RedisKey" => "global::RESPite.StackExchange.Redis.RespFormatters.RedisKey", + "global::StackExchange.Redis.RedisValue" => "global::RESPite.StackExchange.Redis.RespFormatters.RedisValue", _ => null, }; @@ -788,6 +884,8 @@ private static int DataParameterCount( "float?" => RespParsersPrefix + "NullableSingle", "double?" => RespParsersPrefix + "NullableDouble", "global::RESPite.RespParsers.ResponseSummary" => RespParsersPrefix + "ResponseSummary.Parser", + "global::StackExchange.Redis.RedisKey" => "global::RESPite.StackExchange.Redis.RespParsers.RedisKey", + "global::StackExchange.Redis.RedisValue" => "global::RESPite.StackExchange.Redis.RespParsers.RedisValue", _ => null, }; @@ -818,6 +916,7 @@ private enum ParameterFlags DataParameter = Data | Parameter, Key = 1 << 2, Literal = 1 << 3, + CommandFlags = 1 << 4, } // compares whether a formatter can be shared, which depends on the key index and types (not names) diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs index 39550a6cd..3caeacbfc 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs @@ -1,4 +1,5 @@ using System.Net; +using RESPite.Messages; using StackExchange.Redis; namespace RESPite.StackExchange.Redis; @@ -10,11 +11,17 @@ public bool IsConnected(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task PingAsync(CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).Send("ping"u8, DateTime.UtcNow, PingParser.Default).AsTask(); public TimeSpan Ping(CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - + Context(flags).Send("ping"u8, DateTime.UtcNow, PingParser.Default).Wait(SyncTimeout); + + private sealed class PingParser : IRespParser + { + public static readonly PingParser Default = new(); + private PingParser() { } + public TimeSpan Parse(in DateTime state, ref RespReader reader) => DateTime.UtcNow - state; + } public Task IdentifyEndpointAsync(RedisKey key = default, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -47,9 +54,6 @@ public void KeyMigrate( throw new NotImplementedException(); // Debug - public Task DebugObjectAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue DebugObject(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("debug")] + public partial RedisValue DebugObject(RedisKey key, CommandFlags flags = CommandFlags.None); } diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs index 5d89041d8..397f467c8 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs @@ -11,8 +11,20 @@ internal sealed partial class ProxiedDatabase(IRespContextProxy proxy, int db) : { // Question: cache this, or rebuild each time? the latter handles shutdown better. // internal readonly RespContext Context = proxy.Context.WithDatabase(db); - internal RespContext Context => proxy.Context.WithDatabase(db); - + private RespContext Context(CommandFlags flags) + { + // the flags intentionally align between CommandFlags and RespContextFlags + const RespContext.RespContextFlags flagMask = RespContext.RespContextFlags.DemandPrimary + | RespContext.RespContextFlags.DemandReplica + | RespContext.RespContextFlags.PreferReplica + | RespContext.RespContextFlags.NoRedirect + | RespContext.RespContextFlags.FireAndForget + | RespContext.RespContextFlags.NoScriptCache; + + return proxy.Context.With(db, (RespContext.RespContextFlags)flags, flagMask); + } + + private TimeSpan SyncTimeout => proxy.Context.SyncTimeout; public int Database => db; public IConnectionMultiplexer Multiplexer => proxy.Multiplexer; diff --git a/src/RESPite.StackExchange.Redis/RespFormatters.cs b/src/RESPite.StackExchange.Redis/RespFormatters.cs new file mode 100644 index 000000000..374aeeca4 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RespFormatters.cs @@ -0,0 +1,80 @@ +using System.Buffers; +using RESPite.Messages; +using StackExchange.Redis; +using StorageType = StackExchange.Redis.RedisValue.StorageType; + +namespace RESPite.StackExchange.Redis; + +public static class RespFormatters +{ + public static IRespFormatter RedisValue => DefaultFormatter.Instance; + public static IRespFormatter RedisKey => DefaultFormatter.Instance; + + private sealed class DefaultFormatter : IRespFormatter, IRespFormatter + { + public static readonly DefaultFormatter Instance = new(); + private DefaultFormatter() { } + + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in RedisValue request) + { + writer.WriteCommand(command, 1); + writer.Write(request); + } + + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in RedisKey request) + { + writer.WriteCommand(command, 1); + writer.Write(request); + } + } + + // ReSharper disable once MemberCanBePrivate.Global + public static void Write(this ref RespWriter writer, in RedisKey key) + { + if (key.TryGetSimpleBuffer(out var arr)) + { + key.AssertNotNull(); + writer.WriteKey(arr); + } + else + { + var len = key.TotalLength(); + byte[]? lease = null; + var span = len <= 128 ? stackalloc byte[128] : (lease = ArrayPool.Shared.Rent(len)); + var written = key.CopyTo(span); + writer.WriteKey(span.Slice(0, written)); + if (lease is not null) ArrayPool.Shared.Return(lease); + } + } + + // ReSharper disable once MemberCanBePrivate.Global + public static void Write(this ref RespWriter writer, in RedisValue value) + { + switch (value.Type) + { + case StorageType.Double: + writer.WriteBulkString(value.OverlappedValueDouble); + break; + case StorageType.Int64: + writer.WriteBulkString(value.OverlappedValueInt64); + break; + case StorageType.UInt64: + writer.WriteBulkString(value.OverlappedValueUInt64); + break; + case StorageType.String: + writer.WriteBulkString((string)value.DirectObject!); + break; + case StorageType.Raw: + writer.WriteBulkString((ReadOnlyMemory)value); + break; + case StorageType.Null: + value.AssertNotNull(); + break; + default: + Throw(value.Type); + break; + } + static void Throw(StorageType type) + => throw new InvalidOperationException($"Unexpected {type} value."); + } +} diff --git a/src/RESPite.StackExchange.Redis/RespParsers.cs b/src/RESPite.StackExchange.Redis/RespParsers.cs new file mode 100644 index 000000000..04ccf2be2 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RespParsers.cs @@ -0,0 +1,35 @@ +using RESPite.Messages; +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +public static class RespParsers +{ + public static IRespParser RedisValue => DefaultParser.Instance; + public static IRespParser RedisKey => DefaultParser.Instance; + + private sealed class DefaultParser : IRespParser, IRespParser + { + private DefaultParser() { } + public static readonly DefaultParser Instance = new(); + + RedisValue IRespParser.Parse(ref RespReader reader) + { + reader.DemandScalar(); + if (reader.IsNull) return global::StackExchange.Redis.RedisValue.Null; + if (reader.TryReadInt64(out var i64)) return i64; + if (reader.TryReadDouble(out var f64)) return f64; + + if (reader.UnsafeTryReadShortAscii(out var s)) return s; + return reader.ReadByteArray(); + } + + RedisKey IRespParser.Parse(ref RespReader reader) + { + reader.DemandScalar(); + if (reader.IsNull) return global::StackExchange.Redis.RedisKey.Null; + if (reader.UnsafeTryReadShortAscii(out var s)) return s; + return reader.ReadByteArray(); + } + } +} diff --git a/src/RESPite/Messages/RespReader.cs b/src/RESPite/Messages/RespReader.cs index 9b711ea8b..62eb7383c 100644 --- a/src/RESPite/Messages/RespReader.cs +++ b/src/RESPite/Messages/RespReader.cs @@ -100,12 +100,14 @@ public readonly bool TryGetSpan(out ReadOnlySpan value) /// /// The payload length of this scalar element (includes combined length for streaming scalars). /// - public readonly int ScalarLength() => IsInlineScalar ? _length : IsNullScalar ? 0 : checked((int)ScalarLengthSlow()); + public readonly int ScalarLength() => + IsInlineScalar ? _length : IsNullScalar ? 0 : checked((int)ScalarLengthSlow()); /// /// Indicates whether this scalar value is zero-length. /// - public readonly bool ScalarIsEmpty() => IsInlineScalar ? _length == 0 : (IsNullScalar || !ScalarChunks().MoveNext()); + public readonly bool ScalarIsEmpty() => + IsInlineScalar ? _length == 0 : (IsNullScalar || !ScalarChunks().MoveNext()); /// /// The payload length of this scalar element (includes combined length for streaming scalars). @@ -121,6 +123,7 @@ private readonly long ScalarLengthSlow() { length += iterator.CurrentLength; } + return length; } @@ -132,8 +135,10 @@ private readonly long ScalarLengthSlow() /// i.e. a map of the form %2\r\n... will report 4 as the length. /// Note that if the data could be streaming (), it may be preferable to use /// the API, using the API to update the outer reader. - public readonly int AggregateLength() => (_flags & (RespFlags.IsAggregate | RespFlags.IsStreaming)) == RespFlags.IsAggregate - ? _length : AggregateLengthSlow(); + public readonly int AggregateLength() => + (_flags & (RespFlags.IsAggregate | RespFlags.IsStreaming)) == RespFlags.IsAggregate + ? _length + : AggregateLengthSlow(); public delegate T Projection(ref RespReader value); @@ -165,6 +170,7 @@ private readonly int AggregateLengthSlow() { return count; } + reader.SkipChildren(); count++; } @@ -177,7 +183,8 @@ private readonly int AggregateLengthSlow() internal readonly bool IsInlineScalar => (_flags & RespFlags.IsInlineScalar) != 0; - internal readonly bool IsNullScalar => (_flags & (RespFlags.IsScalar | RespFlags.IsNull)) == (RespFlags.IsScalar | RespFlags.IsNull); + internal readonly bool IsNullScalar => + (_flags & (RespFlags.IsScalar | RespFlags.IsNull)) == (RespFlags.IsScalar | RespFlags.IsNull); /// /// Indicates whether this is an aggregate value, i.e. represents a collection of sub-values. @@ -203,7 +210,8 @@ private readonly int AggregateLengthSlow() /// /// Equivalent to both and . /// - internal readonly bool IsStreamingScalar => (_flags & (RespFlags.IsScalar | RespFlags.IsStreaming)) == (RespFlags.IsScalar | RespFlags.IsStreaming); + internal readonly bool IsStreamingScalar => (_flags & (RespFlags.IsScalar | RespFlags.IsStreaming)) == + (RespFlags.IsScalar | RespFlags.IsStreaming); /// /// Indicates errors reported inside the protocol. @@ -218,13 +226,14 @@ private readonly int AggregateLengthSlow() /// we still need to consume. The final terminator for streaming data reports a delta of -1, otherwise: 0. /// /// This does not account for being nested inside a streaming aggregate; the caller must deal with that manually. - internal int Delta() => (_flags & (RespFlags.IsScalar | RespFlags.IsAggregate | RespFlags.IsStreaming | RespFlags.IsAttribute)) switch - { - RespFlags.IsScalar => -1, - RespFlags.IsAggregate => _length - 1, - RespFlags.IsAggregate | RespFlags.IsAttribute => _length, - _ => 0, - }; + internal int Delta() => + (_flags & (RespFlags.IsScalar | RespFlags.IsAggregate | RespFlags.IsStreaming | RespFlags.IsAttribute)) switch + { + RespFlags.IsScalar => -1, + RespFlags.IsAggregate => _length - 1, + RespFlags.IsAggregate | RespFlags.IsAttribute => _length, + _ => 0, + }; /// /// Assert that this is the final element in the current payload. @@ -236,11 +245,14 @@ public void DemandEnd() { if (!TryReadNext()) ThrowEof(); } + if (TryReadNext()) { Throw(Prefix); } - static void Throw(RespPrefix prefix) => throw new InvalidOperationException($"Expected end of payload, but found {prefix}"); + + static void Throw(RespPrefix prefix) => + throw new InvalidOperationException($"Expected end of payload, but found {prefix}"); } private bool TryReadNextSkipAttributes() @@ -256,6 +268,7 @@ private bool TryReadNextSkipAttributes() return true; } } + return false; } @@ -272,6 +285,7 @@ private bool TryReadNextProcessAttributes(RespAttributeReader respAttribut return true; } } + return false; } @@ -292,6 +306,7 @@ public bool TryMoveNext() if (IsError) ThrowError(); return true; } + return false; } @@ -313,6 +328,7 @@ public bool TryMoveNext(bool checkError) if (checkError && IsError) ThrowError(); return true; } + return false; } @@ -336,6 +352,7 @@ public bool TryMoveNext(RespAttributeReader respAttributeReader, ref T att if (IsError) ThrowError(); return true; } + return false; } @@ -388,12 +405,15 @@ private bool MoveNextStreamingScalar() } else { - if (Prefix != RespPrefix.StreamContinuation) ThrowProtocolFailure("Streaming continuation expected"); + if (Prefix != RespPrefix.StreamContinuation) + ThrowProtocolFailure("Streaming continuation expected"); return _length > 0; } } + ThrowEof(); // we should have found something! } + return false; } @@ -455,7 +475,9 @@ public void MoveNext(RespPrefix prefix) internal void Demand(RespPrefix prefix) { if (Prefix != prefix) Throw(prefix, Prefix); - static void Throw(RespPrefix expected, RespPrefix actual) => throw new InvalidOperationException($"Expected {expected} element, but found {actual}."); + + static void Throw(RespPrefix expected, RespPrefix actual) => + throw new InvalidOperationException($"Expected {expected} element, but found {actual}."); } private readonly void ThrowError() => throw new RespException(ReadString()!); @@ -503,6 +525,7 @@ public void SkipChildren() { return IsNull ? null : ""; } + if (Prefix == RespPrefix.VerbatimString && span.Length >= 4 && span[3] == ':') { @@ -522,8 +545,10 @@ public void SkipChildren() { prefix = RespConstants.UTF8.GetString(span.Slice(0, 3)); } + span = span.Slice(4); } + return RespConstants.UTF8.GetString(span); } finally @@ -549,6 +574,7 @@ private static readonly uint { return IsNull ? null : []; } + return span.ToArray(); } finally @@ -658,11 +684,13 @@ private readonly ReadOnlySpan BufferSlow(scoped ref byte[] pooled, Span.Shared.Return(pooled); target = pooled = bigger; current.CopyTo(target.Slice(offset)); } } + return target.Slice(0, offset); } @@ -826,7 +854,8 @@ public RespReader(scoped in ReadOnlySequence value) } [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] - static ReadOnlySequenceSegment MissingNext() => throw new ArgumentException("Unable to extract tail segment", nameof(value)); + static ReadOnlySequenceSegment MissingNext() => + throw new ArgumentException("Unable to extract tail segment", nameof(value)); } /// @@ -851,11 +880,13 @@ public unsafe bool TryReadNext() var comparand = Unsafe.ReadUnaligned(ref origin); // broadcast those 4 bytes into a vector, mask to get just the first and last byte, and apply a SIMD equality test with our known cases - var eqs = Avx2.CompareEqual(Avx2.And(Avx2.BroadcastScalarToVector256(&comparand), Raw.FirstLastMask), Raw.CommonRespPrefixes); + var eqs = + Avx2.CompareEqual(Avx2.And(Avx2.BroadcastScalarToVector256(&comparand), Raw.FirstLastMask), Raw.CommonRespPrefixes); // reinterpret that as floats, and pick out the sign bits (which will be 1 for "equal", 0 for "not equal"); since the // test cases are mutually exclusive, we expect zero or one matches, so: lzcount tells us which matched - var index = Bmi1.TrailingZeroCount((uint)Avx.MoveMask(Unsafe.As, Vector256>(ref eqs))); + var index = + Bmi1.TrailingZeroCount((uint)Avx.MoveMask(Unsafe.As, Vector256>(ref eqs))); int len; #if DEBUG if (VectorizeDisabled) index = uint.MaxValue; // just to break the switch @@ -996,6 +1027,7 @@ public unsafe bool TryReadNext() _flags = RespFlags.IsScalar | RespFlags.IsStreaming; break; } + if (_flags == 0) break; // will need more data to know if (_prefix == RespPrefix.BulkError) _flags |= RespFlags.IsError; _bufferIndex += 1 + consumed; @@ -1006,7 +1038,8 @@ public unsafe bool TryReadNext() { case LengthPrefixResult.Length when _length == 0: // EOF, no payload - _flags = RespFlags.IsScalar; // don't claim as streaming, we want this to count towards delta-decrement + _flags = RespFlags + .IsScalar; // don't claim as streaming, we want this to count towards delta-decrement break; case LengthPrefixResult.Length: // still need to valid terminating CRLF @@ -1020,6 +1053,7 @@ public unsafe bool TryReadNext() ThrowProtocolFailure("Invalid streaming scalar length prefix"); break; } + if (_flags == 0) break; // will need more data to know _bufferIndex += 1 + consumed; return true; @@ -1042,6 +1076,7 @@ public unsafe bool TryReadNext() _flags = RespFlags.IsAggregate | RespFlags.IsStreaming; break; } + if (_flags == 0) break; // will need more data to know if (_prefix is RespPrefix.Attribute) _flags |= RespFlags.IsAttribute; _bufferIndex += consumed + 1; @@ -1113,6 +1148,7 @@ private static bool TryReadNextSlow(ref RespReader live) ThrowProtocolFailure("Unexpected length prefix"); return false; } + if (isolated._prefix == RespPrefix.BulkError) isolated._flags |= RespFlags.IsError; break; case RespPrefix.Array: @@ -1139,6 +1175,7 @@ private static bool TryReadNextSlow(ref RespReader live) ThrowProtocolFailure("Unexpected length prefix"); return false; } + if (isolated._prefix is RespPrefix.Attribute) isolated._flags |= RespFlags.IsAttribute; break; case RespPrefix.Null: // null @@ -1155,7 +1192,9 @@ private static bool TryReadNextSlow(ref RespReader live) { case LengthPrefixResult.Length when isolated._length == 0: // EOF, no payload - isolated._flags = RespFlags.IsScalar; // don't claim as streaming, we want this to count towards delta-decrement + isolated._flags = + RespFlags + .IsScalar; // don't claim as streaming, we want this to count towards delta-decrement break; case LengthPrefixResult.Length: // still need to valid terminating CRLF @@ -1170,11 +1209,13 @@ private static bool TryReadNextSlow(ref RespReader live) default: return false; } + break; default: ThrowProtocolFailure("Unexpected protocol prefix: " + isolated._prefix); return false; } + // commit the speculative changes back, and accept live = isolated; return true; @@ -1190,13 +1231,15 @@ private void AdvanceSlow(long bytes) _bufferIndex += (int)bytes; return; } + bytes -= available; if (!TryMoveToNextSegment()) Throw(); } [DoesNotReturn] - static void Throw() => throw new EndOfStreamException("Unexpected end of payload; this is unexpected because we already validated that it was available!"); + static void Throw() => throw new EndOfStreamException( + "Unexpected end of payload; this is unexpected because we already validated that it was available!"); } private bool AggregateLengthNeedsDoubling() => _prefix is RespPrefix.Map or RespPrefix.Attribute; @@ -1220,11 +1263,13 @@ private bool TryMoveToNextSegment() { _remainingTailLength -= span.Length; } + SetCurrent(span); _bufferIndex = 0; return true; } } + return false; } @@ -1236,6 +1281,7 @@ internal readonly bool IsOK() // go mad with this, because it is used so often var u16 = Unsafe.ReadUnaligned(ref UnsafeCurrent); return u16 == RespConstants.OKUInt16 | u16 == RespConstants.OKUInt16_LC; } + return IsSlow(RespConstants.OKBytes, RespConstants.OKBytes_LC); } @@ -1316,6 +1362,7 @@ private readonly bool IsSlow(ReadOnlySpan testValue) // nothing left to test; if also nothing left to read, great! return !iterator.MoveNext(); } + if (!iterator.MoveNext()) { return false; // test is longer @@ -1360,6 +1407,7 @@ public readonly int CopyTo(Span target) target = target.Slice(value.Length); totalBytes += value.Length; } + return totalBytes; } @@ -1384,6 +1432,7 @@ public readonly int CopyTo(IBufferWriter target) target.Write(value); totalBytes += value.Length; } + return totalBytes; } @@ -1405,12 +1454,13 @@ public readonly long ReadInt64() var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesInt64 + 1]); long value; if (!(span.Length <= RespConstants.MaxRawBytesInt64 - && Utf8Parser.TryParse(span, out value, out int bytes) - && bytes == span.Length)) + && Utf8Parser.TryParse(span, out value, out int bytes) + && bytes == span.Length)) { ThrowFormatException(); value = 0; } + return value; } @@ -1438,12 +1488,13 @@ public readonly int ReadInt32() var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesInt32 + 1]); int value; if (!(span.Length <= RespConstants.MaxRawBytesInt32 - && Utf8Parser.TryParse(span, out value, out int bytes) - && bytes == span.Length)) + && Utf8Parser.TryParse(span, out value, out int bytes) + && bytes == span.Length)) { ThrowFormatException(); value = 0; } + return value; } @@ -1475,6 +1526,7 @@ public readonly double ReadDouble() { return value; } + switch (span.Length) { case 3 when "inf"u8.SequenceEqual(span): @@ -1486,6 +1538,7 @@ public readonly double ReadDouble() case 4 when "-inf"u8.SequenceEqual(span): return double.NegativeInfinity; } + ThrowFormatException(); return 0; } @@ -1527,15 +1580,16 @@ public bool TryReadDouble(out double value, bool allowTokens = true) return false; } - internal readonly bool TryReadShortAscii(out string value) + /// + /// Note this uses a stackalloc buffer; requesting too much may overflow the stack. + /// + internal readonly bool UnsafeTryReadShortAscii(out string value, int maxLength = 127) { - const int ShortLength = 31; - - var span = Buffer(stackalloc byte[ShortLength + 1]); + var span = Buffer(stackalloc byte[maxLength + 1]); value = ""; if (span.IsEmpty) return true; - if (span.Length <= ShortLength) + if (span.Length <= maxLength) { // check for anything that looks binary or unicode foreach (var b in span) @@ -1563,12 +1617,13 @@ public readonly decimal ReadDecimal() var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesNumber + 1]); decimal value; if (!(span.Length <= RespConstants.MaxRawBytesNumber - && Utf8Parser.TryParse(span, out value, out int bytes) - && bytes == span.Length)) + && Utf8Parser.TryParse(span, out value, out int bytes) + && bytes == span.Length)) { ThrowFormatException(); value = 0; } + return value; } @@ -1592,6 +1647,7 @@ public readonly bool ReadBoolean() break; case 2 when Prefix == RespPrefix.SimpleString && IsOK(): return true; } + ThrowFormatException(); return false; } diff --git a/src/RESPite/Messages/RespWriter.cs b/src/RESPite/Messages/RespWriter.cs index cc66aa4f9..17cc35cbd 100644 --- a/src/RESPite/Messages/RespWriter.cs +++ b/src/RESPite/Messages/RespWriter.cs @@ -472,6 +472,35 @@ public void WriteBulkString(long value) } } + /// + /// Write an unsigned integer as a bulk string. + /// + public void WriteBulkString(ulong value) + { + if (value <= (ulong)long.MaxValue) + { + // re-use existing code for most values + WriteBulkString((long)value); + } + else if (Available >= RespConstants.MaxProtocolBytesBulkStringIntegerInt64) + { + WriteRaw("$20\r\n"u8); + if (!Utf8Formatter.TryFormat(value, Tail, out var bytes) || bytes != 20) + ThrowFormatException(); + _index += 20; + WriteCrLfUnsafe(); + } + else + { + WriteRaw("$20\r\n"u8); + Span scratch = stackalloc byte[20]; + if (!Utf8Formatter.TryFormat(value, scratch, out int bytes) || bytes != 20) + ThrowFormatException(); + WriteRaw(scratch); + WriteCrLf(); + } + } + private static void ThrowFormatException() => throw new FormatException(); private void WritePrefixedInteger(RespPrefix prefix, int length) diff --git a/src/RESPite/RespContext.cs b/src/RESPite/RespContext.cs index 92bd70a40..812cf4cd2 100644 --- a/src/RESPite/RespContext.cs +++ b/src/RESPite/RespContext.cs @@ -15,9 +15,57 @@ public readonly struct RespContext private readonly RespConnection _connection; public readonly CancellationToken CancellationToken; private readonly int _database; + private readonly RespContextFlags _flags; - private readonly int _flags; - private const int FlagsDisableCaptureContext = 1 << 0; + public RespContextFlags Flags => _flags; + + [Flags] + public enum RespContextFlags + { + /// + /// No additional flags; this is the default. Operations will prefer primary nodes if available. + /// + None = 0, + + /// + /// The equivalent of with `false`. + /// + DisableCaptureContext = 1, + + // IMPORTANT: the following align with CommandFlags, to avoid needing any additional mapping. + + /// + /// The caller is not interested in the result; the caller will immediately receive a default-value + /// of the expected return type (this value is not indicative of anything at the server). + /// + FireAndForget = 2, + + /// + /// This operation should only be performed on the primary. + /// + DemandPrimary = 4, + + /// + /// This operation should be performed on the replica if it is available, but will be performed on + /// a primary if no replicas are available. Suitable for read operations only. + /// + PreferReplica = 8, // note: we're using a 2-bit set here, which [Flags] formatting hates + + /// + /// This operation should only be performed on a replica. Suitable for read operations only. + /// + DemandReplica = 12, // note: we're using a 2-bit set here, which [Flags] formatting hates + + /// + /// Indicates that this operation should not be forwarded to other servers as a result of an ASK or MOVED response. + /// + NoRedirect = 64, + + /// + /// Indicates that script-related operations should use EVAL, not SCRIPT LOAD + EVALSHA. + /// + NoScriptCache = 512, + } /// public override string ToString() => _connection?.ToString() ?? "(null)"; @@ -149,15 +197,49 @@ public RespContext ConfigureAwait(bool continueOnCapturedContext) { RespContext clone = this; Unsafe.AsRef(in clone._flags) = continueOnCapturedContext - ? _flags & ~FlagsDisableCaptureContext - : _flags | FlagsDisableCaptureContext; + ? _flags & ~RespContextFlags.DisableCaptureContext + : _flags | RespContextFlags.DisableCaptureContext; + return clone; + } + + /// + /// Replaces the associated with this context. + /// + public RespContext WithFlags(RespContextFlags flags) + { + RespContext clone = this; + Unsafe.AsRef(in clone._flags) = flags; + return clone; + } + + /// + /// Replaces the and associated with this context. + /// + public RespContext With(int database, RespContextFlags flags) + { + RespContext clone = this; + Unsafe.AsRef(in clone._database) = database; + Unsafe.AsRef(in clone._flags) = flags; + return clone; + } + + /// + /// Replaces the and associated with this context, + /// using a mask to determine which flags to replace. Passing + /// for will replace no flags. + /// + public RespContext With(int database, RespContextFlags flags, RespContextFlags mask) + { + RespContext clone = this; + Unsafe.AsRef(in clone._database) = database; + Unsafe.AsRef(in clone._flags) = (flags & ~mask) | (_flags & mask); return clone; } public RespBatch CreateBatch(int sizeHint = 0) - #if MULTI_BATCH +#if MULTI_BATCH => new MergingBatchConnection(in this, sizeHint); - #else +#else => new BasicBatchConnection(in this, sizeHint); - #endif +#endif } diff --git a/src/RESPite/RespContextExtensions.cs b/src/RESPite/RespContextExtensions.cs index 7ad30bd9d..8e1eb4e75 100644 --- a/src/RESPite/RespContextExtensions.cs +++ b/src/RESPite/RespContextExtensions.cs @@ -83,6 +83,20 @@ public static RespOperation Send( return op; } + /// + /// Creates an operation and synchronously writes it to the connection. + /// + /// The type of the response data being received. + public static RespOperation Send( + this in RespContext context, + ReadOnlySpan command, + IRespParser parser) + { + var op = CreateOperation(context, command, false, RespFormatters.Empty, parser); + context.Connection.Write(op); + return op; + } + /// /// Creates an operation and synchronously writes it to the connection. /// @@ -105,6 +119,22 @@ public static RespOperation Send( return op; } + /// + /// Creates an operation and synchronously writes it to the connection. + /// + /// The type of state data required by the parser. + /// The type of the response data being received. + public static RespOperation Send( + this in RespContext context, + ReadOnlySpan command, + in TState state, + IRespParser parser) + { + var op = CreateOperation(context, command, false, RespFormatters.Empty, in state, parser); + context.Connection.Write(op); + return op; + } + /// /// Creates an operation and asynchronously writes it to the connection, awaiting the completion of the underlying write. /// From 1a22413a31f6ed8dcdcd51490aa3f94ff6f3b880 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 5 Sep 2025 17:01:19 +0100 Subject: [PATCH 059/108] prefix/suffix tokens --- .../IsExternalInit.cs | 7 + .../RespCommandGenerator.cs | 217 ++++++++++++++---- .../ProxiedDatabase.Connection.cs | 2 +- src/RESPite/RespPrefixAttribute.cs | 7 + src/RESPite/RespSuffixAttribute.cs | 7 + 5 files changed, 196 insertions(+), 44 deletions(-) create mode 100644 eng/StackExchange.Redis.Build/IsExternalInit.cs create mode 100644 src/RESPite/RespPrefixAttribute.cs create mode 100644 src/RESPite/RespSuffixAttribute.cs diff --git a/eng/StackExchange.Redis.Build/IsExternalInit.cs b/eng/StackExchange.Redis.Build/IsExternalInit.cs new file mode 100644 index 000000000..64f57fd4a --- /dev/null +++ b/eng/StackExchange.Redis.Build/IsExternalInit.cs @@ -0,0 +1,7 @@ +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices; +#if !NET5_0_OR_GREATER +internal static class IsExternalInit +{ +} +#endif diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index 8ed5c2434..d0be89390 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -11,6 +11,14 @@ namespace StackExchange.Redis.Build; [Generator(LanguageNames.CSharp)] public class RespCommandGenerator : IIncrementalGenerator { + [Flags] + private enum LiteralFlags + { + None = 0, + Suffix = 1 << 0, // else prefix + // optional, etc + } + public void Initialize(IncrementalGeneratorInitializationContext context) { var literals = context.SyntaxProvider @@ -39,6 +47,29 @@ private bool Predicate(SyntaxNode node, CancellationToken cancellationToken) return false; } + private readonly record struct LiteralTuple(string Token, LiteralFlags Flags); + + private readonly record struct ParameterTuple( + string Type, + string Name, + string Modifiers, + ParameterFlags Flags, + ImmutableArray Literals); + + private readonly record struct MethodTuple( + string Namespace, + string TypeName, + string ReturnType, + string MethodName, + string Command, + ImmutableArray Parameters, + string TypeModifiers, + string MethodModifiers, + string Context, + string? Formatter, + string? Parser, + string DebugNotes); + private static string GetFullName(ITypeSymbol type) => type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); @@ -47,6 +78,8 @@ private enum RESPite RespContext, RespCommandAttribute, RespKeyAttribute, + RespPrefixAttribute, + RespSuffixAttribute, } private static bool IsRESPite(ITypeSymbol? symbol, RESPite type) @@ -56,6 +89,8 @@ private static bool IsRESPite(ITypeSymbol? symbol, RESPite type) RESPite.RespContext => nameof(RESPite.RespContext), RESPite.RespCommandAttribute => nameof(RESPite.RespCommandAttribute), RESPite.RespKeyAttribute => nameof(RESPite.RespKeyAttribute), + RESPite.RespPrefixAttribute => nameof(RESPite.RespPrefixAttribute), + RESPite.RespSuffixAttribute => nameof(RESPite.RespSuffixAttribute), _ => type.ToString(), }; @@ -142,12 +177,9 @@ private static void AddNotes(ref string notes, string note) } } - private (string Namespace, string TypeName, string ReturnType, string MethodName, string Command, - ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> Parameters, string - TypeModifiers, string - MethodModifiers, string Context, string? Formatter, string? Parser, string DebugNotes) Transform( - GeneratorSyntaxContext ctx, - CancellationToken cancellationToken) + private MethodTuple Transform( + GeneratorSyntaxContext ctx, + CancellationToken cancellationToken) { // extract the name and value (defaults to name, but can be overridden via attribute) and the location if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not IMethodSymbol method) return default; @@ -209,9 +241,7 @@ private static void AddNotes(ref string notes, string note) } } - var parameters = - ImmutableArray.CreateBuilder<(string Type, string Name, string Modifiers, ParameterFlags Flags)>( - method.Parameters.Length); + var parameters = ImmutableArray.CreateBuilder(method.Parameters.Length); // get context from the available fields string? context = null; @@ -371,12 +401,58 @@ static bool IsIndirectRespContext(ITypeSymbol type, out string memberName) modifiers = "this " + modifiers; } - parameters.Add((GetFullName(param.Type), param.Name, modifiers, flags)); + List? literals = null; + + void AddLiteral(string token, LiteralFlags literalFlags) + { + (literals ??= new()).Add(new(token, literalFlags)); + } + + AddNotes(ref debugNote, $"checking {param.Name} for literals"); + foreach (var attrib in param.GetAttributes()) + { + if (IsRESPite(attrib.AttributeClass, RESPite.RespPrefixAttribute)) + { + if (attrib.ConstructorArguments.Length == 1) + { + if (attrib.ConstructorArguments[0].Value?.ToString() is { Length: > 0 } val) + { + AddNotes(ref debugNote, $"prefix {val}"); + AddLiteral(val, LiteralFlags.None); + } + } + } + + if (IsRESPite(attrib.AttributeClass, RESPite.RespSuffixAttribute)) + { + if (attrib.ConstructorArguments.Length == 1) + { + if (attrib.ConstructorArguments[0].Value?.ToString() is { Length: > 0 } val) + { + AddNotes(ref debugNote, $"suffix {val}"); + AddLiteral(val, LiteralFlags.Suffix); + } + } + } + } + + var literalArray = literals?.ToImmutableArray() ?? ImmutableArray.Empty; + parameters.Add(new(GetFullName(param.Type), param.Name, modifiers, flags, literalArray)); } var syntax = (MethodDeclarationSyntax)ctx.Node; - return (ns, parentType, returnType, method.Name, value, parameters.ToImmutable(), - TypeModifiers(method.ContainingType), syntax.Modifiers.ToString(), context ?? "", formatter, parser, + return new( + ns, + parentType, + returnType, + method.Name, + value, + parameters.ToImmutable(), + TypeModifiers(method.ContainingType), + syntax.Modifiers.ToString(), + context ?? "", + formatter, + parser, debugNote); static string TypeModifiers(ITypeSymbol type) @@ -427,13 +503,14 @@ private string GetVersion() return asm.GetName().Version?.ToString() ?? "??"; } + private static string CodeLiteral(string value) + => SyntaxFactory + .LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(value)) + .ToFullString(); + private void Generate( SourceProductionContext ctx, - ImmutableArray<(string Namespace, string TypeName, string ReturnType, string MethodName, string Command, - ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> Parameters, string - TypeModifiers, - string - MethodModifiers, string Context, string? Formatter, string? Parser, string DebugNotes)> methods) + ImmutableArray methods) { if (methods.IsDefaultOrEmpty) return; @@ -444,18 +521,30 @@ private void Generate( int indent = 0; // find the unique param types, so we can build helpers - Dictionary, (string Name, + Dictionary, (string Name, bool Shared)> formatters = new(FormatterComparer.Default); foreach (var method in methods) { - if (method.Formatter is not null || DataParameterCount(method.Parameters) < 2) + if (method.Formatter is not null) continue; // using explicit formatter + var count = DataParameterCount(method.Parameters); + switch (count) { - continue; // consumer should add their own extension method for the target type + case 0: continue; // no parameter to consider + case 1: + var p = FirstDataParameter(method.Parameters); + if (p.Literals.IsDefaultOrEmpty) + { + // no literals, and basic write scenario;consumer should add their own extension method + continue; + } + + break; } + // add a new formatter, or mark an existing formatter as shared var key = method.Parameters; if (!formatters.TryGetValue(key, out var existing)) { @@ -469,8 +558,18 @@ private void Generate( StringBuilder NewLine() => sb.AppendLine().Append(' ', Math.Max(indent * 4, 0)); NewLine().Append("using global::RESPite;"); + foreach (var method in methods) + { + if (HasAnyFlag(method.Parameters, ParameterFlags.CommandFlags)) + { + NewLine().Append("using global::RESPite.StackExchange.Redis;"); + break; + } + } + NewLine().Append("using global::System;"); NewLine().Append("using global::System.Threading.Tasks;"); + foreach (var grp in methods.GroupBy(l => (l.Namespace, l.TypeName, l.TypeModifiers))) { NewLine(); @@ -532,9 +631,7 @@ private void Generate( } // perform string escaping on the generated value (this includes the quotes, note) - var csValue = SyntaxFactory - .LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(method.Command)) - .ToFullString(); + var csValue = CodeLiteral(method.Command); WriteMethod(false); WriteMethod(true); @@ -607,6 +704,7 @@ void WriteMethod(bool asAsync) { sb.Append(".AsTask()"); } + sb.Append(';'); } @@ -628,7 +726,9 @@ void WriteMethod(bool asAsync) sb.Append(", ").Append(formatter).Append(", ").Append(parser).Append(")"); if (asAsync) { - sb.Append(HasAnyFlag(method.Parameters, ParameterFlags.CommandFlags) ? ".AsTask()" : ".AsValueTask()"); + sb.Append(HasAnyFlag(method.Parameters, ParameterFlags.CommandFlags) + ? ".AsTask()" + : ".AsValueTask()"); } else { @@ -642,6 +742,7 @@ void WriteMethod(bool asAsync) { sb.Append(method.Context).Append(".SyncTimeout"); } + sb.Append(")"); } @@ -686,12 +787,29 @@ void WriteMethod(bool asAsync) sb.Append(" request)"); NewLine().Append("{"); indent++; - var count = DataParameterCount(parameters); - sb = NewLine().Append("writer.WriteCommand(command, ").Append(count); + var count = DataParameterCount(parameters, out int literalCount); + sb = NewLine().Append("writer.WriteCommand(command, ").Append(count + literalCount); sb.Append(");"); + + void WritePrefix(ParameterTuple p) => WriteLiteral(p, false); + void WriteSuffix(ParameterTuple p) => WriteLiteral(p, true); + + void WriteLiteral(ParameterTuple p, bool suffix) + { + LiteralFlags match = suffix ? LiteralFlags.Suffix : LiteralFlags.None; + foreach (var literal in p.Literals) + { + if ((literal.Flags & LiteralFlags.Suffix) == match) + { + sb = NewLine().Append("writer.WriteBulkString(").Append(CodeLiteral(literal.Token)).Append("u8);"); + } + } + } + if (count == 1) { var p = FirstDataParameter(parameters); + WritePrefix(p); sb = NewLine().Append("writer."); if (p.Type is "global::StackExchange.Redis.RedisValue" or "global::StackExchange.Redis.RedisKey") { @@ -703,6 +821,7 @@ void WriteMethod(bool asAsync) } sb.Append("(request);"); + WriteSuffix(p); } else { @@ -711,6 +830,7 @@ void WriteMethod(bool asAsync) { if ((parameter.Flags & ParameterFlags.DataParameter) == ParameterFlags.DataParameter) { + WritePrefix(parameter); sb = NewLine().Append("writer."); if (parameter.Type is "global::StackExchange.Redis.RedisValue" or "global::StackExchange.Redis.RedisKey") @@ -734,6 +854,7 @@ void WriteMethod(bool asAsync) sb.Append(");"); index++; + WriteSuffix(parameter); } } @@ -750,7 +871,7 @@ void WriteMethod(bool asAsync) ctx.AddSource(GetType().Name + ".generated.cs", sb.ToString()); static void WriteTuple( - ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> parameters, + ImmutableArray parameters, StringBuilder sb, TupleMode mode) { @@ -797,7 +918,9 @@ static void WriteTuple( } } - private static bool HasAnyFlag(ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> parameters, ParameterFlags any) + private static bool HasAnyFlag( + ImmutableArray parameters, + ParameterFlags any) { foreach (var p in parameters) { @@ -808,19 +931,23 @@ private static bool HasAnyFlag(ImmutableArray<(string Type, string Name, string } private static string? InbuiltFormatter( - ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> parameters) + ImmutableArray parameters) { if (DataParameterCount(parameters) == 1) { var p = FirstDataParameter(parameters); - return InbuiltFormatter(p.Type, (p.Flags & ParameterFlags.Key) != 0); + if (p.Literals.IsDefaultOrEmpty) + { + // can only use the inbuilt formatter if there are no literals + return InbuiltFormatter(p.Type, (p.Flags & ParameterFlags.Key) != 0); + } } return null; } - private static (string Type, string Name, string Modifiers, ParameterFlags Flags) FirstDataParameter( - ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> parameters) + private static ParameterTuple FirstDataParameter( + ImmutableArray parameters) { if (!parameters.IsDefaultOrEmpty) { @@ -833,18 +960,24 @@ private static (string Type, string Name, string Modifiers, ParameterFlags Flags } } - return Array.Empty<(string Type, string Name, string Modifiers, ParameterFlags Flags)>().First(); + return Array.Empty().First(); } private static int DataParameterCount( - ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> parameters) + ImmutableArray parameters) + => DataParameterCount(parameters, out _); + + private static int DataParameterCount( + ImmutableArray parameters, out int literalCount) { + literalCount = 0; if (parameters.IsDefaultOrEmpty) return 0; int count = 0; foreach (var parameter in parameters) { if ((parameter.Flags & ParameterFlags.DataParameter) == ParameterFlags.DataParameter) { + if (!parameter.Literals.IsDefaultOrEmpty) literalCount += parameter.Literals.Length; count++; } } @@ -915,21 +1048,20 @@ private enum ParameterFlags Data = 1 << 1, DataParameter = Data | Parameter, Key = 1 << 2, - Literal = 1 << 3, - CommandFlags = 1 << 4, + CommandFlags = 1 << 3, } // compares whether a formatter can be shared, which depends on the key index and types (not names) private sealed class FormatterComparer - : IEqualityComparer> + : IEqualityComparer> { private FormatterComparer() { } public static readonly FormatterComparer Default = new(); public bool Equals( - ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> x, - ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> y) + ImmutableArray x, + ImmutableArray y) { if (x.Length != y.Length) return false; for (int i = 0; i < x.Length; i++) @@ -938,20 +1070,19 @@ public bool Equals( var py = y[i]; if (px.Type != py.Type || px.Flags != py.Flags) return false; // literals need to match by name too - if ((px.Flags & ParameterFlags.Literal) != 0 - && px.Name != py.Name) return false; + if (!px.Literals.SequenceEqual(py.Literals)) return false; } return true; } public int GetHashCode( - ImmutableArray<(string Type, string Name, string Modifiers, ParameterFlags Flags)> obj) + ImmutableArray obj) { var hash = obj.Length; foreach (var p in obj) { - hash ^= p.Type.GetHashCode() ^ (int)p.Flags; + hash ^= p.Type.GetHashCode() ^ (int)p.Flags ^ p.Literals.Length; } return hash; diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs index 3caeacbfc..2bcdc74a7 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs @@ -55,5 +55,5 @@ public void KeyMigrate( // Debug [RespCommand("debug")] - public partial RedisValue DebugObject(RedisKey key, CommandFlags flags = CommandFlags.None); + public partial RedisValue DebugObject([RespPrefix("object")] RedisKey key, CommandFlags flags = CommandFlags.None); } diff --git a/src/RESPite/RespPrefixAttribute.cs b/src/RESPite/RespPrefixAttribute.cs new file mode 100644 index 000000000..7a6f51c9a --- /dev/null +++ b/src/RESPite/RespPrefixAttribute.cs @@ -0,0 +1,7 @@ +namespace RESPite; + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] +public sealed class RespPrefixAttribute(string token) : Attribute +{ + public string Token => token; +} diff --git a/src/RESPite/RespSuffixAttribute.cs b/src/RESPite/RespSuffixAttribute.cs new file mode 100644 index 000000000..e8ab23661 --- /dev/null +++ b/src/RESPite/RespSuffixAttribute.cs @@ -0,0 +1,7 @@ +namespace RESPite; + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] +public sealed class RespSuffixAttribute(string token) : Attribute +{ + public string Token => token; +} From 2e0c1fea72788c6a88ae3324f8d79d8d2deda89e Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 5 Sep 2025 22:38:16 +0100 Subject: [PATCH 060/108] replace immutable array --- StackExchange.Redis.sln.DotSettings | 2 + eng/StackExchange.Redis.Build/EasyArray.cs | 55 +++++++++++++++ .../RespCommandGenerator.cs | 70 +++++++++---------- 3 files changed, 92 insertions(+), 35 deletions(-) create mode 100644 eng/StackExchange.Redis.Build/EasyArray.cs diff --git a/StackExchange.Redis.sln.DotSettings b/StackExchange.Redis.sln.DotSettings index 5a7779446..f8eb5deea 100644 --- a/StackExchange.Redis.sln.DotSettings +++ b/StackExchange.Redis.sln.DotSettings @@ -1,6 +1,8 @@  OK PONG + RES + SE True True True \ No newline at end of file diff --git a/eng/StackExchange.Redis.Build/EasyArray.cs b/eng/StackExchange.Redis.Build/EasyArray.cs new file mode 100644 index 000000000..400438e1c --- /dev/null +++ b/eng/StackExchange.Redis.Build/EasyArray.cs @@ -0,0 +1,55 @@ +using System.Collections; + +namespace StackExchange.Redis.Build; + +/// +/// Think ImmutableArray{T}, but with structural equality. +/// +/// The data being wrapped. +internal readonly struct EasyArray(T[]? array) : IEquatable>, IEnumerable +{ + public static readonly EasyArray Empty = new([]); + private readonly T[]? _array = array ?? []; + public int Length => _array?.Length ?? 0; + public ref readonly T this[int index] => ref _array![index]; + public ReadOnlySpan Span => _array.AsSpan(); + public bool IsEmpty => Length == 0; + + public static bool operator ==(EasyArray x, EasyArray y) + => x.Equals(y); + + public static bool operator !=(EasyArray x, EasyArray y) + => x.Equals(y); + + public bool Equals(EasyArray other) + { + T[]? tArr = this._array, oArr = other._array; + if (tArr is null) return oArr is null || oArr.Length == 0; + if (oArr is null) return tArr.Length == 0; + + if (tArr.Length != oArr.Length) return false; + for (int i = 0; i < tArr.Length; i++) + { + if (ReferenceEquals(tArr[i], oArr[i])) + return false; + } + return true; + } + + public IEnumerator GetEnumerator() => ((IEnumerable)(_array ?? [])).GetEnumerator(); + + public override bool Equals(object? obj) + => obj is EasyArray other && Equals(other); + + public override int GetHashCode() + { + var arr = _array; + if (arr is null) return 0; + // use length and first item for a quick hash + return arr.Length == 0 + ? 0 + : arr.Length ^ EqualityComparer.Default.GetHashCode(arr[0]); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index d0be89390..33ed2b416 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -54,7 +54,7 @@ private readonly record struct ParameterTuple( string Name, string Modifiers, ParameterFlags Flags, - ImmutableArray Literals); + EasyArray Literals); private readonly record struct MethodTuple( string Namespace, @@ -62,7 +62,7 @@ private readonly record struct MethodTuple( string ReturnType, string MethodName, string Command, - ImmutableArray Parameters, + EasyArray Parameters, string TypeModifiers, string MethodModifiers, string Context, @@ -241,7 +241,7 @@ private MethodTuple Transform( } } - var parameters = ImmutableArray.CreateBuilder(method.Parameters.Length); + var parameters = new List(method.Parameters.Length); // get context from the available fields string? context = null; @@ -321,7 +321,7 @@ private MethodTuple Transform( } } - // See whether instead of x (param, etc) *being* a RespContext, it could be something that *provides* + // See whether instead of x (param, etc.) *being* a RespContext, it could be something that *provides* // a RespContext; this is especially useful for using punned structs (that just wrap a RespContext) to // narrow the methods into logical groups, i.e. "strings", "hashes", etc. static bool IsIndirectRespContext(ITypeSymbol type, out string memberName) @@ -436,7 +436,7 @@ void AddLiteral(string token, LiteralFlags literalFlags) } } - var literalArray = literals?.ToImmutableArray() ?? ImmutableArray.Empty; + var literalArray = literals is null ? EasyArray.Empty : new(literals.ToArray()); parameters.Add(new(GetFullName(param.Type), param.Name, modifiers, flags, literalArray)); } @@ -447,7 +447,7 @@ void AddLiteral(string token, LiteralFlags literalFlags) returnType, method.Name, value, - parameters.ToImmutable(), + new(parameters.ToArray()), TypeModifiers(method.ContainingType), syntax.Modifiers.ToString(), context ?? "", @@ -521,12 +521,12 @@ private void Generate( int indent = 0; // find the unique param types, so we can build helpers - Dictionary, (string Name, + Dictionary, (string Name, bool Shared)> formatters = new(FormatterComparer.Default); - foreach (var method in methods) + foreach (var method in methods.AsSpan()) { if (method.Formatter is not null) continue; // using explicit formatter var count = DataParameterCount(method.Parameters); @@ -535,7 +535,7 @@ private void Generate( case 0: continue; // no parameter to consider case 1: var p = FirstDataParameter(method.Parameters); - if (p.Literals.IsDefaultOrEmpty) + if (p.Literals.IsEmpty) { // no literals, and basic write scenario;consumer should add their own extension method continue; @@ -558,7 +558,7 @@ private void Generate( StringBuilder NewLine() => sb.AppendLine().Append(' ', Math.Max(indent * 4, 0)); NewLine().Append("using global::RESPite;"); - foreach (var method in methods) + foreach (var method in methods.AsSpan()) { if (HasAnyFlag(method.Parameters, ParameterFlags.CommandFlags)) { @@ -586,11 +586,11 @@ private void Generate( { if (grp.Key.TypeName.Contains('.')) // nested types { - var toks = grp.Key.TypeName.Split('.'); - for (var i = 0; i < toks.Length; i++) + var tokens = grp.Key.TypeName.Split('.'); + for (var i = 0; i < tokens.Length; i++) { - var part = toks[i]; - if (i == toks.Length - 1) + var part = tokens[i]; + if (i == tokens.Length - 1) { NewLine().Append(grp.Key.TypeModifiers).Append(' ').Append(part); } @@ -797,7 +797,7 @@ void WriteMethod(bool asAsync) void WriteLiteral(ParameterTuple p, bool suffix) { LiteralFlags match = suffix ? LiteralFlags.Suffix : LiteralFlags.None; - foreach (var literal in p.Literals) + foreach (var literal in p.Literals.Span) { if ((literal.Flags & LiteralFlags.Suffix) == match) { @@ -826,7 +826,7 @@ void WriteLiteral(ParameterTuple p, bool suffix) else { int index = 0; - foreach (var parameter in parameters) + foreach (var parameter in parameters.Span) { if ((parameter.Flags & ParameterFlags.DataParameter) == ParameterFlags.DataParameter) { @@ -871,7 +871,7 @@ void WriteLiteral(ParameterTuple p, bool suffix) ctx.AddSource(GetType().Name + ".generated.cs", sb.ToString()); static void WriteTuple( - ImmutableArray parameters, + EasyArray parameters, StringBuilder sb, TupleMode mode) { @@ -886,7 +886,7 @@ static void WriteTuple( sb.Append('('); int index = 0; - foreach (var param in parameters) + foreach (var param in parameters.Span) { if ((param.Flags & ParameterFlags.DataParameter) != ParameterFlags.DataParameter) { @@ -919,10 +919,10 @@ static void WriteTuple( } private static bool HasAnyFlag( - ImmutableArray parameters, + EasyArray parameters, ParameterFlags any) { - foreach (var p in parameters) + foreach (var p in parameters.Span) { if ((p.Flags & any) != 0) return true; } @@ -931,12 +931,12 @@ private static bool HasAnyFlag( } private static string? InbuiltFormatter( - ImmutableArray parameters) + EasyArray parameters) { if (DataParameterCount(parameters) == 1) { var p = FirstDataParameter(parameters); - if (p.Literals.IsDefaultOrEmpty) + if (p.Literals.IsEmpty) { // can only use the inbuilt formatter if there are no literals return InbuiltFormatter(p.Type, (p.Flags & ParameterFlags.Key) != 0); @@ -947,11 +947,11 @@ private static bool HasAnyFlag( } private static ParameterTuple FirstDataParameter( - ImmutableArray parameters) + EasyArray parameters) { - if (!parameters.IsDefaultOrEmpty) + if (!parameters.IsEmpty) { - foreach (var parameter in parameters) + foreach (var parameter in parameters.Span) { if ((parameter.Flags & ParameterFlags.DataParameter) == ParameterFlags.DataParameter) { @@ -964,20 +964,20 @@ private static ParameterTuple FirstDataParameter( } private static int DataParameterCount( - ImmutableArray parameters) + EasyArray parameters) => DataParameterCount(parameters, out _); private static int DataParameterCount( - ImmutableArray parameters, out int literalCount) + EasyArray parameters, out int literalCount) { literalCount = 0; - if (parameters.IsDefaultOrEmpty) return 0; + if (parameters.IsEmpty) return 0; int count = 0; - foreach (var parameter in parameters) + foreach (var parameter in parameters.Span) { if ((parameter.Flags & ParameterFlags.DataParameter) == ParameterFlags.DataParameter) { - if (!parameter.Literals.IsDefaultOrEmpty) literalCount += parameter.Literals.Length; + if (!parameter.Literals.IsEmpty) literalCount += parameter.Literals.Length; count++; } } @@ -1054,14 +1054,14 @@ private enum ParameterFlags // compares whether a formatter can be shared, which depends on the key index and types (not names) private sealed class FormatterComparer - : IEqualityComparer> + : IEqualityComparer> { private FormatterComparer() { } public static readonly FormatterComparer Default = new(); public bool Equals( - ImmutableArray x, - ImmutableArray y) + EasyArray x, + EasyArray y) { if (x.Length != y.Length) return false; for (int i = 0; i < x.Length; i++) @@ -1077,10 +1077,10 @@ public bool Equals( } public int GetHashCode( - ImmutableArray obj) + EasyArray obj) { var hash = obj.Length; - foreach (var p in obj) + foreach (var p in obj.Span) { hash ^= p.Type.GetHashCode() ^ (int)p.Flags ^ p.Literals.Length; } From 4d77cb43dffdcc18905008afc05a16bce481e753 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 6 Sep 2025 10:25:10 +0100 Subject: [PATCH 061/108] implement some basic commands for benchark --- .../RespCommandGenerator.cs | 26 +- src/RESPite.Benchmark/BridgeBenchmark.cs | 15 + src/RESPite.Benchmark/OldCoreBenchmark.cs | 287 +---------------- src/RESPite.Benchmark/OldCoreBenchmarkBase.cs | 291 ++++++++++++++++++ src/RESPite.Benchmark/Program.cs | 3 + .../RESPite.Benchmark.csproj | 5 +- .../ProxiedDatabase.Key.cs | 7 +- .../ProxiedDatabase.List.cs | 59 ++-- .../ProxiedDatabase.Set.cs | 14 +- .../ProxiedDatabase.String.cs | 117 ++++--- .../RespParsers.cs | 14 +- 11 files changed, 471 insertions(+), 367 deletions(-) create mode 100644 src/RESPite.Benchmark/BridgeBenchmark.cs create mode 100644 src/RESPite.Benchmark/OldCoreBenchmarkBase.cs diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index 33ed2b416..c3af3cfa2 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -222,22 +222,25 @@ private MethodTuple Transform( if (attrib.ConstructorArguments[0].Value?.ToString() is { Length: > 0 } val) { value = val; - break; } + } - foreach (var tuple in attrib.NamedArguments) + foreach (var tuple in attrib.NamedArguments) + { + switch (tuple.Key) { - switch (tuple.Key) - { - case "Formatter": - formatter = tuple.Value.Value?.ToString(); - break; - case "Parser": - parser = tuple.Value.Value?.ToString(); - break; - } + case "Formatter": + formatter = tuple.Value.Value?.ToString(); + AddNotes(ref debugNote, $"custom formatter: '{formatter}'"); + break; + case "Parser": + parser = tuple.Value.Value?.ToString(); + AddNotes(ref debugNote, $"custom parser: '{parser}'"); + break; } } + + break; // we don't expect another [RespCommand] } } @@ -1019,6 +1022,7 @@ private static int DataParameterCount( "global::RESPite.RespParsers.ResponseSummary" => RespParsersPrefix + "ResponseSummary.Parser", "global::StackExchange.Redis.RedisKey" => "global::RESPite.StackExchange.Redis.RespParsers.RedisKey", "global::StackExchange.Redis.RedisValue" => "global::RESPite.StackExchange.Redis.RespParsers.RedisValue", + "global::StackExchange.Redis.Lease" => "global::RESPite.StackExchange.Redis.RespParsers.BytesLease", _ => null, }; diff --git a/src/RESPite.Benchmark/BridgeBenchmark.cs b/src/RESPite.Benchmark/BridgeBenchmark.cs new file mode 100644 index 000000000..30ac8078a --- /dev/null +++ b/src/RESPite.Benchmark/BridgeBenchmark.cs @@ -0,0 +1,15 @@ +using RESPite.StackExchange.Redis; +using StackExchange.Redis; + +namespace RESPite.Benchmark; + +public sealed class BridgeBenchmark(string[] args) : OldCoreBenchmarkBase(args) +{ + public override string ToString() => "bridge SE.Redis"; + protected override IConnectionMultiplexer Create(int port) + { + var obj = new RespMultiplexer(); + obj.Connect($"127.0.0.1:{Port}"); + return obj; + } +} diff --git a/src/RESPite.Benchmark/OldCoreBenchmark.cs b/src/RESPite.Benchmark/OldCoreBenchmark.cs index 0bf650b88..630460e2b 100644 --- a/src/RESPite.Benchmark/OldCoreBenchmark.cs +++ b/src/RESPite.Benchmark/OldCoreBenchmark.cs @@ -1,290 +1,9 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Threading.Tasks; -using StackExchange.Redis; +using StackExchange.Redis; namespace RESPite.Benchmark; -public class OldCoreBenchmark : BenchmarkBase +public sealed class OldCoreBenchmark(string[] args) : OldCoreBenchmarkBase(args) { public override string ToString() => "legacy SE.Redis"; - - private readonly IConnectionMultiplexer _connectionMultiplexer; - private readonly IDatabase _client; - private readonly KeyValuePair[] _pairs; - - public OldCoreBenchmark(string[] args) : base(args) - { - _connectionMultiplexer = ConnectionMultiplexer.Connect($"127.0.0.1:{Port}"); - _client = _connectionMultiplexer.GetDatabase(); - _pairs = new KeyValuePair[10]; - - for (var i = 0; i < 10; i++) - { - _pairs[i] = new($"{"key:__rand_int__"}{i}", Payload); - } - } - - protected override async Task OnCleanupAsync(IDatabaseAsync client) - { - foreach (var pair in _pairs) - { - await client.KeyDeleteAsync(pair.Key); - } - } - - protected override Task InitAsync(IDatabaseAsync client) => client.PingAsync(); - - public override void Dispose() - { - _connectionMultiplexer.Dispose(); - } - - protected override IDatabaseAsync GetClient(int index) => _client; - protected override Task DeleteAsync(IDatabaseAsync client, string key) => client.KeyDeleteAsync(key); - - public override async Task RunAll() - { - await InitAsync().ConfigureAwait(false); - // await RunAsync(PingInline).ConfigureAwait(false); - await RunAsync(null, PingBulk).ConfigureAwait(false); - - await RunAsync(GetSetKey, Set).ConfigureAwait(false); - await RunAsync(GetSetKey, Get, GetInit).ConfigureAwait(false); - await RunAsync(CounterKey, Incr).ConfigureAwait(false); - await RunAsync(ListKey, LPush).ConfigureAwait(false); - await RunAsync(ListKey, RPush).ConfigureAwait(false); - await RunAsync(ListKey, LPop, LPopInit).ConfigureAwait(false); - await RunAsync(ListKey, RPop, LPopInit).ConfigureAwait(false); - await RunAsync(SetKey, SAdd).ConfigureAwait(false); - await RunAsync(HashKey, HSet).ConfigureAwait(false); - await RunAsync(SetKey, SPop, SPopInit).ConfigureAwait(false); - await RunAsync(SortedSetKey, ZAdd).ConfigureAwait(false); - await RunAsync(SortedSetKey, ZPopMin, ZPopMinInit).ConfigureAwait(false); - await RunAsync(null, MSet).ConfigureAwait(false); - await RunAsync(StreamKey, XAdd).ConfigureAwait(false); - - // leave until last, they're slower - await RunAsync(ListKey, LRange100, LRangeInit).ConfigureAwait(false); - await RunAsync(ListKey, LRange300, LRangeInit).ConfigureAwait(false); - await RunAsync(ListKey, LRange500, LRangeInit).ConfigureAwait(false); - await RunAsync(ListKey, LRange600, LRangeInit).ConfigureAwait(false); - - await CleanupAsync().ConfigureAwait(false); - } - - protected override IDatabaseAsync CreateBatch(IDatabaseAsync client) => ((IDatabase)client).CreateBatch(); - - protected override ValueTask Flush(IDatabaseAsync client) - { - if (client is IBatch batch) - { - batch.Execute(); - } - - return default; - } - - protected override async Task RunBasicLoopAsync(int clientId) - { - // The purpose of this is to represent a more realistic loop using natural code - // rather than code that is drowning in test infrastructure. - var client = (IDatabase)GetClient(clientId); // need IDatabase for CreateBatch - var depth = PipelineDepth; - int tickCount = 0; // this is just so we don't query DateTime. - var tmp = await client.StringGetAsync(CounterKey).ConfigureAwait(false); - long previousValue = tmp.IsNull ? 0 : (long)tmp, currentValue = previousValue; - var watch = Stopwatch.StartNew(); - long previousMillis = watch.ElapsedMilliseconds; - - bool Tick() - { - var currentMillis = watch.ElapsedMilliseconds; - var elapsedMillis = currentMillis - previousMillis; - if (elapsedMillis >= 1000) - { - if (clientId == 0) // only one client needs to update the UI - { - var qty = currentValue - previousValue; - var seconds = elapsedMillis / 1000.0; - Console.WriteLine( - $"{qty:#,###,##0} ops in {seconds:#0.00}s, {qty / seconds:#,###,##0}/s\ttotal: {currentValue:#,###,###,##0}"); - - // reset for next UI update - previousValue = currentValue; - previousMillis = currentMillis; - } - - if (currentMillis >= 20_000) - { - if (clientId == 0) - { - Console.WriteLine(); - Console.WriteLine( - $"\t Overall: {currentValue:#,###,###,##0} ops in {currentMillis / 1000:#0.00}s, {currentValue / (currentMillis / 1000.0):#,###,##0}/s"); - Console.WriteLine(); - } - - return true; // stop after some time - } - } - - tickCount = 0; - return false; - } - - if (depth <= 1) - { - while (true) - { - currentValue = await client.StringIncrementAsync(CounterKey).ConfigureAwait(false); - - if (++tickCount >= 1000 && Tick()) break; // only check whether to output every N iterations - } - } - else - { - Task[] pending = new Task[depth]; - var batch = client.CreateBatch(depth); - while (true) - { - for (int i = 0; i < depth; i++) - { - pending[i] = batch.StringIncrementAsync(CounterKey); - } - - batch.Execute(); - for (int i = 0; i < depth; i++) - { - currentValue = await pending[i].ConfigureAwait(false); - } - - tickCount += depth; - if (tickCount >= 1000 && Tick()) break; // only check whether to output every N iterations - } - } - } - - [DisplayName("GET")] - private ValueTask Get(IDatabaseAsync client) => GetAndMeasureString(client); - - private async ValueTask GetAndMeasureString(IDatabaseAsync client) - { - using var lease = await client.StringGetLeaseAsync(GetSetKey).ConfigureAwait(false); - return lease?.Length ?? -1; - } - - [DisplayName("SET")] - private ValueTask Set(IDatabaseAsync client) => client.StringSetAsync(GetSetKey, Payload).AsValueTask(); - - private ValueTask GetInit(IDatabaseAsync client) => - client.StringSetAsync(GetSetKey, Payload).AsUntypedValueTask(); - - private ValueTask PingInline(IDatabaseAsync client) => client.PingAsync().AsValueTask(); - - [DisplayName("PING_BULK")] - private ValueTask PingBulk(IDatabaseAsync client) => client.PingAsync().AsValueTask(); - - [DisplayName("INCR")] - private ValueTask Incr(IDatabaseAsync client) => client.StringIncrementAsync(CounterKey).AsValueTask(); - - [DisplayName("HSET")] - private ValueTask HSet(IDatabaseAsync client) => - client.HashSetAsync(HashKey, "element:__rand_int__", Payload).AsValueTask(); - - [DisplayName("SADD")] - private ValueTask SAdd(IDatabaseAsync client) => - client.SetAddAsync(SetKey, "element:__rand_int__").AsValueTask(); - - [DisplayName("LPUSH")] - private ValueTask LPush(IDatabaseAsync client) => client.ListLeftPushAsync(ListKey, Payload).AsValueTask(); - - [DisplayName("RPUSH")] - private ValueTask RPush(IDatabaseAsync client) => client.ListRightPushAsync(ListKey, Payload).AsValueTask(); - - [DisplayName("LPOP")] - private ValueTask LPop(IDatabaseAsync client) => client.ListLeftPopAsync(ListKey).AsValueTask(); - - [DisplayName("RPOP")] - private ValueTask RPop(IDatabaseAsync client) => client.ListRightPopAsync(ListKey).AsValueTask(); - - private ValueTask LPopInit(IDatabaseAsync client) => - client.ListLeftPushAsync(ListKey, Payload).AsUntypedValueTask(); - - [DisplayName("SPOP")] - private ValueTask SPop(IDatabaseAsync client) => client.SetPopAsync(SetKey).AsValueTask(); - - private ValueTask SPopInit(IDatabaseAsync client) => - client.SetAddAsync(SetKey, "element:__rand_int__").AsUntypedValueTask(); - - [DisplayName("ZADD")] - private ValueTask ZAdd(IDatabaseAsync client) => - client.SortedSetAddAsync(SortedSetKey, "element:__rand_int__", 0).AsValueTask(); - - [DisplayName("ZPOPMIN")] - private ValueTask ZPopMin(IDatabaseAsync client) => CountAsync(client.SortedSetPopAsync(SortedSetKey, 1)); - - private async ValueTask ZPopMinInit(IDatabaseAsync client) - { - int ops = TotalOperations; - var rand = new Random(); - for (int i = 0; i < ops; i++) - { - await client.SortedSetAddAsync(SortedSetKey, "element:__rand_int__", (rand.NextDouble() * 2000) - 1000) - .ConfigureAwait(false); - } - } - - [DisplayName("MSET")] - private ValueTask MSet(IDatabaseAsync client) => client.StringSetAsync(_pairs).AsValueTask(); - - [DisplayName("XADD")] - private ValueTask XAdd(IDatabaseAsync client) => - client.StreamAddAsync(StreamKey, "myfield", Payload).AsValueTask(); - - [DisplayName("LRANGE_100")] - private ValueTask LRange100(IDatabaseAsync client) => CountAsync(client.ListRangeAsync(ListKey, 0, 99)); - - [DisplayName("LRANGE_300")] - private ValueTask LRange300(IDatabaseAsync client) => CountAsync(client.ListRangeAsync(ListKey, 0, 299)); - - [DisplayName("LRANGE_500")] - private ValueTask LRange500(IDatabaseAsync client) => CountAsync(client.ListRangeAsync(ListKey, 0, 499)); - - [DisplayName("LRANGE_600")] - private ValueTask LRange600(IDatabaseAsync client) => - CountAsync(client.ListRangeAsync(ListKey, 0, 599)); - - private static ValueTask CountAsync(Task task) => task.ContinueWith( - t => t.Result.Length, TaskContinuationOptions.ExecuteSynchronously).AsValueTask(); - - private async ValueTask LRangeInit(IDatabaseAsync client) - { - var ops = TotalOperations; - for (int i = 0; i < ops; i++) - { - await client.ListLeftPushAsync(ListKey, Payload); - } - } -} - -internal static class TaskExtensions -{ - public static ValueTask AsValueTask(this Task task) => new(task); - public static ValueTask AsUntypedValueTask(this Task task) => new(task); - public static ValueTask AsValueTask(this Task task) => new(task); - - public static ValueTask AsUntypedValueTask(this ValueTask task) - { - if (!task.IsCompleted) return Awaited(task); - task.GetAwaiter().GetResult(); - return default; - - static async ValueTask Awaited(ValueTask task) - { - await task.ConfigureAwait(false); - } - } + protected override IConnectionMultiplexer Create(int port) => ConnectionMultiplexer.Connect($"127.0.0.1:{Port}"); } diff --git a/src/RESPite.Benchmark/OldCoreBenchmarkBase.cs b/src/RESPite.Benchmark/OldCoreBenchmarkBase.cs new file mode 100644 index 000000000..0cbf0bed6 --- /dev/null +++ b/src/RESPite.Benchmark/OldCoreBenchmarkBase.cs @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Threading.Tasks; +using StackExchange.Redis; + +namespace RESPite.Benchmark; + +public abstract class OldCoreBenchmarkBase : BenchmarkBase +{ + private readonly IConnectionMultiplexer _connectionMultiplexer; + private readonly IDatabase _client; + private readonly KeyValuePair[] _pairs; + + public OldCoreBenchmarkBase(string[] args) : base(args) + { + // ReSharper disable once VirtualMemberCallInConstructor + _connectionMultiplexer = Create(Port); + _client = _connectionMultiplexer.GetDatabase(); + _pairs = new KeyValuePair[10]; + + for (var i = 0; i < 10; i++) + { + _pairs[i] = new($"{"key:__rand_int__"}{i}", Payload); + } + } + + protected abstract IConnectionMultiplexer Create(int port); + + protected override async Task OnCleanupAsync(IDatabaseAsync client) + { + foreach (var pair in _pairs) + { + await client.KeyDeleteAsync(pair.Key); + } + } + + protected override Task InitAsync(IDatabaseAsync client) => client.PingAsync(); + + public override void Dispose() + { + _connectionMultiplexer.Dispose(); + } + + protected override IDatabaseAsync GetClient(int index) => _client; + protected override Task DeleteAsync(IDatabaseAsync client, string key) => client.KeyDeleteAsync(key); + + public override async Task RunAll() + { + await InitAsync().ConfigureAwait(false); + // await RunAsync(PingInline).ConfigureAwait(false); + await RunAsync(null, PingBulk).ConfigureAwait(false); + + await RunAsync(GetSetKey, Set).ConfigureAwait(false); + await RunAsync(GetSetKey, Get, GetInit).ConfigureAwait(false); + await RunAsync(CounterKey, Incr).ConfigureAwait(false); + await RunAsync(ListKey, LPush).ConfigureAwait(false); + await RunAsync(ListKey, RPush).ConfigureAwait(false); + await RunAsync(ListKey, LPop, LPopInit).ConfigureAwait(false); + await RunAsync(ListKey, RPop, LPopInit).ConfigureAwait(false); + await RunAsync(SetKey, SAdd).ConfigureAwait(false); + await RunAsync(HashKey, HSet).ConfigureAwait(false); + await RunAsync(SetKey, SPop, SPopInit).ConfigureAwait(false); + await RunAsync(SortedSetKey, ZAdd).ConfigureAwait(false); + await RunAsync(SortedSetKey, ZPopMin, ZPopMinInit).ConfigureAwait(false); + await RunAsync(null, MSet).ConfigureAwait(false); + await RunAsync(StreamKey, XAdd).ConfigureAwait(false); + + // leave until last, they're slower + await RunAsync(ListKey, LRange100, LRangeInit).ConfigureAwait(false); + await RunAsync(ListKey, LRange300, LRangeInit).ConfigureAwait(false); + await RunAsync(ListKey, LRange500, LRangeInit).ConfigureAwait(false); + await RunAsync(ListKey, LRange600, LRangeInit).ConfigureAwait(false); + + await CleanupAsync().ConfigureAwait(false); + } + + protected override IDatabaseAsync CreateBatch(IDatabaseAsync client) => ((IDatabase)client).CreateBatch(); + + protected override ValueTask Flush(IDatabaseAsync client) + { + if (client is IBatch batch) + { + batch.Execute(); + } + + return default; + } + + protected override async Task RunBasicLoopAsync(int clientId) + { + // The purpose of this is to represent a more realistic loop using natural code + // rather than code that is drowning in test infrastructure. + var client = (IDatabase)GetClient(clientId); // need IDatabase for CreateBatch + var depth = PipelineDepth; + int tickCount = 0; // this is just so we don't query DateTime. + var tmp = await client.StringGetAsync(CounterKey).ConfigureAwait(false); + long previousValue = tmp.IsNull ? 0 : (long)tmp, currentValue = previousValue; + var watch = Stopwatch.StartNew(); + long previousMillis = watch.ElapsedMilliseconds; + + bool Tick() + { + var currentMillis = watch.ElapsedMilliseconds; + var elapsedMillis = currentMillis - previousMillis; + if (elapsedMillis >= 1000) + { + if (clientId == 0) // only one client needs to update the UI + { + var qty = currentValue - previousValue; + var seconds = elapsedMillis / 1000.0; + Console.WriteLine( + $"{qty:#,###,##0} ops in {seconds:#0.00}s, {qty / seconds:#,###,##0}/s\ttotal: {currentValue:#,###,###,##0}"); + + // reset for next UI update + previousValue = currentValue; + previousMillis = currentMillis; + } + + if (currentMillis >= 20_000) + { + if (clientId == 0) + { + Console.WriteLine(); + Console.WriteLine( + $"\t Overall: {currentValue:#,###,###,##0} ops in {currentMillis / 1000:#0.00}s, {currentValue / (currentMillis / 1000.0):#,###,##0}/s"); + Console.WriteLine(); + } + + return true; // stop after some time + } + } + + tickCount = 0; + return false; + } + + if (depth <= 1) + { + while (true) + { + currentValue = await client.StringIncrementAsync(CounterKey).ConfigureAwait(false); + + if (++tickCount >= 1000 && Tick()) break; // only check whether to output every N iterations + } + } + else + { + Task[] pending = new Task[depth]; + var batch = client.CreateBatch(depth); + while (true) + { + for (int i = 0; i < depth; i++) + { + pending[i] = batch.StringIncrementAsync(CounterKey); + } + + batch.Execute(); + for (int i = 0; i < depth; i++) + { + currentValue = await pending[i].ConfigureAwait(false); + } + + tickCount += depth; + if (tickCount >= 1000 && Tick()) break; // only check whether to output every N iterations + } + } + } + + [DisplayName("GET")] + private ValueTask Get(IDatabaseAsync client) => GetAndMeasureString(client); + + private async ValueTask GetAndMeasureString(IDatabaseAsync client) + { + using var lease = await client.StringGetLeaseAsync(GetSetKey).ConfigureAwait(false); + return lease?.Length ?? -1; + } + + [DisplayName("SET")] + private ValueTask Set(IDatabaseAsync client) => client.StringSetAsync(GetSetKey, Payload).AsValueTask(); + + private ValueTask GetInit(IDatabaseAsync client) => + client.StringSetAsync(GetSetKey, Payload).AsUntypedValueTask(); + + private ValueTask PingInline(IDatabaseAsync client) => client.PingAsync().AsValueTask(); + + [DisplayName("PING_BULK")] + private ValueTask PingBulk(IDatabaseAsync client) => client.PingAsync().AsValueTask(); + + [DisplayName("INCR")] + private ValueTask Incr(IDatabaseAsync client) => client.StringIncrementAsync(CounterKey).AsValueTask(); + + [DisplayName("HSET")] + private ValueTask HSet(IDatabaseAsync client) => + client.HashSetAsync(HashKey, "element:__rand_int__", Payload).AsValueTask(); + + [DisplayName("SADD")] + private ValueTask SAdd(IDatabaseAsync client) => + client.SetAddAsync(SetKey, "element:__rand_int__").AsValueTask(); + + [DisplayName("LPUSH")] + private ValueTask LPush(IDatabaseAsync client) => client.ListLeftPushAsync(ListKey, Payload).AsValueTask(); + + [DisplayName("RPUSH")] + private ValueTask RPush(IDatabaseAsync client) => client.ListRightPushAsync(ListKey, Payload).AsValueTask(); + + [DisplayName("LPOP")] + private ValueTask LPop(IDatabaseAsync client) => client.ListLeftPopAsync(ListKey).AsValueTask(); + + [DisplayName("RPOP")] + private ValueTask RPop(IDatabaseAsync client) => client.ListRightPopAsync(ListKey).AsValueTask(); + + private ValueTask LPopInit(IDatabaseAsync client) => + client.ListLeftPushAsync(ListKey, Payload).AsUntypedValueTask(); + + [DisplayName("SPOP")] + private ValueTask SPop(IDatabaseAsync client) => client.SetPopAsync(SetKey).AsValueTask(); + + private ValueTask SPopInit(IDatabaseAsync client) => + client.SetAddAsync(SetKey, "element:__rand_int__").AsUntypedValueTask(); + + [DisplayName("ZADD")] + private ValueTask ZAdd(IDatabaseAsync client) => + client.SortedSetAddAsync(SortedSetKey, "element:__rand_int__", 0).AsValueTask(); + + [DisplayName("ZPOPMIN")] + private ValueTask ZPopMin(IDatabaseAsync client) => CountAsync(client.SortedSetPopAsync(SortedSetKey, 1)); + + private async ValueTask ZPopMinInit(IDatabaseAsync client) + { + int ops = TotalOperations; + var rand = new Random(); + for (int i = 0; i < ops; i++) + { + await client.SortedSetAddAsync(SortedSetKey, "element:__rand_int__", (rand.NextDouble() * 2000) - 1000) + .ConfigureAwait(false); + } + } + + [DisplayName("MSET")] + private ValueTask MSet(IDatabaseAsync client) => client.StringSetAsync(_pairs).AsValueTask(); + + [DisplayName("XADD")] + private ValueTask XAdd(IDatabaseAsync client) => + client.StreamAddAsync(StreamKey, "myfield", Payload).AsValueTask(); + + [DisplayName("LRANGE_100")] + private ValueTask LRange100(IDatabaseAsync client) => CountAsync(client.ListRangeAsync(ListKey, 0, 99)); + + [DisplayName("LRANGE_300")] + private ValueTask LRange300(IDatabaseAsync client) => CountAsync(client.ListRangeAsync(ListKey, 0, 299)); + + [DisplayName("LRANGE_500")] + private ValueTask LRange500(IDatabaseAsync client) => CountAsync(client.ListRangeAsync(ListKey, 0, 499)); + + [DisplayName("LRANGE_600")] + private ValueTask LRange600(IDatabaseAsync client) => + CountAsync(client.ListRangeAsync(ListKey, 0, 599)); + + private static ValueTask CountAsync(Task task) => task.ContinueWith( + t => t.Result.Length, TaskContinuationOptions.ExecuteSynchronously).AsValueTask(); + + private async ValueTask LRangeInit(IDatabaseAsync client) + { + var ops = TotalOperations; + for (int i = 0; i < ops; i++) + { + await client.ListLeftPushAsync(ListKey, Payload); + } + } +} + +internal static class TaskExtensions +{ + public static ValueTask AsValueTask(this Task task) => new(task); + public static ValueTask AsUntypedValueTask(this Task task) => new(task); + public static ValueTask AsValueTask(this Task task) => new(task); + + public static ValueTask AsUntypedValueTask(this ValueTask task) + { + if (!task.IsCompleted) return Awaited(task); + task.GetAwaiter().GetResult(); + return default; + + static async ValueTask Awaited(ValueTask task) + { + await task.ConfigureAwait(false); + } + } +} diff --git a/src/RESPite.Benchmark/Program.cs b/src/RESPite.Benchmark/Program.cs index 34f3c790b..3d421d173 100644 --- a/src/RESPite.Benchmark/Program.cs +++ b/src/RESPite.Benchmark/Program.cs @@ -20,6 +20,9 @@ private static async Task Main(string[] args) case "--old": benchmarks.Add(new OldCoreBenchmark(args)); break; + case "--bridge": + benchmarks.Add(new BridgeBenchmark(args)); + break; case "--new": benchmarks.Add(new NewCoreBenchmark(args)); break; diff --git a/src/RESPite.Benchmark/RESPite.Benchmark.csproj b/src/RESPite.Benchmark/RESPite.Benchmark.csproj index f3066e102..42ba2c533 100644 --- a/src/RESPite.Benchmark/RESPite.Benchmark.csproj +++ b/src/RESPite.Benchmark/RESPite.Benchmark.csproj @@ -17,9 +17,10 @@ + - - + + diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs index 8726e0501..a7c22ca0c 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs @@ -13,9 +13,6 @@ public Task KeyCopyAsync( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task KeyDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - public Task KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -107,8 +104,8 @@ public bool KeyCopy( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("del")] + public partial bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None); public long KeyDelete(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.List.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.List.cs index b65fb94b6..68f87ea44 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.List.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.List.cs @@ -22,9 +22,6 @@ public Task ListInsertBeforeAsync( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task ListLeftPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - public Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -52,8 +49,12 @@ public Task ListLeftPushAsync( RedisKey key, RedisValue value, When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) => when switch + { + When.Always => LPushAsync(key, value, flags), + When.Exists => LPushXAsync(key, value, flags), + _ => Task.FromResult(NotSupportedInt64(when)), + }; public Task ListLeftPushAsync( RedisKey key, @@ -90,9 +91,6 @@ public Task ListRemoveAsync( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task ListRightPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - public Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -109,8 +107,12 @@ public Task ListRightPushAsync( RedisKey key, RedisValue value, When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) => when switch + { + When.Always => RPushAsync(key, value, flags), + When.Exists => RPushXAsync(key, value, flags), + _ => Task.FromResult(NotSupportedInt64(when)), + }; public Task ListRightPushAsync( RedisKey key, @@ -154,8 +156,8 @@ public long ListInsertBefore( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("lpop")] + public partial RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.None); public RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -184,8 +186,20 @@ public long ListLeftPush( RedisKey key, RedisValue value, When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) => when switch + { + When.Always => LPush(key, value, flags), + When.Exists => LPushX(key, value, flags), + _ => NotSupportedInt64(when), + }; + + private static long NotSupportedInt64(When when) => throw new NotSupportedException( + $"The condition '{when}' is not supported for this command"); + + [RespCommand] + private partial long LPush(RedisKey key, RedisValue value, CommandFlags flags); + [RespCommand] + private partial long LPushX(RedisKey key, RedisValue value, CommandFlags flags); public long ListLeftPush( RedisKey key, @@ -222,8 +236,8 @@ public long ListRemove( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("rpop")] + public partial RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.None); public RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -241,8 +255,17 @@ public long ListRightPush( RedisKey key, RedisValue value, When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) => when switch + { + When.Always => RPush(key, value, flags), + When.Exists => RPushX(key, value, flags), + _ => NotSupportedInt64(when), + }; + + [RespCommand] + private partial long RPush(RedisKey key, RedisValue value, CommandFlags flags); + [RespCommand] + private partial long RPushX(RedisKey key, RedisValue value, CommandFlags flags); public long ListRightPush( RedisKey key, diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Set.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Set.cs index 3fb5423f2..2f8d20fd6 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Set.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Set.cs @@ -5,9 +5,6 @@ namespace RESPite.StackExchange.Redis; internal sealed partial class ProxiedDatabase { // Async Set methods - public Task SetAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - public Task SetAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -64,9 +61,6 @@ public Task SetMoveAsync( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task SetPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - public Task SetPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -92,8 +86,8 @@ public IAsyncEnumerable SetScanAsync( throw new NotImplementedException(); // Synchronous Set methods - public bool SetAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("sadd")] + public partial bool SetAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); public long SetAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -145,8 +139,8 @@ public bool SetMove( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue SetPop(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("spop")] + public partial RedisValue SetPop(RedisKey key, CommandFlags flags = CommandFlags.None); public RedisValue[] SetPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs index b662a7a27..f7d6f8e5a 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs @@ -1,4 +1,5 @@ -using StackExchange.Redis; +using RESPite.Messages; +using StackExchange.Redis; namespace RESPite.StackExchange.Redis; @@ -52,15 +53,9 @@ public Task StringDecrementAsync(RedisKey key, long value = 1, CommandFlag public Task StringDecrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StringGetAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - public Task StringGetAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task?> StringGetLeaseAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - public Task StringGetBitAsync(RedisKey key, long offset, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -92,11 +87,8 @@ public Task StringGetDeleteAsync(RedisKey key, CommandFlags flags = public Task StringGetWithExpiryAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task StringIncrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task StringIncrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task StringIncrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) + => value == 1 ? StringIncrementUnitAsync(key, flags) : StringIncrementNonUnitAsync(key, value, flags); public Task StringLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -121,19 +113,10 @@ public Task StringLongestCommonSubsequenceWithMatchesAsync( throw new NotImplementedException(); public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when) => - throw new NotImplementedException(); + StringSetAsync(key, value, expiry, false, when, CommandFlags.None); public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => - throw new NotImplementedException(); - - public Task StringSetAsync( - RedisKey key, - RedisValue value, - TimeSpan? expiry = null, - bool keepTtl = false, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + StringSetAsync(key, value, expiry, false, when, flags); public Task StringSetAsync( KeyValuePair[] values, @@ -216,14 +199,14 @@ public long StringDecrement(RedisKey key, long value = 1, CommandFlags flags = C public double StringDecrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue StringGet(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("get")] + public partial RedisValue StringGet(RedisKey key, CommandFlags flags = CommandFlags.None); public RedisValue[] StringGet(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Lease? StringGetLease(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("get")] + public partial Lease? StringGetLease(RedisKey key, CommandFlags flags = CommandFlags.None); public bool StringGetBit(RedisKey key, long offset, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -246,11 +229,17 @@ public RedisValue StringGetDelete(RedisKey key, CommandFlags flags = CommandFlag public RedisValueWithExpiry StringGetWithExpiry(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long StringIncrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public long StringIncrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) + => value == 1 ? StringIncrementUnit(key, flags) : StringIncrementNonUnit(key, value, flags); - public double StringIncrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("incr")] + private partial long StringIncrementUnit(RedisKey key, CommandFlags flags); + + [RespCommand("incrby")] + private partial long StringIncrementNonUnit(RedisKey key, long value, CommandFlags flags); + + [RespCommand("incrbyfloat")] + public partial double StringIncrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None); public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -275,19 +264,75 @@ public LCSMatchResult StringLongestCommonSubsequenceWithMatches( throw new NotImplementedException(); public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when) => - throw new NotImplementedException(); + StringSet(key, value, expiry, false, when, CommandFlags.None); public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => - throw new NotImplementedException(); + StringSet(key, value, expiry, false, when, flags); - public bool StringSet( + [RespCommand("set", Formatter = StringSetFormatter.Formatter)] + public partial bool StringSet( RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None); + + private sealed class StringSetFormatter : IRespFormatter<(RedisKey Key, RedisValue Value, TimeSpan? Expiry, bool + KeepTtl, + When When)> + { + public const string Formatter = $"{nameof(StringSetFormatter)}.{nameof(Instance)}"; + public static readonly StringSetFormatter Instance = new StringSetFormatter(); + private StringSetFormatter() { } + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, RedisValue Value, TimeSpan? Expiry, bool KeepTtl, When When) request) + { + // SET key value [NX | XX] [GET] [EX seconds | PX milliseconds | + // EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL] + var argCount = 2 + request.When switch + { + When.Always => 0, + When.Exists or When.NotExists => 1, + _ => throw new ArgumentOutOfRangeException(nameof(request.When)), + } + (request.Expiry.HasValue ? 2 : 0) + (request.KeepTtl ? 1 : 0); + writer.WriteCommand(command, argCount); + writer.Write(request.Key); + writer.Write(request.Value); + switch (request.When) + { + case When.Exists: + writer.WriteBulkString("EX"u8); + break; + case When.NotExists: + writer.WriteBulkString("NX"u8); + break; + } + + if (request.Expiry.HasValue) + { + var millis = (long)request.Expiry.Value.TotalMilliseconds; + if ((millis % 1000) == 0) + { + writer.WriteBulkString("EX"u8); + writer.WriteBulkString(millis / 1000); + } + else + { + writer.WriteBulkString("PX"u8); + writer.WriteBulkString(millis); + } + } + + if (request.KeepTtl) + { + writer.WriteBulkString("KEEPTTL"u8); + } + } + } public bool StringSet( KeyValuePair[] values, diff --git a/src/RESPite.StackExchange.Redis/RespParsers.cs b/src/RESPite.StackExchange.Redis/RespParsers.cs index 04ccf2be2..1ffed4dec 100644 --- a/src/RESPite.StackExchange.Redis/RespParsers.cs +++ b/src/RESPite.StackExchange.Redis/RespParsers.cs @@ -7,8 +7,10 @@ public static class RespParsers { public static IRespParser RedisValue => DefaultParser.Instance; public static IRespParser RedisKey => DefaultParser.Instance; + public static IRespParser> BytesLease => DefaultParser.Instance; - private sealed class DefaultParser : IRespParser, IRespParser + private sealed class DefaultParser : IRespParser, IRespParser, + IRespParser> { private DefaultParser() { } public static readonly DefaultParser Instance = new(); @@ -31,5 +33,15 @@ RedisKey IRespParser.Parse(ref RespReader reader) if (reader.UnsafeTryReadShortAscii(out var s)) return s; return reader.ReadByteArray(); } + + Lease IRespParser>.Parse(ref RespReader reader) + { + reader.DemandScalar(); + if (reader.IsNull) return null!; + var len = reader.ScalarLength(); + var lease = Lease.Create(len); + reader.CopyTo(lease.Span); + return lease; + } } } From 19a62e5afc276f0ae9d8b8ad4e383b5ce03aee9d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 6 Sep 2025 12:30:43 +0100 Subject: [PATCH 062/108] proxied batches --- src/RESPite.StackExchange.Redis/Node.cs | 3 +- .../ProxiedBatch.cs | 22 +++++++++++ .../ProxiedDatabase.Connection.cs | 4 +- .../ProxiedDatabase.Geo.cs | 2 +- .../ProxiedDatabase.Hash.cs | 2 +- .../ProxiedDatabase.HyperLogLog.cs | 2 +- .../ProxiedDatabase.Key.cs | 2 +- .../ProxiedDatabase.List.cs | 2 +- .../ProxiedDatabase.Lock.cs | 2 +- .../ProxiedDatabase.Script.cs | 2 +- .../ProxiedDatabase.Set.cs | 2 +- .../ProxiedDatabase.Sort.cs | 2 +- .../ProxiedDatabase.SortedSet.cs | 2 +- .../ProxiedDatabase.Stream.cs | 2 +- .../ProxiedDatabase.String.cs | 2 +- .../ProxiedDatabase.cs | 38 ++++++++++++++----- 16 files changed, 66 insertions(+), 25 deletions(-) create mode 100644 src/RESPite.StackExchange.Redis/ProxiedBatch.cs diff --git a/src/RESPite.StackExchange.Redis/Node.cs b/src/RESPite.StackExchange.Redis/Node.cs index fade24621..240c009d7 100644 --- a/src/RESPite.StackExchange.Redis/Node.cs +++ b/src/RESPite.StackExchange.Redis/Node.cs @@ -1,5 +1,6 @@ using System.Net; using System.Net.Sockets; +using RESPite.Connections; using StackExchange.Redis; namespace RESPite.StackExchange.Redis; @@ -163,7 +164,7 @@ public async Task ConnectAsync(TextWriter? log = null, bool force = false) // finalize the connections log.LogLocked($"[{_label}] Finalizing..."); var oldConnection = _connection; - _connection = connection; + _connection = connection.Synchronized(); await oldConnection.DisposeAsync().ConfigureAwait(false); // check nothing changed while we weren't looking diff --git a/src/RESPite.StackExchange.Redis/ProxiedBatch.cs b/src/RESPite.StackExchange.Redis/ProxiedBatch.cs new file mode 100644 index 000000000..eea330b40 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/ProxiedBatch.cs @@ -0,0 +1,22 @@ +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal sealed class ProxiedBatch : ProxiedDatabase, IBatch, IRespContextProxy +{ + private readonly IRespContextProxy _originalProxy; + private RespBatch _batch; + + public ProxiedBatch(IRespContextProxy proxy, int db) : base(proxy, db) + { + _originalProxy = proxy; + _batch = proxy.Context.CreateBatch(); + SetProxy(this); + } + + void IBatch.Execute() => _batch.Flush(); + + public RespMultiplexer Multiplexer => _originalProxy.Multiplexer; + public ref readonly RespContext Context => ref _batch.Context; + RespContextProxyKind IRespContextProxy.RespContextProxyKind => RespContextProxyKind; +} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs index 2bcdc74a7..c959cc21e 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs @@ -4,7 +4,7 @@ namespace RESPite.StackExchange.Redis; -internal sealed partial class ProxiedDatabase +internal partial class ProxiedDatabase { // Connection and core methods public bool IsConnected(RedisKey key, CommandFlags flags = CommandFlags.None) => @@ -29,7 +29,7 @@ private PingParser() { } throw new NotImplementedException(); public IBatch CreateBatch(object? asyncState = null) => - throw new NotImplementedException(); + new ProxiedBatch(_proxy, _db); public ITransaction CreateTransaction(object? asyncState = null) => throw new NotImplementedException(); diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Geo.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Geo.cs index 7bba05b00..8d7949b9a 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Geo.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Geo.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal sealed partial class ProxiedDatabase +internal partial class ProxiedDatabase { // Async Geo methods public Task GeoAddAsync( diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Hash.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Hash.cs index 543173456..863653b40 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Hash.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Hash.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal sealed partial class ProxiedDatabase +internal partial class ProxiedDatabase { // Async Hash methods public Task HashDecrementAsync( diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.HyperLogLog.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.HyperLogLog.cs index 2fbb8746d..ab69ce21e 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.HyperLogLog.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.HyperLogLog.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal sealed partial class ProxiedDatabase +internal partial class ProxiedDatabase { // Async HyperLogLog methods public Task HyperLogLogAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs index a7c22ca0c..7ac906e29 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal sealed partial class ProxiedDatabase +internal partial class ProxiedDatabase { // Async Key methods public Task KeyCopyAsync( diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.List.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.List.cs index 68f87ea44..e8720c60d 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.List.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.List.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal sealed partial class ProxiedDatabase +internal partial class ProxiedDatabase { // Async List methods public Task ListGetByIndexAsync(RedisKey key, long index, CommandFlags flags = CommandFlags.None) => diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Lock.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Lock.cs index 63d51ab1f..244b64fd7 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Lock.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Lock.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal sealed partial class ProxiedDatabase +internal partial class ProxiedDatabase { // Async Lock methods public Task LockExtendAsync( diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Script.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Script.cs index 208105405..021a69163 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Script.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Script.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal sealed partial class ProxiedDatabase +internal partial class ProxiedDatabase { // Async Script/Execute/Publish methods public Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) => diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Set.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Set.cs index 2f8d20fd6..d4401b8e4 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Set.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Set.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal sealed partial class ProxiedDatabase +internal partial class ProxiedDatabase { // Async Set methods public Task SetAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Sort.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Sort.cs index 54497ca21..3b4bdd973 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Sort.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Sort.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal sealed partial class ProxiedDatabase +internal partial class ProxiedDatabase { // Async Sort methods public Task SortAsync( diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.SortedSet.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.SortedSet.cs index 6f4652aa7..53a8947b8 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.SortedSet.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.SortedSet.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal sealed partial class ProxiedDatabase +internal partial class ProxiedDatabase { // Async SortedSet methods public Task SortedSetAddAsync( diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Stream.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Stream.cs index a5673ed9b..45987e1aa 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Stream.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.Stream.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal sealed partial class ProxiedDatabase +internal partial class ProxiedDatabase { // Async Stream methods public Task StreamAcknowledgeAsync( diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs index f7d6f8e5a..775b2f44a 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs @@ -3,7 +3,7 @@ namespace RESPite.StackExchange.Redis; -internal sealed partial class ProxiedDatabase +internal partial class ProxiedDatabase { // Async String methods public Task StringAppendAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs b/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs index 397f467c8..a62fec882 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs +++ b/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs @@ -7,8 +7,26 @@ namespace RESPite.StackExchange.Redis; /// could be direct to a known server or routed - the is responsible for /// that determination. /// -internal sealed partial class ProxiedDatabase(IRespContextProxy proxy, int db) : IDatabase +internal partial class ProxiedDatabase : IDatabase { + private IRespContextProxy _proxy; + private readonly int _db; + + /// + /// Implements IDatabase on top of a , which provides access to a RESP context; this + /// could be direct to a known server or routed - the is responsible for + /// that determination. + /// + public ProxiedDatabase(IRespContextProxy proxy, int db) + { + _proxy = proxy; + _db = db; + } + + // change the proxy being used + protected void SetProxy(IRespContextProxy proxy) + => _proxy = proxy; + // Question: cache this, or rebuild each time? the latter handles shutdown better. // internal readonly RespContext Context = proxy.Context.WithDatabase(db); private RespContext Context(CommandFlags flags) @@ -21,20 +39,20 @@ private RespContext Context(CommandFlags flags) | RespContext.RespContextFlags.FireAndForget | RespContext.RespContextFlags.NoScriptCache; - return proxy.Context.With(db, (RespContext.RespContextFlags)flags, flagMask); + return _proxy.Context.With(_db, (RespContext.RespContextFlags)flags, flagMask); } - private TimeSpan SyncTimeout => proxy.Context.SyncTimeout; - public int Database => db; + private TimeSpan SyncTimeout => _proxy.Context.SyncTimeout; + public int Database => _db; - public IConnectionMultiplexer Multiplexer => proxy.Multiplexer; - public RespContextProxyKind RespContextProxyKind => proxy.RespContextProxyKind; + IConnectionMultiplexer IRedisAsync.Multiplexer => _proxy.Multiplexer; + public RespContextProxyKind RespContextProxyKind => _proxy.RespContextProxyKind; - public bool TryWait(Task task) => proxy.Multiplexer.TryWait(task); + public bool TryWait(Task task) => _proxy.Multiplexer.TryWait(task); - public void Wait(Task task) => proxy.Multiplexer.Wait(task); + public void Wait(Task task) => _proxy.Multiplexer.Wait(task); - public T Wait(Task task) => proxy.Multiplexer.Wait(task); + public T Wait(Task task) => _proxy.Multiplexer.Wait(task); - public void WaitAll(params Task[] tasks) => proxy.Multiplexer.WaitAll(tasks); + public void WaitAll(params Task[] tasks) => _proxy.Multiplexer.WaitAll(tasks); } From d0656a05f4300510fc231db3934497ee867556db Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 6 Sep 2025 18:36:55 +0100 Subject: [PATCH 063/108] naming is hard --- ...pContextProxy.cs => IRespContextSource.cs} | 2 +- src/RESPite.StackExchange.Redis/Node.cs | 8 ++--- .../ProxiedBatch.cs | 22 ------------ .../RESPite.StackExchange.Redis.csproj | 4 +-- .../RespContextBatch.cs | 26 ++++++++++++++ ...n.cs => RespContextDatabase.Connection.cs} | 4 +-- ...xiedDatabase.cs => RespContextDatabase.Cs} | 36 +++++++++---------- ...base.Geo.cs => RespContextDatabase.Geo.cs} | 2 +- ...se.Hash.cs => RespContextDatabase.Hash.cs} | 2 +- ....cs => RespContextDatabase.HyperLogLog.cs} | 2 +- ...base.Key.cs => RespContextDatabase.Key.cs} | 2 +- ...se.List.cs => RespContextDatabase.List.cs} | 2 +- ...se.Lock.cs => RespContextDatabase.Lock.cs} | 2 +- ...cript.cs => RespContextDatabase.Script.cs} | 2 +- ...base.Set.cs => RespContextDatabase.Set.cs} | 2 +- ...se.Sort.cs => RespContextDatabase.Sort.cs} | 2 +- ...et.cs => RespContextDatabase.SortedSet.cs} | 2 +- ...tream.cs => RespContextDatabase.Stream.cs} | 2 +- ...tring.cs => RespContextDatabase.String.cs} | 2 +- .../RespMultiplexer.cs | 12 +++---- tests/RESPite.Tests/RespMultiplexerTests.cs | 2 +- 21 files changed, 72 insertions(+), 68 deletions(-) rename src/RESPite.StackExchange.Redis/{IRespContextProxy.cs => IRespContextSource.cs} (92%) delete mode 100644 src/RESPite.StackExchange.Redis/ProxiedBatch.cs create mode 100644 src/RESPite.StackExchange.Redis/RespContextBatch.cs rename src/RESPite.StackExchange.Redis/{ProxiedDatabase.Connection.cs => RespContextDatabase.Connection.cs} (96%) rename src/RESPite.StackExchange.Redis/{ProxiedDatabase.cs => RespContextDatabase.Cs} (58%) rename src/RESPite.StackExchange.Redis/{ProxiedDatabase.Geo.cs => RespContextDatabase.Geo.cs} (99%) rename src/RESPite.StackExchange.Redis/{ProxiedDatabase.Hash.cs => RespContextDatabase.Hash.cs} (99%) rename src/RESPite.StackExchange.Redis/{ProxiedDatabase.HyperLogLog.cs => RespContextDatabase.HyperLogLog.cs} (98%) rename src/RESPite.StackExchange.Redis/{ProxiedDatabase.Key.cs => RespContextDatabase.Key.cs} (99%) rename src/RESPite.StackExchange.Redis/{ProxiedDatabase.List.cs => RespContextDatabase.List.cs} (99%) rename src/RESPite.StackExchange.Redis/{ProxiedDatabase.Lock.cs => RespContextDatabase.Lock.cs} (97%) rename src/RESPite.StackExchange.Redis/{ProxiedDatabase.Script.cs => RespContextDatabase.Script.cs} (98%) rename src/RESPite.StackExchange.Redis/{ProxiedDatabase.Set.cs => RespContextDatabase.Set.cs} (99%) rename src/RESPite.StackExchange.Redis/{ProxiedDatabase.Sort.cs => RespContextDatabase.Sort.cs} (97%) rename src/RESPite.StackExchange.Redis/{ProxiedDatabase.SortedSet.cs => RespContextDatabase.SortedSet.cs} (99%) rename src/RESPite.StackExchange.Redis/{ProxiedDatabase.Stream.cs => RespContextDatabase.Stream.cs} (99%) rename src/RESPite.StackExchange.Redis/{ProxiedDatabase.String.cs => RespContextDatabase.String.cs} (99%) diff --git a/src/RESPite.StackExchange.Redis/IRespContextProxy.cs b/src/RESPite.StackExchange.Redis/IRespContextSource.cs similarity index 92% rename from src/RESPite.StackExchange.Redis/IRespContextProxy.cs rename to src/RESPite.StackExchange.Redis/IRespContextSource.cs index bc67e0f93..f93a22494 100644 --- a/src/RESPite.StackExchange.Redis/IRespContextProxy.cs +++ b/src/RESPite.StackExchange.Redis/IRespContextSource.cs @@ -3,7 +3,7 @@ /// /// Provides access to a RESP context to use for operations; this context could be direct to a known server or routed. /// -internal interface IRespContextProxy +internal interface IRespContextSource { RespMultiplexer Multiplexer { get; } ref readonly RespContext Context { get; } diff --git a/src/RESPite.StackExchange.Redis/Node.cs b/src/RESPite.StackExchange.Redis/Node.cs index 240c009d7..760421834 100644 --- a/src/RESPite.StackExchange.Redis/Node.cs +++ b/src/RESPite.StackExchange.Redis/Node.cs @@ -5,7 +5,7 @@ namespace RESPite.StackExchange.Redis; -internal sealed class Node : IDisposable, IAsyncDisposable, IRespContextProxy +internal sealed class Node : IDisposable, IAsyncDisposable, IRespContextSource { private bool _isDisposed; @@ -44,7 +44,7 @@ public async ValueTask DisposeAsync() private NodeConnection? _subscription; public ref readonly RespContext Context => ref _interactive.Context; - RespContextProxyKind IRespContextProxy.RespContextProxyKind => RespContextProxyKind.ConnectionInteractive; + RespContextProxyKind IRespContextSource.RespContextProxyKind => RespContextProxyKind.ConnectionInteractive; public RespConnection InteractiveConnection => _interactive.Connection; @@ -73,7 +73,7 @@ public Task ConnectAsync( public IServer AsServer() => _server ??= new NodeServer(this); } -internal sealed class NodeConnection : IDisposable, IAsyncDisposable, IRespContextProxy +internal sealed class NodeConnection : IDisposable, IAsyncDisposable, IRespContextSource { private EventHandler? _onConnectionError; private readonly RespMultiplexer _multiplexer; @@ -90,7 +90,7 @@ public NodeConnection(RespMultiplexer multiplexer, EndPoint endPoint, Connection _label = Format.ToString(endPoint); } - RespContextProxyKind IRespContextProxy.RespContextProxyKind => _connectionType switch + RespContextProxyKind IRespContextSource.RespContextProxyKind => _connectionType switch { ConnectionType.Interactive => RespContextProxyKind.ConnectionInteractive, ConnectionType.Subscription => RespContextProxyKind.ConnectionSubscription, diff --git a/src/RESPite.StackExchange.Redis/ProxiedBatch.cs b/src/RESPite.StackExchange.Redis/ProxiedBatch.cs deleted file mode 100644 index eea330b40..000000000 --- a/src/RESPite.StackExchange.Redis/ProxiedBatch.cs +++ /dev/null @@ -1,22 +0,0 @@ -using StackExchange.Redis; - -namespace RESPite.StackExchange.Redis; - -internal sealed class ProxiedBatch : ProxiedDatabase, IBatch, IRespContextProxy -{ - private readonly IRespContextProxy _originalProxy; - private RespBatch _batch; - - public ProxiedBatch(IRespContextProxy proxy, int db) : base(proxy, db) - { - _originalProxy = proxy; - _batch = proxy.Context.CreateBatch(); - SetProxy(this); - } - - void IBatch.Execute() => _batch.Flush(); - - public RespMultiplexer Multiplexer => _originalProxy.Multiplexer; - public ref readonly RespContext Context => ref _batch.Context; - RespContextProxyKind IRespContextProxy.RespContextProxyKind => RespContextProxyKind; -} diff --git a/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj b/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj index 933f2390c..725f17405 100644 --- a/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj +++ b/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj @@ -23,8 +23,8 @@ - - ProxiedDatabase.cs + + RespContextDatabase.cs diff --git a/src/RESPite.StackExchange.Redis/RespContextBatch.cs b/src/RESPite.StackExchange.Redis/RespContextBatch.cs new file mode 100644 index 000000000..daa7060dc --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RespContextBatch.cs @@ -0,0 +1,26 @@ +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal sealed class RespContextBatch : RespContextDatabase, IBatch, IRespContextSource, IDisposable +{ + private readonly IRespContextSource _originalSource; + private readonly RespBatch _batch; + + public RespContextBatch(IRespContextSource source, int db) : base(source, db) + { + _originalSource = source; + _batch = source.Context.CreateBatch(); + SetSource(this); + } + + void IBatch.Execute() => _batch.Flush(); + + public void Dispose() => _batch.Dispose(); + + public RespMultiplexer Multiplexer => _originalSource.Multiplexer; + + public ref readonly RespContext Context => ref _batch.Context; + + RespContextProxyKind IRespContextSource.RespContextProxyKind => RespContextProxyKind; +} diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs similarity index 96% rename from src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs rename to src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs index c959cc21e..4064b715b 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Connection.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs @@ -4,7 +4,7 @@ namespace RESPite.StackExchange.Redis; -internal partial class ProxiedDatabase +internal partial class RespContextDatabase { // Connection and core methods public bool IsConnected(RedisKey key, CommandFlags flags = CommandFlags.None) => @@ -29,7 +29,7 @@ private PingParser() { } throw new NotImplementedException(); public IBatch CreateBatch(object? asyncState = null) => - new ProxiedBatch(_proxy, _db); + new RespContextBatch(source, _db); public ITransaction CreateTransaction(object? asyncState = null) => throw new NotImplementedException(); diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Cs similarity index 58% rename from src/RESPite.StackExchange.Redis/ProxiedDatabase.cs rename to src/RESPite.StackExchange.Redis/RespContextDatabase.Cs index a62fec882..699233775 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Cs @@ -3,29 +3,29 @@ namespace RESPite.StackExchange.Redis; /// -/// Implements IDatabase on top of a , which provides access to a RESP context; this -/// could be direct to a known server or routed - the is responsible for +/// Implements IDatabase on top of a , which provides access to a RESP context; this +/// could be direct to a known server or routed - the is responsible for /// that determination. /// -internal partial class ProxiedDatabase : IDatabase +internal partial class RespContextDatabase : IDatabase { - private IRespContextProxy _proxy; + private IRespContextSource source; private readonly int _db; /// - /// Implements IDatabase on top of a , which provides access to a RESP context; this - /// could be direct to a known server or routed - the is responsible for + /// Implements IDatabase on top of a , which provides access to a RESP context; this + /// could be direct to a known server or routed - the is responsible for /// that determination. /// - public ProxiedDatabase(IRespContextProxy proxy, int db) + public RespContextDatabase(IRespContextSource source, int db) { - _proxy = proxy; + this.source = source; _db = db; } // change the proxy being used - protected void SetProxy(IRespContextProxy proxy) - => _proxy = proxy; + protected void SetSource(IRespContextSource source) + => this.source = source; // Question: cache this, or rebuild each time? the latter handles shutdown better. // internal readonly RespContext Context = proxy.Context.WithDatabase(db); @@ -39,20 +39,20 @@ private RespContext Context(CommandFlags flags) | RespContext.RespContextFlags.FireAndForget | RespContext.RespContextFlags.NoScriptCache; - return _proxy.Context.With(_db, (RespContext.RespContextFlags)flags, flagMask); + return source.Context.With(_db, (RespContext.RespContextFlags)flags, flagMask); } - private TimeSpan SyncTimeout => _proxy.Context.SyncTimeout; + private TimeSpan SyncTimeout => source.Context.SyncTimeout; public int Database => _db; - IConnectionMultiplexer IRedisAsync.Multiplexer => _proxy.Multiplexer; - public RespContextProxyKind RespContextProxyKind => _proxy.RespContextProxyKind; + IConnectionMultiplexer IRedisAsync.Multiplexer => source.Multiplexer; + public RespContextProxyKind RespContextProxyKind => source.RespContextProxyKind; - public bool TryWait(Task task) => _proxy.Multiplexer.TryWait(task); + public bool TryWait(Task task) => source.Multiplexer.TryWait(task); - public void Wait(Task task) => _proxy.Multiplexer.Wait(task); + public void Wait(Task task) => source.Multiplexer.Wait(task); - public T Wait(Task task) => _proxy.Multiplexer.Wait(task); + public T Wait(Task task) => source.Multiplexer.Wait(task); - public void WaitAll(params Task[] tasks) => _proxy.Multiplexer.WaitAll(tasks); + public void WaitAll(params Task[] tasks) => source.Multiplexer.WaitAll(tasks); } diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Geo.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Geo.cs similarity index 99% rename from src/RESPite.StackExchange.Redis/ProxiedDatabase.Geo.cs rename to src/RESPite.StackExchange.Redis/RespContextDatabase.Geo.cs index 8d7949b9a..97296b0a5 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Geo.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Geo.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal partial class ProxiedDatabase +internal partial class RespContextDatabase { // Async Geo methods public Task GeoAddAsync( diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Hash.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs similarity index 99% rename from src/RESPite.StackExchange.Redis/ProxiedDatabase.Hash.cs rename to src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs index 863653b40..7c68f470a 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Hash.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal partial class ProxiedDatabase +internal partial class RespContextDatabase { // Async Hash methods public Task HashDecrementAsync( diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.HyperLogLog.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.HyperLogLog.cs similarity index 98% rename from src/RESPite.StackExchange.Redis/ProxiedDatabase.HyperLogLog.cs rename to src/RESPite.StackExchange.Redis/RespContextDatabase.HyperLogLog.cs index ab69ce21e..0ce47cb9d 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.HyperLogLog.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.HyperLogLog.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal partial class ProxiedDatabase +internal partial class RespContextDatabase { // Async HyperLogLog methods public Task HyperLogLogAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs similarity index 99% rename from src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs rename to src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs index 7ac906e29..c8288e9ea 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Key.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal partial class ProxiedDatabase +internal partial class RespContextDatabase { // Async Key methods public Task KeyCopyAsync( diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.List.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.List.cs similarity index 99% rename from src/RESPite.StackExchange.Redis/ProxiedDatabase.List.cs rename to src/RESPite.StackExchange.Redis/RespContextDatabase.List.cs index e8720c60d..0ef20774c 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.List.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.List.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal partial class ProxiedDatabase +internal partial class RespContextDatabase { // Async List methods public Task ListGetByIndexAsync(RedisKey key, long index, CommandFlags flags = CommandFlags.None) => diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Lock.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Lock.cs similarity index 97% rename from src/RESPite.StackExchange.Redis/ProxiedDatabase.Lock.cs rename to src/RESPite.StackExchange.Redis/RespContextDatabase.Lock.cs index 244b64fd7..cc9e4c919 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Lock.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Lock.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal partial class ProxiedDatabase +internal partial class RespContextDatabase { // Async Lock methods public Task LockExtendAsync( diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Script.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Script.cs similarity index 98% rename from src/RESPite.StackExchange.Redis/ProxiedDatabase.Script.cs rename to src/RESPite.StackExchange.Redis/RespContextDatabase.Script.cs index 021a69163..9930e6f9f 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Script.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Script.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal partial class ProxiedDatabase +internal partial class RespContextDatabase { // Async Script/Execute/Publish methods public Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) => diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Set.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Set.cs similarity index 99% rename from src/RESPite.StackExchange.Redis/ProxiedDatabase.Set.cs rename to src/RESPite.StackExchange.Redis/RespContextDatabase.Set.cs index d4401b8e4..6410ee4f4 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Set.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Set.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal partial class ProxiedDatabase +internal partial class RespContextDatabase { // Async Set methods public Task SetAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Sort.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Sort.cs similarity index 97% rename from src/RESPite.StackExchange.Redis/ProxiedDatabase.Sort.cs rename to src/RESPite.StackExchange.Redis/RespContextDatabase.Sort.cs index 3b4bdd973..dc1148194 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Sort.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Sort.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal partial class ProxiedDatabase +internal partial class RespContextDatabase { // Async Sort methods public Task SortAsync( diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.SortedSet.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs similarity index 99% rename from src/RESPite.StackExchange.Redis/ProxiedDatabase.SortedSet.cs rename to src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs index 53a8947b8..39eee1088 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.SortedSet.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal partial class ProxiedDatabase +internal partial class RespContextDatabase { // Async SortedSet methods public Task SortedSetAddAsync( diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Stream.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Stream.cs similarity index 99% rename from src/RESPite.StackExchange.Redis/ProxiedDatabase.Stream.cs rename to src/RESPite.StackExchange.Redis/RespContextDatabase.Stream.cs index 45987e1aa..f00d5a296 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.Stream.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Stream.cs @@ -2,7 +2,7 @@ namespace RESPite.StackExchange.Redis; -internal partial class ProxiedDatabase +internal partial class RespContextDatabase { // Async Stream methods public Task StreamAcknowledgeAsync( diff --git a/src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.String.cs similarity index 99% rename from src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs rename to src/RESPite.StackExchange.Redis/RespContextDatabase.String.cs index 775b2f44a..10d3aea9b 100644 --- a/src/RESPite.StackExchange.Redis/ProxiedDatabase.String.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.String.cs @@ -3,7 +3,7 @@ namespace RESPite.StackExchange.Redis; -internal partial class ProxiedDatabase +internal partial class RespContextDatabase { // Async String methods public Task StringAppendAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => diff --git a/src/RESPite.StackExchange.Redis/RespMultiplexer.cs b/src/RESPite.StackExchange.Redis/RespMultiplexer.cs index ab198cf7d..c9f0991f2 100644 --- a/src/RESPite.StackExchange.Redis/RespMultiplexer.cs +++ b/src/RESPite.StackExchange.Redis/RespMultiplexer.cs @@ -7,7 +7,7 @@ namespace RESPite.StackExchange.Redis; -public sealed class RespMultiplexer : IConnectionMultiplexer, IRespContextProxy +public sealed class RespMultiplexer : IConnectionMultiplexer, IRespContextSource { /// public override string ToString() => GetType().Name; @@ -27,9 +27,9 @@ public RespMultiplexer() private RespConnection _routedConnection; private RespContext _defaultContext; internal ref readonly RespContext Context => ref _defaultContext; - ref readonly RespContext IRespContextProxy.Context => ref _defaultContext; - RespContextProxyKind IRespContextProxy.RespContextProxyKind => RespContextProxyKind.Multiplexer; - RespMultiplexer IRespContextProxy.Multiplexer => this; + ref readonly RespContext IRespContextSource.Context => ref _defaultContext; + RespContextProxyKind IRespContextSource.RespContextProxyKind => RespContextProxyKind.Multiplexer; + RespMultiplexer IRespContextSource.Multiplexer => this; private readonly CancellationTokenSource _lifetime = new(); private ConfigurationOptions? _options; @@ -272,8 +272,8 @@ public T Wait(Task task) public IDatabase GetDatabase(int db = -1, object? asyncState = null) { if (db < 0) db = _defaultDatabase; - if (db < LowDatabaseCount) return _lowDatabases[db] ??= new ProxiedDatabase(this, db); - return new ProxiedDatabase(this, db); + if (db < LowDatabaseCount) return _lowDatabases[db] ??= new RespContextDatabase(this, db); + return new RespContextDatabase(this, db); } private const int LowDatabaseCount = 16; diff --git a/tests/RESPite.Tests/RespMultiplexerTests.cs b/tests/RESPite.Tests/RespMultiplexerTests.cs index d7c7e021f..2770073ad 100644 --- a/tests/RESPite.Tests/RespMultiplexerTests.cs +++ b/tests/RESPite.Tests/RespMultiplexerTests.cs @@ -22,7 +22,7 @@ public async Task CanConnect() await server.PingAsync(); var db = muxer.GetDatabase(); - var proxied = Assert.IsType(db); + var proxied = Assert.IsType(db); // since this is a single-node instance, we expect the proxied database to use the interactive connection Assert.Equal(RespContextProxyKind.ConnectionInteractive, proxied.RespContextProxyKind); db.Ping(); From 9dbce2ca165652b3bcbb55fa8f80f66842ea5666 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 9 Sep 2025 08:53:44 +0100 Subject: [PATCH 064/108] non-working overnight --- .../IRespContextSource.cs | 20 -- .../RespContextBatch.cs | 13 +- .../RespContextDatabase.Connection.cs | 2 +- .../RespContextDatabase.Cs | 28 ++- .../RespMultiplexer.cs | 78 +++--- .../RoutingRespConnection.cs | 15 -- src/RESPite/Connections/IRespContextSource.cs | 6 + .../Connections/Internal}/Node.cs | 139 +++++------ .../Connections/Internal/RoutedConnection.cs | 110 +++++++++ src/RESPite/Connections/Internal/Shard.cs | 84 +++++++ .../Connections/RespConnectionFactory.cs | 88 +++++++ .../Connections/RespConnectionManager.cs | 231 ++++++++++++++++++ src/RESPite/Internal/RespMessageBase.cs | 13 +- .../Internal}/Utils.cs | 2 +- src/RESPite/RespConfiguration.cs | 16 +- src/RESPite/RespConnection.cs | 45 +--- src/RESPite/RespOperation.cs | 14 +- 17 files changed, 690 insertions(+), 214 deletions(-) delete mode 100644 src/RESPite.StackExchange.Redis/IRespContextSource.cs delete mode 100644 src/RESPite.StackExchange.Redis/RoutingRespConnection.cs create mode 100644 src/RESPite/Connections/IRespContextSource.cs rename src/{RESPite.StackExchange.Redis => RESPite/Connections/Internal}/Node.cs (65%) create mode 100644 src/RESPite/Connections/Internal/RoutedConnection.cs create mode 100644 src/RESPite/Connections/Internal/Shard.cs create mode 100644 src/RESPite/Connections/RespConnectionFactory.cs create mode 100644 src/RESPite/Connections/RespConnectionManager.cs rename src/{RESPite.StackExchange.Redis => RESPite/Internal}/Utils.cs (94%) diff --git a/src/RESPite.StackExchange.Redis/IRespContextSource.cs b/src/RESPite.StackExchange.Redis/IRespContextSource.cs deleted file mode 100644 index f93a22494..000000000 --- a/src/RESPite.StackExchange.Redis/IRespContextSource.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace RESPite.StackExchange.Redis; - -/// -/// Provides access to a RESP context to use for operations; this context could be direct to a known server or routed. -/// -internal interface IRespContextSource -{ - RespMultiplexer Multiplexer { get; } - ref readonly RespContext Context { get; } - RespContextProxyKind RespContextProxyKind { get; } -} - -internal enum RespContextProxyKind -{ - Unknown, - Multiplexer, - ConnectionInteractive, - ConnectionSubscription, - Batch, -} diff --git a/src/RESPite.StackExchange.Redis/RespContextBatch.cs b/src/RESPite.StackExchange.Redis/RespContextBatch.cs index daa7060dc..f16353a2d 100644 --- a/src/RESPite.StackExchange.Redis/RespContextBatch.cs +++ b/src/RESPite.StackExchange.Redis/RespContextBatch.cs @@ -1,15 +1,14 @@ -using StackExchange.Redis; +using RESPite.Connections; +using StackExchange.Redis; namespace RESPite.StackExchange.Redis; -internal sealed class RespContextBatch : RespContextDatabase, IBatch, IRespContextSource, IDisposable +internal sealed class RespContextBatch : RespContextDatabase, IBatch, IDisposable, IRespContextSource { - private readonly IRespContextSource _originalSource; private readonly RespBatch _batch; - public RespContextBatch(IRespContextSource source, int db) : base(source, db) + public RespContextBatch(IRedisAsync parent, IRespContextSource source, int db) : base(parent, source, db) { - _originalSource = source; _batch = source.Context.CreateBatch(); SetSource(this); } @@ -18,9 +17,5 @@ public RespContextBatch(IRespContextSource source, int db) : base(source, db) public void Dispose() => _batch.Dispose(); - public RespMultiplexer Multiplexer => _originalSource.Multiplexer; - public ref readonly RespContext Context => ref _batch.Context; - - RespContextProxyKind IRespContextSource.RespContextProxyKind => RespContextProxyKind; } diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs index 4064b715b..24488ab4b 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs @@ -29,7 +29,7 @@ private PingParser() { } throw new NotImplementedException(); public IBatch CreateBatch(object? asyncState = null) => - new RespContextBatch(source, _db); + new RespContextBatch(_source, _db); public ITransaction CreateTransaction(object? asyncState = null) => throw new NotImplementedException(); diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Cs index 699233775..f27ea0dd3 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Cs @@ -1,4 +1,5 @@ -using StackExchange.Redis; +using RESPite.Connections; +using StackExchange.Redis; namespace RESPite.StackExchange.Redis; @@ -9,7 +10,8 @@ namespace RESPite.StackExchange.Redis; /// internal partial class RespContextDatabase : IDatabase { - private IRespContextSource source; + private readonly IRedisAsync _parent; + private IRespContextSource _source; private readonly int _db; /// @@ -17,15 +19,16 @@ internal partial class RespContextDatabase : IDatabase /// could be direct to a known server or routed - the is responsible for /// that determination. /// - public RespContextDatabase(IRespContextSource source, int db) + public RespContextDatabase(IRedisAsync parent, IRespContextSource source, int db) { - this.source = source; + _parent = parent; + _source = source; _db = db; } // change the proxy being used protected void SetSource(IRespContextSource source) - => this.source = source; + => this._source = source; // Question: cache this, or rebuild each time? the latter handles shutdown better. // internal readonly RespContext Context = proxy.Context.WithDatabase(db); @@ -39,20 +42,19 @@ internal partial class RespContextDatabase : IDatabase | RespContext.RespContextFlags.FireAndForget | RespContext.RespContextFlags.NoScriptCache; - return source.Context.With(_db, (RespContext.RespContextFlags)flags, flagMask); + return _source.Context.With(_db, (RespContext.RespContextFlags)flags, flagMask); } - private TimeSpan SyncTimeout => source.Context.SyncTimeout; + private TimeSpan SyncTimeout => _source.Context.SyncTimeout; public int Database => _db; - IConnectionMultiplexer IRedisAsync.Multiplexer => source.Multiplexer; - public RespContextProxyKind RespContextProxyKind => source.RespContextProxyKind; + IConnectionMultiplexer IRedisAsync.Multiplexer => _parent.Multiplexer; - public bool TryWait(Task task) => source.Multiplexer.TryWait(task); + public bool TryWait(Task task) => _parent.Multiplexer.TryWait(task); - public void Wait(Task task) => source.Multiplexer.Wait(task); + public void Wait(Task task) => _parent.Multiplexer.Wait(task); - public T Wait(Task task) => source.Multiplexer.Wait(task); + public T Wait(Task task) => _parent.Multiplexer.Wait(task); - public void WaitAll(params Task[] tasks) => source.Multiplexer.WaitAll(tasks); + public void WaitAll(params Task[] tasks) => _parent.Multiplexer.WaitAll(tasks); } diff --git a/src/RESPite.StackExchange.Redis/RespMultiplexer.cs b/src/RESPite.StackExchange.Redis/RespMultiplexer.cs index c9f0991f2..1987467ab 100644 --- a/src/RESPite.StackExchange.Redis/RespMultiplexer.cs +++ b/src/RESPite.StackExchange.Redis/RespMultiplexer.cs @@ -1,5 +1,7 @@ -using System.Diagnostics.CodeAnalysis; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; using System.Net; +using RESPite.Connections; using RESPite.Connections.Internal; using StackExchange.Redis; using StackExchange.Redis.Maintenance; @@ -7,29 +9,25 @@ namespace RESPite.StackExchange.Redis; -public sealed class RespMultiplexer : IConnectionMultiplexer, IRespContextSource +public sealed class RespMultiplexer : IConnectionMultiplexer { /// public override string ToString() => GetType().Name; - public RespMultiplexer() - { - _routedConnection = RespContext.Null.Connection; // until we've connected - _defaultContext = _routedConnection.Context; - } - + private readonly RespConnectionManager _connectionManager = new(); private int _defaultDatabase; + /* // the routed connection performs message-inspection based routing; on a single node // instance that isn't necessary, so the default-connection abstracts over that: // in a single-node instance, the default-connection will be the single interactive connection // otherwise, the default-connection will be the routed connection - private RespConnection _routedConnection; - private RespContext _defaultContext; + private RoutedConnection? _routedConnection; + private RespContext _defaultContext = RespContext.Null; internal ref readonly RespContext Context => ref _defaultContext; ref readonly RespContext IRespContextSource.Context => ref _defaultContext; - RespContextProxyKind IRespContextSource.RespContextProxyKind => RespContextProxyKind.Multiplexer; - RespMultiplexer IRespContextSource.Multiplexer => this; + RespContextProxyKind ITypedRespContextSource.RespContextProxyKind => RespContextProxyKind.Multiplexer; + RespMultiplexer ITypedRespContextSource.Multiplexer => this; private readonly CancellationTokenSource _lifetime = new(); private ConfigurationOptions? _options; @@ -68,6 +66,7 @@ private void OnConnect(ConfigurationOptions options) } } } + ep.SetDefaultPorts(ServerType.Standalone, ssl: options.Ssl); // add nodes from the endpoints @@ -76,15 +75,9 @@ private void OnConnect(ConfigurationOptions options) { nodes[i] = new Node(this, ep[i]); } - _nodes = nodes; + _nodes = nodes; _defaultDatabase = options.DefaultDatabase ?? 0; - - // setup a basic connection that comes via ourselves - var ctx = RespContext.Null; // this is just the template - _routedConnection = new RoutingRespConnection(this, ctx); - // set the default context (this might get simplified later, in OnNodesChanged) - _defaultContext = _routedConnection.Context; } public void Connect(string configuration = "", TextWriter? log = null) @@ -133,12 +126,11 @@ public async Task ConnectAsync(ConfigurationOptions options, TextWriter? log = n public void Dispose() { - RespConnection conn = _routedConnection; - _routedConnection = NullConnection.Disposed; - _defaultContext = _routedConnection.Context; + var routed = _routedConnection; + _routedConnection = null; + _defaultContext = NullConnection.Disposed.Context; _lifetime.Cancel(); - conn.Dispose(); - _routedConnection.Dispose(); + routed?.Dispose(); foreach (var node in _nodes) { node.Dispose(); @@ -147,16 +139,19 @@ public void Dispose() public async ValueTask DisposeAsync() { - RespConnection conn = _routedConnection; - _routedConnection = RespContext.Null.Connection; - _defaultContext = _routedConnection.Context; + var routed = _routedConnection; + _routedConnection = null; + _defaultContext = NullConnection.Disposed.Context; #if NET8_0_OR_GREATER await _lifetime.CancelAsync().ConfigureAwait(false); #else _lifetime.Cancel(); #endif - await conn.DisposeAsync().ConfigureAwait(false); - await _routedConnection.DisposeAsync().ConfigureAwait(false); + if (routed is not null) + { + await routed.DisposeAsync().ConfigureAwait(false); + } + foreach (var node in _nodes) { await node.DisposeAsync().ConfigureAwait(false); @@ -285,7 +280,8 @@ public IServer GetServer(string host, int port, object? asyncState = null) public IServer GetServer(string hostAndPort, object? asyncState = null) => Format.TryParseEndPoint(hostAndPort, out var ep) ? GetServer(ep, asyncState) - : throw new ArgumentException($"The specified host and port could not be parsed: {hostAndPort}", nameof(hostAndPort)); + : throw new ArgumentException($"The specified host and port could not be parsed: {hostAndPort}", + nameof(hostAndPort)); public IServer GetServer(IPAddress host, int port) { @@ -296,6 +292,7 @@ public IServer GetServer(IPAddress host, int port) return node.AsServer(); } } + throw new ArgumentException("The specified endpoint is not defined", nameof(host)); } @@ -318,11 +315,27 @@ private void OnNodesChanged() _defaultContext = nodes.Length switch { 0 => NullConnection.NonRoutable.Context, // nowhere to go - 1 when nodes[0] is { IsConnected: true } node => node.InteractiveConnection.Context, - _ => _routedConnection.Context, + 1 => nodes[0] is { IsConnected: true } conn + ? conn.Context + : NullConnection.NonRoutable.Context, // nowhere to go + _ => BuildRouted(nodes), }; } + private ref readonly RespContext BuildRouted(Node[] nodes) + { + Shard[] oversized = ArrayPool.Shared.Rent(nodes.Length); + for (int i = 0; i < nodes.Length; i++) + { + oversized[i] = nodes[i].AsShard(); + } + Array.Sort(oversized, 0, nodes.Length); + var conn = _routedConnection ??= new(); + conn.SetRoutingTable(new ReadOnlySpan(oversized, 0, nodes.Length)); + ArrayPool.Shared.Return(oversized); + return ref conn.Context; + } + public IServer[] GetServers() => Array.ConvertAll(_nodes, static x => x.AsServer()); public Task ConfigureAsync(TextWriter? log = null) => throw new NotImplementedException(); @@ -352,4 +365,5 @@ public void ExportConfiguration(Stream destination, ExportOptions options = Expo throw new NotImplementedException(); public void AddLibraryNameSuffix(string suffix) => throw new NotImplementedException(); + */ } diff --git a/src/RESPite.StackExchange.Redis/RoutingRespConnection.cs b/src/RESPite.StackExchange.Redis/RoutingRespConnection.cs deleted file mode 100644 index 00e4dcb10..000000000 --- a/src/RESPite.StackExchange.Redis/RoutingRespConnection.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace RESPite.StackExchange.Redis; - -internal sealed class RoutingRespConnection(RespMultiplexer multiplexer, in RespContext tail) - : RespConnection(in tail, multiplexer.Configuration) -{ - public override event EventHandler? ConnectionError; - internal override int OutstandingOperations => 0; - - internal void OnConnectionError(RespConnectionErrorEventArgs e) => ConnectionError?.Invoke(this, e); - - public override void Write(in RespOperation message) - { - throw new NotImplementedException(); - } -} diff --git a/src/RESPite/Connections/IRespContextSource.cs b/src/RESPite/Connections/IRespContextSource.cs new file mode 100644 index 000000000..2305e45c3 --- /dev/null +++ b/src/RESPite/Connections/IRespContextSource.cs @@ -0,0 +1,6 @@ +namespace RESPite.Connections; + +public interface IRespContextSource +{ + ref readonly RespContext Context { get; } +} diff --git a/src/RESPite.StackExchange.Redis/Node.cs b/src/RESPite/Connections/Internal/Node.cs similarity index 65% rename from src/RESPite.StackExchange.Redis/Node.cs rename to src/RESPite/Connections/Internal/Node.cs index 760421834..2ce12f253 100644 --- a/src/RESPite.StackExchange.Redis/Node.cs +++ b/src/RESPite/Connections/Internal/Node.cs @@ -1,27 +1,28 @@ -using System.Net; -using System.Net.Sockets; -using RESPite.Connections; -using StackExchange.Redis; +using RESPite.Internal; -namespace RESPite.StackExchange.Redis; +namespace RESPite.Connections.Internal; internal sealed class Node : IDisposable, IAsyncDisposable, IRespContextSource { private bool _isDisposed; - - public Version Version { get; } - public EndPoint EndPoint => _interactive.EndPoint; - public RespMultiplexer Multiplexer => _interactive.Multiplexer; - - public Node(RespMultiplexer multiplexer, EndPoint endPoint) + public override string ToString() => Label; + public string EndPoint { get; } + public int Port { get; } + private string? _label; + internal string Label => _label ??= $"{EndPoint}:{Port}"; + internal RespConnectionManager Manager { get; } + + public Node(RespConnectionManager manager, string endPoint, int port) { - _interactive = new(multiplexer, endPoint, ConnectionType.Interactive); - Version = multiplexer.Options.DefaultVersion; - // defer on pub/sub + Manager = manager; + EndPoint = endPoint; + Port = port; + _interactive = new(this, false); } public bool IsConnected => _interactive.IsConnected; public bool IsConnecting => _interactive.IsConnecting; + public bool IsReplica { get; private set; } public void Dispose() { @@ -44,64 +45,56 @@ public async ValueTask DisposeAsync() private NodeConnection? _subscription; public ref readonly RespContext Context => ref _interactive.Context; - RespContextProxyKind IRespContextSource.RespContextProxyKind => RespContextProxyKind.ConnectionInteractive; public RespConnection InteractiveConnection => _interactive.Connection; public Task ConnectAsync( TextWriter? log = null, bool force = false, - ConnectionType connectionType = ConnectionType.Interactive) + bool pubSub = false) { if (_isDisposed) return Task.FromResult(false); - if (connectionType == ConnectionType.Interactive) + if (!pubSub) { return _interactive.ConnectAsync(log, force); } - else if (connectionType == ConnectionType.Subscription) - { - _subscription ??= new(_interactive.Multiplexer, _interactive.EndPoint, ConnectionType.Subscription); - return _subscription.ConnectAsync(log, force); - } - else - { - throw new ArgumentOutOfRangeException(nameof(connectionType)); - } + + _subscription ??= new(this, pubSub); + return _subscription.ConnectAsync(log, force); } - private IServer? _server; - public IServer AsServer() => _server ??= new NodeServer(this); + public Shard AsShard() + { + return new( + 0, + int.MaxValue, + Port, + IsReplica ? ShardFlags.Replica : ShardFlags.None, + EndPoint, + "", + this); + } } internal sealed class NodeConnection : IDisposable, IAsyncDisposable, IRespContextSource { - private EventHandler? _onConnectionError; - private readonly RespMultiplexer _multiplexer; - private readonly EndPoint _endPoint; - private readonly ConnectionType _connectionType; + // private EventHandler? _onConnectionError; + private readonly Node _node; + private readonly bool _pubSub; - public RespMultiplexer Multiplexer => _multiplexer; + public override string ToString() => Label; - public NodeConnection(RespMultiplexer multiplexer, EndPoint endPoint, ConnectionType connectionType) + public NodeConnection(Node node, bool pubSub) { - _multiplexer = multiplexer; - _endPoint = endPoint; - _connectionType = connectionType; - _label = Format.ToString(endPoint); + _node = node; + _pubSub = pubSub; } - RespContextProxyKind IRespContextSource.RespContextProxyKind => _connectionType switch - { - ConnectionType.Interactive => RespContextProxyKind.ConnectionInteractive, - ConnectionType.Subscription => RespContextProxyKind.ConnectionSubscription, - _ => RespContextProxyKind.Unknown, - }; - - public EndPoint EndPoint => _endPoint; + private string? _label; + private string Label => _label ??= _pubSub ? $"{_node.Label}/s" : _node.Label; + public Node Node => _node; private int _state = (int)NodeState.Disconnected; - private readonly string _label; - public override string ToString() => $"{_label}: {State}"; private NodeState State => (NodeState)_state; private enum NodeState @@ -121,7 +114,10 @@ private enum NodeState private RespConnection _connection = RespContext.Null.Connection; public RespConnection Connection => _connection; - public async Task ConnectAsync(TextWriter? log = null, bool force = false) + public async Task ConnectAsync( + TextWriter? log = null, + bool force = false, + CancellationToken cancellationToken = default) { int state; bool connecting = false; @@ -132,14 +128,14 @@ public async Task ConnectAsync(TextWriter? log = null, bool force = false) { case NodeState.Connected when force: case NodeState.Connecting when force: - log.LogLocked($"[{_label}] (already {(NodeState)state}, but forcing reconnect...)"); + log.LogLocked($"[{Label}] (already {(NodeState)state}, but forcing reconnect...)"); break; // reconnect anyway! case NodeState.Connected: case NodeState.Connecting: - log.LogLocked($"[{_label}] (already {(NodeState)state})"); + log.LogLocked($"[{Label}] (already {(NodeState)state})"); return true; case NodeState.Disposed: - log.LogLocked($"[{_label}] (already {(NodeState)state})"); + log.LogLocked($"[{Label}] (already {(NodeState)state})"); return false; } } @@ -151,18 +147,21 @@ public async Task ConnectAsync(TextWriter? log = null, bool force = false) // observe outcome of CEX above (noting that if forcing, we don't do that CEX) if (State == NodeState.Connecting) state = (int)NodeState.Connecting; - log.LogLocked($"[{_label}] {_endPoint.GetType().Name} connecting..."); + log.LogLocked($"[{Label}] connecting..."); connecting = true; - var connection = await RespConnection.CreateAsync( - _endPoint, - cancellationToken: _multiplexer.Lifetime).ConfigureAwait(false); + var manager = _node.Manager; + var stream = await manager.ConnectionFactory.ConnectAsync( + _node.EndPoint, + _node.Port, + cancellationToken: cancellationToken).ConfigureAwait(false); connecting = false; - log.LogLocked($"[{_label}] Performing handshake..."); + var connection = RespConnection.Create(stream, manager.Configuration); + log.LogLocked($"[{Label}] Performing handshake..."); // TODO: handshake // finalize the connections - log.LogLocked($"[{_label}] Finalizing..."); + log.LogLocked($"[{Label}] Finalizing..."); var oldConnection = _connection; _connection = connection.Synchronized(); await oldConnection.DisposeAsync().ConfigureAwait(false); @@ -171,20 +170,22 @@ public async Task ConnectAsync(TextWriter? log = null, bool force = false) if (Interlocked.CompareExchange(ref _state, (int)NodeState.Connected, state) == state) { // success - log.LogLocked($"[{_label}] (success)"); + log.LogLocked($"[{Label}] (success)"); + /* connection.ConnectionError += _onConnectionError ??= OnConnectionError; if (state == (int)NodeState.Faulted) OnConnectionRestored(); + */ return true; } - log.LogLocked($"[{_label}] (unable to complete; became {State})"); + log.LogLocked($"[{Label}] (unable to complete; became {State})"); _connection = oldConnection; return false; } catch (Exception ex) { - log.LogLocked($"[{_label}] Faulted: {ex.Message}"); + log.LogLocked($"[{Label}] Faulted: {ex.Message}{(connecting ? " (while connecting)" : "")}"); // something failed; cleanup and move to faulted, unless disposed if (State != NodeState.Disposed) { @@ -195,6 +196,7 @@ public async Task ConnectAsync(TextWriter? log = null, bool force = false) _connection = RespContext.Null.Connection; await conn.DisposeAsync(); + /* var failureType = ConnectionFailureType.InternalFailure; if (connecting) { @@ -210,10 +212,11 @@ public async Task ConnectAsync(TextWriter? log = null, bool force = false) } OnConnectionError(failureType, ex); + */ return false; } } - +/* private void OnConnectionError(object? sender, RespConnection.RespConnectionErrorEventArgs e) { var handler = _multiplexer.DirectConnectionFailed; @@ -226,7 +229,7 @@ private void OnConnectionError(object? sender, RespConnection.RespConnectionErro _connectionType, ConnectionFailureType.InternalFailure, e.Exception, - _label)); + Label)); } } @@ -242,7 +245,7 @@ private void OnConnectionError(ConnectionFailureType failureType, Exception? exc _connectionType, failureType, exception, - _label)); + Label)); } } @@ -258,9 +261,9 @@ private void OnConnectionRestored() _connectionType, ConnectionFailureType.None, null, - _label)); + Label)); } - } + }*/ public void Dispose() { @@ -268,7 +271,7 @@ public void Dispose() var conn = _connection; _connection = RespContext.Null.Connection; conn.Dispose(); - OnConnectionError(ConnectionFailureType.ConnectionDisposed); + // OnConnectionError(ConnectionFailureType.ConnectionDisposed); } public async ValueTask DisposeAsync() @@ -277,6 +280,6 @@ public async ValueTask DisposeAsync() var conn = _connection; _connection = RespContext.Null.Connection; await conn.DisposeAsync().ConfigureAwait(false); - OnConnectionError(ConnectionFailureType.ConnectionDisposed); + // OnConnectionError(ConnectionFailureType.ConnectionDisposed); } } diff --git a/src/RESPite/Connections/Internal/RoutedConnection.cs b/src/RESPite/Connections/Internal/RoutedConnection.cs new file mode 100644 index 000000000..de9ea6310 --- /dev/null +++ b/src/RESPite/Connections/Internal/RoutedConnection.cs @@ -0,0 +1,110 @@ +using System.Runtime.CompilerServices; +using RESPite.Internal; + +namespace RESPite.Connections.Internal; + +internal sealed class RoutedConnection : RespConnection +{ + private Shard[] _shards = []; + + private Shard[] _primaries = [], _replicas = []; + + public void SetRoutingTable(ReadOnlySpan shards) + { + if (shards.Length == _shards.Length) + { + bool match = true; + int index = 0; + Shard previous = default; + foreach (ref readonly Shard shard in shards) + { + if (index != 0 && previous.CompareTo(shard) > 0) ThrowNotSorted(); + if (!shard.Equals(_shards[index++])) + { + match = false; + break; + } + + previous = shard; + } + + if (match) return; // nothing has changed + } + + _shards = shards.ToArray(); + + static void ThrowNotSorted() => + throw new InvalidOperationException($"The input to {nameof(SetRoutingTable)} must be pre-sorted."); + } + + public override event EventHandler? ConnectionError + { + add => throw new NotSupportedException(); + remove => throw new NotSupportedException(); + } + + internal override int OutstandingOperations + { + get + { + int count = 0; + foreach (var shard in _shards) + { + if (shard.GetConnection() is { } conn) count += conn.OutstandingOperations; + } + + return count; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(in RespOperation message) + { + // simplest thing possible for now; long term, we could do bunching. + var conn = Select( + replicas: (message.Flags & RespMessageBase.StateFlags.Replica) != 0, + slot: message.Slot); + if (conn is null) + { + WriteNonPreferred(message); + } + else + { + // this is the happy path + conn.Write(in message); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void WriteNonPreferred(in RespOperation message) + { + var flags = message.Flags; + var conn = (flags & RespMessageBase.StateFlags.Demand) == 0 + ? Select((flags & RespMessageBase.StateFlags.Replica) == 0, message.Slot) + : null; + if (conn is null) + { + message.TrySetException( + new InvalidOperationException("No connection is available to handle this request.")); + } + else + { + conn.Write(in message); + } + } + + private RespConnection? Select(bool replicas, int slot) + { + var shards = replicas ? _replicas : _primaries; + foreach (var shard in shards) + { + if ((shard.From <= slot & shard.To >= slot) + && shard.GetConnection() is { IsHealthy: true } conn) + { + return conn; + } + } + + return null; + } +} diff --git a/src/RESPite/Connections/Internal/Shard.cs b/src/RESPite/Connections/Internal/Shard.cs new file mode 100644 index 000000000..d35a02b70 --- /dev/null +++ b/src/RESPite/Connections/Internal/Shard.cs @@ -0,0 +1,84 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace RESPite.Connections.Internal; + +[Flags] +internal enum ShardFlags +{ + None = 0, + Replica = 1, +} + +internal readonly struct Shard( + int from, + int to, + int port, + ShardFlags flags, + string primary, + string secondary, + IRespContextSource? source) : IEquatable, IComparable, IComparable +{ + public readonly int From = from; + public readonly int To = to; + public readonly int Port = port; + public readonly ShardFlags Flags = flags; + public readonly string Primary = primary; + public readonly string Secondary = secondary; + public bool Repliace => (Flags & ShardFlags.Replica) != 0; + + private readonly IRespContextSource? source = source; + + public override string ToString() => $"[{From}-{To}] {source}"; + public int CompareTo(object? obj) => obj is Shard shard ? CompareTo(in shard) : -1; + + public override int GetHashCode() => From ^ To ^ Port ^ (int)Flags ^ Primary.GetHashCode(); + + public override bool Equals([NotNullWhen(true)] object? obj) + => obj is Shard other && Equals(other); + + bool IEquatable.Equals(Shard other) => Equals(in other); + + public bool Equals(in Shard other) => + (From == other.From + & To == other.To + & Port == other.Port + & Flags == other.Flags + & Primary == other.Primary + & Secondary == other.Secondary) + && ReferenceEquals(source, other.source); + + int IComparable.CompareTo(Shard other) => CompareTo(in other); + + public int CompareTo(in Shard other) + { + int delta = From - other.From; + if (delta == 0) + { + delta = To - other.To; + if (delta == 0) + { + delta = (int)Flags - (int)other.Flags; + if (delta == 0) + { + delta = string.CompareOrdinal(Primary, other.Primary); + } + } + } + + return delta; + } + + public RespConnection? GetConnection() + { + if (source is not null) + { + // in this *very specific* case: watch out for null by-refs; we don't + // do this exhaustively! + ref readonly RespContext ctx = ref source.Context; + if (!Unsafe.IsNullRef(ref Unsafe.AsRef(in ctx))) return ctx.Connection; + } + + return null; + } +} diff --git a/src/RESPite/Connections/RespConnectionFactory.cs b/src/RESPite/Connections/RespConnectionFactory.cs new file mode 100644 index 000000000..702bc96c9 --- /dev/null +++ b/src/RESPite/Connections/RespConnectionFactory.cs @@ -0,0 +1,88 @@ +using System.Net; +using System.Net.Sockets; + +namespace RESPite.Connections; + +/// +/// Controls connection to endpoints. By default, this is TCP streams. +/// +// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global +public class RespConnectionFactory +{ + private static RespConnectionFactory? _default, _defaultTls; + public static RespConnectionFactory Default => _default ??= new(); + public static RespConnectionFactory DefaultTls => _defaultTls ??= new(true); + protected RespConnectionFactory(bool tls = false) => _tls = tls; + private readonly bool _tls; + + public virtual string DefaultHost => "127.0.0.1"; + public virtual int DefaultPort => _tls ? 6380 : 6379; + + /// + /// Connect to the designated endpoint and return an open for the duplex + /// connection. + /// + /// The location to connect to; how this is interpreted is implementation-specific, + /// but will commonly be an IP address or DNS hostname. + /// The port to connect to, if appropriate. + /// The configuration for the connection. + /// Cancellation for the operation. + /// An open for the duplex connection. + public virtual async ValueTask ConnectAsync( + string endpoint, + int port, + RespConfiguration? configuration = null, + CancellationToken cancellationToken = default) + { + var ep = GetEndPoint(endpoint, port); + Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.NoDelay = true; +#if NET6_0_OR_GREATER + await socket.ConnectAsync(endpoint ?? DefaultEndPoint, cancellationToken).ConfigureAwait(false); +#else + // hack together cancellation via dispose + using (cancellationToken.Register( + static state => ((Socket)state).Dispose(), socket)) + { + try + { + await socket.ConnectAsync(ep).ConfigureAwait(false); + } + catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(cancellationToken); + } + catch (SocketException) when (cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(cancellationToken); + } + } +#endif + var stream = new NetworkStream(socket); + var authed = await AuthenticateAsync(stream, cancellationToken).ConfigureAwait(false); + return RespConnection.Create(authed, configuration); + } + + protected virtual ValueTask AuthenticateAsync(Stream stream, CancellationToken cancellationToken) + { + if (_tls) throw new NotImplementedException("TLS"); + return new(stream); + } + + protected virtual EndPoint GetEndPoint(string endpoint, int port) + { + if (port == 0) port = DefaultPort; + if (string.IsNullOrWhiteSpace(endpoint)) + { + endpoint = DefaultHost; + } + + return endpoint switch + { + "127.0.0.1" => new IPEndPoint(IPAddress.Loopback, port), + "::1" or "0:0:0:0:0:0:0:1" => new IPEndPoint(IPAddress.IPv6Loopback, port), + _ when IPAddress.TryParse(endpoint, out var address) => new IPEndPoint(address, port), + _ => new DnsEndPoint(endpoint, port), + }; + } +} diff --git a/src/RESPite/Connections/RespConnectionManager.cs b/src/RESPite/Connections/RespConnectionManager.cs new file mode 100644 index 000000000..41e06d1f6 --- /dev/null +++ b/src/RESPite/Connections/RespConnectionManager.cs @@ -0,0 +1,231 @@ +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using RESPite.Connections.Internal; + +namespace RESPite.Connections; + +public sealed class RespConnectionManager : IRespContextSource +{ + /// + public override string ToString() => GetType().Name; + + // the routed connection performs message-inspection based routing; on a single node + // instance that isn't necessary, so the default-connection abstracts over that: + // in a single-node instance, the default-connection will be the single interactive connection + // otherwise, the default-connection will be the routed connection + private RoutedConnection? _routedConnection; + private RespContext _defaultContext = RespContext.Null; + internal ref readonly RespContext Context => ref _defaultContext; + ref readonly RespContext IRespContextSource.Context => ref _defaultContext; + + private readonly CancellationTokenSource _lifetime = new(); + + private RespConnectionFactory? _factory; + + public RespConnectionFactory ConnectionFactory + { + get => _factory ??= RespConnectionFactory.Default; + set + { + // ReSharper disable once JoinNullCheckWithUsage + if (value is null) throw new ArgumentNullException(nameof(ConnectionFactory)); + _factory = value; + } + } + + private Node[] _nodes = []; + internal CancellationToken Lifetime => _lifetime.Token; + private RespConfiguration? _options; + internal RespConfiguration Options => _options ?? ThrowNotConnected(); + + [DoesNotReturn] + private RespConfiguration ThrowNotConnected() + => throw new InvalidOperationException($"The {GetType().Name} has not been connected."); + + internal readonly struct EndpointPair(string endpoint, int port) + { + public override string ToString() => $"{Endpoint}:{Port}"; + + public readonly string Endpoint = endpoint; + public readonly int Port = port; + public override int GetHashCode() => (Endpoint?.GetHashCode() ?? 0) ^ Port; + + public override bool Equals(object? obj) => obj is EndpointPair other && + (Endpoint == other.Endpoint & Port == other.Port); + } + + private void OnConnect(RespConfiguration options, ReadOnlySpan endpoints) + { + if (options is null) throw new ArgumentNullException(nameof(options)); + if (Interlocked.CompareExchange(ref _options, options, null) is not null) + { + throw new InvalidOperationException($"A {GetType().Name} can only be connected once."); + } + + var nodes = new Node[Math.Max(endpoints.Length, 1)]; + var factory = ConnectionFactory; + if (endpoints.IsEmpty) + { + nodes[0] = new Node(this, factory.DefaultHost, factory.DefaultPort); + } + else + { + for (int i = 0; i < endpoints.Length; i++) + { + var host = endpoints[i].Endpoint; + if (string.IsNullOrWhiteSpace(host) || host is "." or "localhost") + host = "127.0.0.1"; + var port = endpoints[i].Port; + if (port == 0) port = factory.DefaultPort; + nodes[i] = new Node(this, host, port); + } + } + + _nodes = nodes; + } + + /* + public void Connect(string configuration = "", TextWriter? log = null) + { + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + var config = ConfigurationOptions.Parse(configuration ?? ""); + Connect(config, log); + } + + public void Connect(ConfigurationOptions options, TextWriter? log = null) + // use sync over async; reduce code-duplication, and sync wouldn't add anything + => ConnectAsync(options, log).Wait(Lifetime); + + public Task ConnectAsync(string configuration = "", TextWriter? log = null) + { + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + if (string.IsNullOrWhiteSpace(configuration)) configuration = "."; // localhost by default + var config = ConfigurationOptions.Parse(configuration ?? ""); + return ConnectAsync(config, log); + } + + public async Task ConnectAsync(ConfigurationOptions options, TextWriter? log = null) + { + OnConnect(options); + var snapshot = _nodes; + log.LogLocked($"Connecting to {snapshot.Length} nodes..."); + Task[] pending = new Task[snapshot.Length]; + for (int i = 0; i < snapshot.Length; i++) + { + pending[i] = snapshot[i].ConnectAsync(log); + } + + await Task.WhenAll(pending).ConfigureAwait(false); + int success = 0; + foreach (var task in pending) + { + // note WhenAll ensures all connected + if (task.Result) success++; + } + + // configure our primary connection + OnNodesChanged(); + + log.LogLocked($"Connected to {success} of {snapshot.Length} nodes."); + } + */ + + public void Dispose() + { + var routed = _routedConnection; + _routedConnection = null; + _defaultContext = NullConnection.Disposed.Context; + _lifetime.Cancel(); + routed?.Dispose(); + foreach (var node in _nodes) + { + node.Dispose(); + } + } + + public async ValueTask DisposeAsync() + { + var routed = _routedConnection; + _routedConnection = null; + _defaultContext = NullConnection.Disposed.Context; +#if NET8_0_OR_GREATER + await _lifetime.CancelAsync().ConfigureAwait(false); +#else + _lifetime.Cancel(); +#endif + if (routed is not null) + { + await routed.DisposeAsync().ConfigureAwait(false); + } + + foreach (var node in _nodes) + { + await node.DisposeAsync().ConfigureAwait(false); + } + } + + public string ClientName { get; private set; } = ""; + public int TimeoutMilliseconds => (int)Options.SyncTimeout.TotalMilliseconds; + public long OperationCount => 0; + + public bool PreserveAsyncOrder + { + get => false; + [Obsolete("This feature is no longer supported", false)] + set { } + } + + public bool IsConnected + { + get + { + foreach (var node in _nodes) + { + if (node.IsConnected) return true; + } + + return false; + } + } + + public bool IsConnecting + { + get + { + foreach (var node in _nodes) + { + if (node.IsConnecting) return true; + } + + return false; + } + } + + private void OnNodesChanged() + { + var nodes = _nodes; + _defaultContext = nodes.Length switch + { + 0 => NullConnection.NonRoutable.Context, // nowhere to go + 1 => nodes[0] is { IsConnected: true } conn + ? conn.Context + : NullConnection.NonRoutable.Context, // nowhere to go + _ => BuildRouted(nodes), + }; + } + + private ref readonly RespContext BuildRouted(Node[] nodes) + { + Shard[] oversized = ArrayPool.Shared.Rent(nodes.Length); + for (int i = 0; i < nodes.Length; i++) + { + oversized[i] = nodes[i].AsShard(); + } + + Array.Sort(oversized, 0, nodes.Length); + var conn = _routedConnection ??= new(); + conn.SetRoutingTable(new ReadOnlySpan(oversized, 0, nodes.Length)); + ArrayPool.Shared.Return(oversized); + return ref conn.Context; + } +} diff --git a/src/RESPite/Internal/RespMessageBase.cs b/src/RESPite/Internal/RespMessageBase.cs index b114e17fb..1d06f9150 100644 --- a/src/RESPite/Internal/RespMessageBase.cs +++ b/src/RESPite/Internal/RespMessageBase.cs @@ -12,12 +12,12 @@ internal abstract class RespMessageBase : IValueTaskSource private CancellationToken _cancellationToken; private CancellationTokenRegistration _cancellationTokenRegistration; - private int _requestRefCount, _flags; + private int _requestRefCount, _flags, _slot; private ReadOnlySequence _request; public ref readonly CancellationToken CancellationToken => ref _cancellationToken; [Flags] - protected enum StateFlags + internal enum StateFlags { None = 0, IsSent = 1 << 0, // the request has been sent @@ -28,10 +28,13 @@ protected enum StateFlags HasParser = 1 << 6, // we have a parser MetadataParser = 1 << 7, // the parser wants to consume metadata InlineParser = 1 << 8, // we can safely use the parser on the IO thread + Replica = 1 << 9, // request a replica (otherwise, primary is requested) + Demand = 1 << 10, // the presence/absence of Replica is a hard demand } - protected StateFlags Flags => (StateFlags)Volatile.Read(ref _flags); + internal StateFlags Flags => (StateFlags)Volatile.Read(ref _flags); public virtual int MessageCount => 1; + internal int Slot => _slot; protected void InitParser(object? parser) { @@ -188,8 +191,8 @@ protected virtual void Reset(bool recycle) // note we only reset on success, and on // success we've already unregistered cancellation _request = default; - _requestRefCount = 0; - _flags = 0; + _requestRefCount = _flags = 0; + _slot = -1; NextToken(); if (recycle) Recycle(); } diff --git a/src/RESPite.StackExchange.Redis/Utils.cs b/src/RESPite/Internal/Utils.cs similarity index 94% rename from src/RESPite.StackExchange.Redis/Utils.cs rename to src/RESPite/Internal/Utils.cs index 0b24c8f5f..c3ba8f3d2 100644 --- a/src/RESPite.StackExchange.Redis/Utils.cs +++ b/src/RESPite/Internal/Utils.cs @@ -1,4 +1,4 @@ -namespace RESPite.StackExchange.Redis; +namespace RESPite.Internal; internal static class Utils { diff --git a/src/RESPite/RespConfiguration.cs b/src/RESPite/RespConfiguration.cs index fd0bdb519..f0d079e9f 100644 --- a/src/RESPite/RespConfiguration.cs +++ b/src/RESPite/RespConfiguration.cs @@ -10,15 +10,16 @@ public class RespConfiguration private static readonly TimeSpan DefaultSyncTimeout = TimeSpan.FromSeconds(10); public static RespConfiguration Default { get; } = new( - RespCommandMap.Default, [], DefaultSyncTimeout, NullServiceProvider.Instance); + RespCommandMap.Default, [], DefaultSyncTimeout, NullServiceProvider.Instance, 0); - public static Builder Create() => default; // for discoverability + public static Builder CreateBuilder() => default; // for discoverability public struct Builder // intentionally mutable { public TimeSpan? SyncTimeout { get; set; } public IServiceProvider? ServiceProvider { get; set; } public RespCommandMap? CommandMap { get; set; } + public int DefaultDatabase { get; set; } public object? KeyPrefix { get; set; } // can be a string or byte[] public Builder(RespConfiguration? source) @@ -29,13 +30,14 @@ public Builder(RespConfiguration? source) SyncTimeout = source.SyncTimeout; KeyPrefix = source.KeyPrefix.ToArray(); ServiceProvider = source.ServiceProvider; + DefaultDatabase = source.DefaultDatabase; // undo defaults if (ReferenceEquals(CommandMap, RespCommandMap.Default)) CommandMap = null; if (ReferenceEquals(ServiceProvider, NullServiceProvider.Instance)) ServiceProvider = null; } } - public RespConfiguration Create() + public RespConfiguration CreateConfiguration() { byte[] prefix = KeyPrefix switch { @@ -53,7 +55,8 @@ public RespConfiguration Create() CommandMap ?? RespCommandMap.Default, prefix, SyncTimeout ?? DefaultSyncTimeout, - ServiceProvider ?? NullServiceProvider.Instance); + ServiceProvider ?? NullServiceProvider.Instance, + DefaultDatabase); } } @@ -61,12 +64,14 @@ private RespConfiguration( RespCommandMap commandMap, byte[] keyPrefix, TimeSpan syncTimeout, - IServiceProvider serviceProvider) + IServiceProvider serviceProvider, + int defaultDatabase) { CommandMap = commandMap; SyncTimeout = syncTimeout; _keyPrefix = (byte[])keyPrefix.Clone(); // create isolated copy ServiceProvider = serviceProvider; + DefaultDatabase = defaultDatabase; } private readonly byte[] _keyPrefix; @@ -74,6 +79,7 @@ private RespConfiguration( public RespCommandMap CommandMap { get; } public TimeSpan SyncTimeout { get; } public ReadOnlySpan KeyPrefix => _keyPrefix; + public int DefaultDatabase { get; } public Builder AsBuilder() => new(this); diff --git a/src/RESPite/RespConnection.cs b/src/RESPite/RespConnection.cs index ddde8942d..d9f056fce 100644 --- a/src/RESPite/RespConnection.cs +++ b/src/RESPite/RespConnection.cs @@ -3,12 +3,13 @@ using System.Net; using System.Net.Sockets; using System.Runtime.CompilerServices; +using RESPite.Connections; using RESPite.Connections.Internal; using RESPite.Internal; namespace RESPite; -public abstract class RespConnection : IDisposable, IAsyncDisposable +public abstract class RespConnection : IDisposable, IAsyncDisposable, IRespContextSource { public sealed class RespConnectionErrorEventArgs(Exception exception, [CallerMemberName] string operation = "") : EventArgs @@ -41,51 +42,9 @@ private protected static void OnConnectionError( internal readonly RespCommandMap? NonDefaultCommandMap; // prevent checking this each write public TimeSpan SyncTimeout { get; } - private static EndPoint? _defaultEndPoint; // do not expose externally; vexingly mutable - private static EndPoint DefaultEndPoint => _defaultEndPoint ??= new IPEndPoint(IPAddress.Loopback, 6379); - public static RespConnection Create(Stream stream, RespConfiguration? configuration = null) => new StreamConnection(configuration ?? RespConfiguration.Default, stream); - public static RespConnection Create(EndPoint? endpoint = null, RespConfiguration? config = null) - { - Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - socket.NoDelay = true; - socket.Connect(endpoint ?? DefaultEndPoint); - return Create(new NetworkStream(socket), config); - } - - public static async ValueTask CreateAsync( - EndPoint? endpoint = null, - RespConfiguration? config = null, - CancellationToken cancellationToken = default) - { - Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - socket.NoDelay = true; -#if NET6_0_OR_GREATER - await socket.ConnectAsync(endpoint ?? DefaultEndPoint, cancellationToken).ConfigureAwait(false); -#else - // hack together cancellation via dispose - using (var reg = cancellationToken.Register( - static state => ((Socket)state).Dispose(), socket)) - { - try - { - await socket.ConnectAsync(endpoint).ConfigureAwait(false); - } - catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested) - { - throw new OperationCanceledException(cancellationToken); - } - catch (SocketException) when (cancellationToken.IsCancellationRequested) - { - throw new OperationCanceledException(cancellationToken); - } - } -#endif - return Create(new NetworkStream(socket), config); - } - // this is the usual usage, since we want context to be preserved private protected RespConnection(in RespContext tail, RespConfiguration? configuration = null) { diff --git a/src/RESPite/RespOperation.cs b/src/RESPite/RespOperation.cs index 9154396e9..572ea17c3 100644 --- a/src/RESPite/RespOperation.cs +++ b/src/RESPite/RespOperation.cs @@ -46,6 +46,7 @@ internal RespOperation(RespMessageBase message, short token, bool disableCapture _token = token; _disableCaptureContext = disableCaptureContext; } + internal RespOperation(RespMessageBase message, bool disableCaptureContext = false) { _message = message; @@ -91,8 +92,13 @@ public void Wait(TimeSpan timeout = default) internal short Token => _token; internal int MessageCount => Message.MessageCount; internal bool TrySetException(Exception exception) => Message.TrySetException(_token, exception); - internal bool TrySetCancelled(CancellationToken cancellationToken = default) => Message.TrySetCanceled(_token, cancellationToken); - internal bool TryReserveRequest(out ReadOnlySequence payload, bool recordSent = true) => Message.TryReserveRequest(_token, out payload, recordSent); + + internal bool TrySetCancelled(CancellationToken cancellationToken = default) => + Message.TrySetCanceled(_token, cancellationToken); + + internal bool TryReserveRequest(out ReadOnlySequence payload, bool recordSent = true) => + Message.TryReserveRequest(_token, out payload, recordSent); + internal void ReleaseRequest() => Message.ReleaseRequest(); internal static readonly Action InvokeState = static state => ((Action)state!).Invoke(); @@ -225,5 +231,9 @@ public static RespOperation Create( internal bool TryGetSubMessages(out ReadOnlySpan operations) => Message.TryGetSubMessages(Token, out operations); + internal bool TrySetResultAfterUnloadingSubMessages() => Message.TrySetResultAfterUnloadingSubMessages(Token); + + internal RespMessageBase.StateFlags Flags => Message.Flags; + internal int Slot => _message.Slot; } From 2d1a74030e316ee696d53d559bc8499524593606 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 15 Sep 2025 15:28:07 +0100 Subject: [PATCH 065/108] in progress --- .../{NodeServer.cs => RespContextServer.cs} | 17 ++-- .../RespMultiplexer.cs | 37 +------- src/RESPite/Connections/Internal/Node.cs | 3 +- .../Connections/RespConnectionFactory.cs | 2 +- src/RESPite/Connections/RespConnectionPool.cs | 95 +++++++++++++------ 5 files changed, 80 insertions(+), 74 deletions(-) rename src/RESPite.StackExchange.Redis/{NodeServer.cs => RespContextServer.cs} (96%) diff --git a/src/RESPite.StackExchange.Redis/NodeServer.cs b/src/RESPite.StackExchange.Redis/RespContextServer.cs similarity index 96% rename from src/RESPite.StackExchange.Redis/NodeServer.cs rename to src/RESPite.StackExchange.Redis/RespContextServer.cs index c07cd90b0..66f0a80c8 100644 --- a/src/RESPite.StackExchange.Redis/NodeServer.cs +++ b/src/RESPite.StackExchange.Redis/RespContextServer.cs @@ -1,27 +1,28 @@ using System.Net; +using RESPite.Connections; using StackExchange.Redis; namespace RESPite.StackExchange.Redis; /// -/// Implements IServer on top of a , which represents a fixed single connection +/// Implements IServer on top of a , which represents a fixed single connection /// to a single redis instance. The connection exposed is the "interactive" connection. /// -internal sealed class NodeServer(Node node) : IServer +internal sealed class RespContextServer(IRedisAsync parent, IRespContextSource source) : IServer { // deliberately not caching this - if the connection changes, we want to know about it - internal ref readonly RespContext Context => ref node.Context; + internal ref readonly RespContext Context => ref source.Context; - public IConnectionMultiplexer Multiplexer => node.Multiplexer; + public IConnectionMultiplexer Multiplexer => parent.Multiplexer; public Task PingAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public bool TryWait(Task task) => node.Multiplexer.TryWait(task); + public bool TryWait(Task task) => parent.Multiplexer.TryWait(task); - public void Wait(Task task) => node.Multiplexer.Wait(task); + public void Wait(Task task) => parent.Multiplexer.Wait(task); - public T Wait(Task task) => node.Multiplexer.Wait(task); + public T Wait(Task task) => parent.Multiplexer.Wait(task); - public void WaitAll(params Task[] tasks) => node.Multiplexer.WaitAll(tasks); + public void WaitAll(params Task[] tasks) => parent.Multiplexer.WaitAll(tasks); public TimeSpan Ping(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); diff --git a/src/RESPite.StackExchange.Redis/RespMultiplexer.cs b/src/RESPite.StackExchange.Redis/RespMultiplexer.cs index 1987467ab..ad314b64b 100644 --- a/src/RESPite.StackExchange.Redis/RespMultiplexer.cs +++ b/src/RESPite.StackExchange.Redis/RespMultiplexer.cs @@ -1,11 +1,5 @@ -using System.Buffers; -using System.Diagnostics.CodeAnalysis; -using System.Net; -using RESPite.Connections; -using RESPite.Connections.Internal; +using RESPite.Connections; using StackExchange.Redis; -using StackExchange.Redis.Maintenance; -using StackExchange.Redis.Profiling; namespace RESPite.StackExchange.Redis; @@ -309,35 +303,9 @@ public IServer GetServer(EndPoint endpoint, object? asyncState = null) throw new ArgumentException("The specified endpoint is not defined", nameof(endpoint)); } - private void OnNodesChanged() - { - var nodes = _nodes; - _defaultContext = nodes.Length switch - { - 0 => NullConnection.NonRoutable.Context, // nowhere to go - 1 => nodes[0] is { IsConnected: true } conn - ? conn.Context - : NullConnection.NonRoutable.Context, // nowhere to go - _ => BuildRouted(nodes), - }; - } - - private ref readonly RespContext BuildRouted(Node[] nodes) - { - Shard[] oversized = ArrayPool.Shared.Rent(nodes.Length); - for (int i = 0; i < nodes.Length; i++) - { - oversized[i] = nodes[i].AsShard(); - } - Array.Sort(oversized, 0, nodes.Length); - var conn = _routedConnection ??= new(); - conn.SetRoutingTable(new ReadOnlySpan(oversized, 0, nodes.Length)); - ArrayPool.Shared.Return(oversized); - return ref conn.Context; - } public IServer[] GetServers() => Array.ConvertAll(_nodes, static x => x.AsServer()); - +*/ public Task ConfigureAsync(TextWriter? log = null) => throw new NotImplementedException(); public bool Configure(TextWriter? log = null) => throw new NotImplementedException(); @@ -365,5 +333,4 @@ public void ExportConfiguration(Stream destination, ExportOptions options = Expo throw new NotImplementedException(); public void AddLibraryNameSuffix(string suffix) => throw new NotImplementedException(); - */ } diff --git a/src/RESPite/Connections/Internal/Node.cs b/src/RESPite/Connections/Internal/Node.cs index 2ce12f253..c3521a6fa 100644 --- a/src/RESPite/Connections/Internal/Node.cs +++ b/src/RESPite/Connections/Internal/Node.cs @@ -150,13 +150,12 @@ public async Task ConnectAsync( log.LogLocked($"[{Label}] connecting..."); connecting = true; var manager = _node.Manager; - var stream = await manager.ConnectionFactory.ConnectAsync( + var connection = await manager.ConnectionFactory.ConnectAsync( _node.EndPoint, _node.Port, cancellationToken: cancellationToken).ConfigureAwait(false); connecting = false; - var connection = RespConnection.Create(stream, manager.Configuration); log.LogLocked($"[{Label}] Performing handshake..."); // TODO: handshake diff --git a/src/RESPite/Connections/RespConnectionFactory.cs b/src/RESPite/Connections/RespConnectionFactory.cs index 702bc96c9..ab2e48d99 100644 --- a/src/RESPite/Connections/RespConnectionFactory.cs +++ b/src/RESPite/Connections/RespConnectionFactory.cs @@ -38,7 +38,7 @@ public virtual async ValueTask ConnectAsync( Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.NoDelay = true; #if NET6_0_OR_GREATER - await socket.ConnectAsync(endpoint ?? DefaultEndPoint, cancellationToken).ConfigureAwait(false); + await socket.ConnectAsync(ep, cancellationToken).ConfigureAwait(false); #else // hack together cancellation via dispose using (cancellationToken.Register( diff --git a/src/RESPite/Connections/RespConnectionPool.cs b/src/RESPite/Connections/RespConnectionPool.cs index b305f8cc8..47364cfeb 100644 --- a/src/RESPite/Connections/RespConnectionPool.cs +++ b/src/RESPite/Connections/RespConnectionPool.cs @@ -17,21 +17,24 @@ public sealed class RespConnectionPool : IDisposable public bool UseCustomNetworkStream { get; set; } private readonly ConcurrentQueue _pool = []; - private readonly Func _createConnection; + private readonly Func> _createConnection; private readonly int _count; private readonly RespContext _defaultTemplate; public ref readonly RespContext Template => ref _defaultTemplate; public event EventHandler? ConnectionError; + private void OnConnectionError(object? sender, RespConnection.RespConnectionErrorEventArgs e) => ConnectionError?.Invoke(this, e); // mask sender private readonly EventHandler _onConnectionError; + public RespConnectionPool() : this(RespContext.Null, "127.0.0.1", 6379) { } + public RespConnectionPool( in RespContext template, - Func createConnection, + Func> createConnection, int count = DefaultCount) { _createConnection = createConnection; @@ -43,58 +46,90 @@ public RespConnectionPool( _onConnectionError = OnConnectionError; } - public RespConnectionPool( - Func createConnection, - int count = DefaultCount) : this(RespContext.Null, createConnection, count) - { - } - public RespConnectionPool( in RespContext template, - IPAddress? address = null, - int port = 6379, - int count = DefaultCount) - : this(in template, new IPEndPoint(address ?? IPAddress.Loopback, port), count) + string endpoint, + int port, + int count = DefaultCount, + RespConnectionFactory? connectionFactory = null) + : this(template, MakeCreateConnection(endpoint, port, connectionFactory), count) { } - public RespConnectionPool( - IPAddress? address = null, - int port = 6379, - int count = DefaultCount) : this(RespContext.Null, address, port, count) + private static Func> MakeCreateConnection( + string endpoint, + int port, + RespConnectionFactory? connectionFactory) { + connectionFactory ??= RespConnectionFactory.Default; + return (config, cancellationToken) + => connectionFactory.ConnectAsync(endpoint, port, config, cancellationToken); } - public RespConnectionPool(EndPoint endPoint, int count = DefaultCount) - : this(RespContext.Null, endPoint, count) + /// + /// Borrow a connection from the pool, using the default template. + /// + public RespConnection GetConnection(CancellationToken cancellationToken = default) { + if (cancellationToken.CanBeCanceled) + { + var context = _defaultTemplate.WithCancellationToken(cancellationToken); + return GetConnection(in context); + } + else + { + return GetConnection(in _defaultTemplate); + } } - public RespConnectionPool(in RespContext template, EndPoint endPoint, int count = DefaultCount) - : this(template, config => RespConnection.Create(endPoint, config), count) + public RespConnection GetConnection(in RespContext template) // sync over async { + var pending = GetConnectionAsync(in template); + if (!pending.IsCompleted) return pending.AsTask().GetAwaiter().GetResult(); + return pending.GetAwaiter().GetResult(); } /// /// Borrow a connection from the pool, using the default template. /// - public RespConnection GetConnection() => GetConnection(in _defaultTemplate); + public ValueTask GetConnectionAsync(CancellationToken cancellationToken = default) + { + if (cancellationToken.CanBeCanceled) + { + var context = _defaultTemplate.WithCancellationToken(cancellationToken); + return GetConnectionAsync(in context); + } + else + { + return GetConnectionAsync(in _defaultTemplate); + } + } /// /// Borrow a connection from the pool. /// /// The template context to use for the leased connection; everything except the connection /// will be inherited by the new context. - public RespConnection GetConnection(in RespContext template) + public ValueTask GetConnectionAsync(in RespContext template) { ThrowIfDisposed(); template.CancellationToken.ThrowIfCancellationRequested(); - if (!_pool.TryDequeue(out var connection)) - { - connection = _createConnection(template.Connection.Configuration); - connection.ConnectionError += _onConnectionError; - } + if (_pool.TryDequeue(out var connection)) return new(connection); + + var pending = _createConnection(template.Connection.Configuration, template.CancellationToken); + if (!pending.IsCompleted) return Awaited(template, pending); + + connection = pending.GetAwaiter().GetResult(); + connection.ConnectionError += _onConnectionError; + connection = new PoolWrapper(this, template.WithConnection(connection)); + return new(connection); + } + + private async ValueTask Awaited(RespContext template, ValueTask pending) + { + var connection = await pending.ConfigureAwait(false); + connection.ConnectionError += _onConnectionError; return new PoolWrapper(this, template.WithConnection(connection)); } @@ -131,18 +166,22 @@ private sealed class PoolWrapper( { protected override bool OwnsConnection => false; - private const string ConnectionErrorNotSupportedMessage = $"{nameof(ConnectionError)} events are not supported on pooled connections; use {nameof(RespConnectionPool)}.{nameof(RespConnectionPool.ConnectionError)} instead"; + private const string ConnectionErrorNotSupportedMessage = + $"{nameof(ConnectionError)} events are not supported on pooled connections; use {nameof(RespConnectionPool)}.{nameof(RespConnectionPool.ConnectionError)} instead"; + public override event EventHandler? ConnectionError { add => throw new NotSupportedException(ConnectionErrorNotSupportedMessage); remove => throw new NotSupportedException(ConnectionErrorNotSupportedMessage); } + protected override void OnDispose(bool disposing) { if (disposing) { pool.Return(Tail); } + base.OnDispose(disposing); } From c0f09dc5a3745d9e674896b043c92414c41044c9 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 16 Sep 2025 17:01:06 +0100 Subject: [PATCH 066/108] well, it compiles --- src/RESPite.Benchmark/BridgeBenchmark.cs | 2 +- .../RESPite.StackExchange.Redis.csproj | 1 + .../RespContextBatch.cs | 2 +- .../RespContextDatabase.Connection.cs | 11 +- ...textDatabase.Cs => RespContextDatabase.cs} | 17 +- .../RespContextServer.cs | 27 +- .../RespMultiplexer.cs | 389 ++++++++---------- src/RESPite/Connections/Internal/Node.cs | 1 + .../Connections/RespConnectionFactory.cs | 51 ++- .../Connections/RespConnectionManager.cs | 46 ++- src/RESPite/Connections/RespConnectionPool.cs | 4 +- tests/RESPite.Tests/ConnectionFixture.cs | 2 +- tests/RESPite.Tests/RespMultiplexerTests.cs | 5 +- 13 files changed, 290 insertions(+), 268 deletions(-) rename src/RESPite.StackExchange.Redis/{RespContextDatabase.Cs => RespContextDatabase.cs} (78%) diff --git a/src/RESPite.Benchmark/BridgeBenchmark.cs b/src/RESPite.Benchmark/BridgeBenchmark.cs index 30ac8078a..defa330c1 100644 --- a/src/RESPite.Benchmark/BridgeBenchmark.cs +++ b/src/RESPite.Benchmark/BridgeBenchmark.cs @@ -9,7 +9,7 @@ public sealed class BridgeBenchmark(string[] args) : OldCoreBenchmarkBase(args) protected override IConnectionMultiplexer Create(int port) { var obj = new RespMultiplexer(); - obj.Connect($"127.0.0.1:{Port}"); + obj.Connect("127.0.0.1:{Port}"); return obj; } } diff --git a/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj b/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj index 725f17405..c912996e5 100644 --- a/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj +++ b/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj @@ -8,6 +8,7 @@ $(NoWarn);CS1591 2025 - $([System.DateTime]::Now.Year) Marc Gravell readme.md + false diff --git a/src/RESPite.StackExchange.Redis/RespContextBatch.cs b/src/RESPite.StackExchange.Redis/RespContextBatch.cs index f16353a2d..d6e80cc6f 100644 --- a/src/RESPite.StackExchange.Redis/RespContextBatch.cs +++ b/src/RESPite.StackExchange.Redis/RespContextBatch.cs @@ -7,7 +7,7 @@ internal sealed class RespContextBatch : RespContextDatabase, IBatch, IDisposabl { private readonly RespBatch _batch; - public RespContextBatch(IRedisAsync parent, IRespContextSource source, int db) : base(parent, source, db) + public RespContextBatch(IConnectionMultiplexer muxer, IRespContextSource source, int db) : base(muxer, source, db) { _batch = source.Context.CreateBatch(); SetSource(this); diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs index 24488ab4b..0eb13978e 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs @@ -28,11 +28,16 @@ private PingParser() { } public EndPoint? IdentifyEndpoint(RedisKey key = default, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public IBatch CreateBatch(object? asyncState = null) => - new RespContextBatch(_source, _db); + public IBatch CreateBatch(object? asyncState = null) + { + if (asyncState is not null) throw new NotSupportedException($"{nameof(asyncState)} is not supported"); + return new RespContextBatch(_muxer, _source, _db); + } - public ITransaction CreateTransaction(object? asyncState = null) => + public ITransaction CreateTransaction(object? asyncState = null) + { throw new NotImplementedException(); + } // Key migration public Task KeyMigrateAsync( diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.cs similarity index 78% rename from src/RESPite.StackExchange.Redis/RespContextDatabase.Cs rename to src/RESPite.StackExchange.Redis/RespContextDatabase.cs index f27ea0dd3..9de355428 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.cs @@ -10,18 +10,19 @@ namespace RESPite.StackExchange.Redis; /// internal partial class RespContextDatabase : IDatabase { - private readonly IRedisAsync _parent; + private readonly IConnectionMultiplexer _muxer; private IRespContextSource _source; private readonly int _db; /// + /// Initializes a new instance of the class. /// Implements IDatabase on top of a , which provides access to a RESP context; this /// could be direct to a known server or routed - the is responsible for /// that determination. /// - public RespContextDatabase(IRedisAsync parent, IRespContextSource source, int db) + public RespContextDatabase(IConnectionMultiplexer muxer, IRespContextSource source, int db) { - _parent = parent; + _muxer = muxer; _source = source; _db = db; } @@ -48,13 +49,13 @@ private RespContext Context(CommandFlags flags) private TimeSpan SyncTimeout => _source.Context.SyncTimeout; public int Database => _db; - IConnectionMultiplexer IRedisAsync.Multiplexer => _parent.Multiplexer; + IConnectionMultiplexer IRedisAsync.Multiplexer => _muxer; - public bool TryWait(Task task) => _parent.Multiplexer.TryWait(task); + public bool TryWait(Task task) => task.Wait(SyncTimeout); - public void Wait(Task task) => _parent.Multiplexer.Wait(task); + public void Wait(Task task) => _muxer.Wait(task); - public T Wait(Task task) => _parent.Multiplexer.Wait(task); + public T Wait(Task task) => _muxer.Wait(task); - public void WaitAll(params Task[] tasks) => _parent.Multiplexer.WaitAll(tasks); + public void WaitAll(params Task[] tasks) => _muxer.WaitAll(tasks); } diff --git a/src/RESPite.StackExchange.Redis/RespContextServer.cs b/src/RESPite.StackExchange.Redis/RespContextServer.cs index 66f0a80c8..d8433f4a7 100644 --- a/src/RESPite.StackExchange.Redis/RespContextServer.cs +++ b/src/RESPite.StackExchange.Redis/RespContextServer.cs @@ -1,5 +1,6 @@ using System.Net; using RESPite.Connections; +using RESPite.Connections.Internal; using StackExchange.Redis; namespace RESPite.StackExchange.Redis; @@ -8,36 +9,36 @@ namespace RESPite.StackExchange.Redis; /// Implements IServer on top of a , which represents a fixed single connection /// to a single redis instance. The connection exposed is the "interactive" connection. /// -internal sealed class RespContextServer(IRedisAsync parent, IRespContextSource source) : IServer +internal sealed class RespContextServer(RespMultiplexer muxer, Node node) : IServer { // deliberately not caching this - if the connection changes, we want to know about it - internal ref readonly RespContext Context => ref source.Context; + internal ref readonly RespContext Context => ref node.Context; - public IConnectionMultiplexer Multiplexer => parent.Multiplexer; + public IConnectionMultiplexer Multiplexer => muxer; public Task PingAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public bool TryWait(Task task) => parent.Multiplexer.TryWait(task); + public bool TryWait(Task task) => task.Wait(Multiplexer.TimeoutMilliseconds); - public void Wait(Task task) => parent.Multiplexer.Wait(task); + public void Wait(Task task) => Multiplexer.Wait(task); - public T Wait(Task task) => parent.Multiplexer.Wait(task); + public T Wait(Task task) => Multiplexer.Wait(task); - public void WaitAll(params Task[] tasks) => parent.Multiplexer.WaitAll(tasks); + public void WaitAll(params Task[] tasks) => Multiplexer.WaitAll(tasks); public TimeSpan Ping(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public ClusterConfiguration? ClusterConfiguration { get; } - public EndPoint EndPoint => node.EndPoint; + public ClusterConfiguration? ClusterConfiguration => throw new NotImplementedException(); + public EndPoint EndPoint => node.Manager.ConnectionFactory.GetEndPoint(node.EndPoint, node.Port); public RedisFeatures Features => new(Version); public bool IsConnected => node.IsConnected; public RedisProtocol Protocol { get; } - public bool IsSlave { get; } - public bool IsReplica { get; } + bool IServer.IsSlave => node.IsReplica; + public bool IsReplica => node.IsReplica; public bool AllowSlaveWrites { get; set; } public bool AllowReplicaWrites { get; set; } public ServerType ServerType { get; } - public Version Version => node.Version; - public int DatabaseCount { get; } + public Version Version => throw new NotImplementedException(); + public int DatabaseCount => throw new NotImplementedException(); public void ClientKill(EndPoint endpoint, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task ClientKillAsync(EndPoint endpoint, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); diff --git a/src/RESPite.StackExchange.Redis/RespMultiplexer.cs b/src/RESPite.StackExchange.Redis/RespMultiplexer.cs index ad314b64b..6d54be655 100644 --- a/src/RESPite.StackExchange.Redis/RespMultiplexer.cs +++ b/src/RESPite.StackExchange.Redis/RespMultiplexer.cs @@ -1,311 +1,274 @@ -using RESPite.Connections; +using System.Buffers; +using System.Net; +using RESPite.Connections; +using RESPite.Connections.Internal; using StackExchange.Redis; +using StackExchange.Redis.Maintenance; +using StackExchange.Redis.Profiling; namespace RESPite.StackExchange.Redis; public sealed class RespMultiplexer : IConnectionMultiplexer { - /// - public override string ToString() => GetType().Name; - private readonly RespConnectionManager _connectionManager = new(); - private int _defaultDatabase; - - /* - // the routed connection performs message-inspection based routing; on a single node - // instance that isn't necessary, so the default-connection abstracts over that: - // in a single-node instance, the default-connection will be the single interactive connection - // otherwise, the default-connection will be the routed connection - private RoutedConnection? _routedConnection; - private RespContext _defaultContext = RespContext.Null; - internal ref readonly RespContext Context => ref _defaultContext; - ref readonly RespContext IRespContextSource.Context => ref _defaultContext; - RespContextProxyKind ITypedRespContextSource.RespContextProxyKind => RespContextProxyKind.Multiplexer; - RespMultiplexer ITypedRespContextSource.Multiplexer => this; - - private readonly CancellationTokenSource _lifetime = new(); private ConfigurationOptions? _options; - internal RespConfiguration Configuration { get; private set; } = RespConfiguration.Default; - private Node[] _nodes = []; - internal CancellationToken Lifetime => _lifetime.Token; - internal ConfigurationOptions Options => _options ?? ThrowNotConnected(); + private string _clientName = ""; - [DoesNotReturn] - private static ConfigurationOptions ThrowNotConnected() - => throw new InvalidOperationException($"The {nameof(RespMultiplexer)} has not been connected."); - - private void OnConnect(ConfigurationOptions options) + private ConfigurationOptions Options { - if (options is null) throw new ArgumentNullException(nameof(options)); - if (Interlocked.CompareExchange(ref _options, options, null) is not null) + get { - throw new InvalidOperationException($"A {GetType().Name} can only be connected once."); - } + return _options ?? ThrowNotConnected(); - // fixup the endpoints in an isolated collection - var ep = options.EndPoints.Clone(); - if (ep.Count == 0) - { - // no endpoints; add a default, deferring the port to the SSL setting - ep.Add(new IPEndPoint(IPAddress.Loopback, 0)); + static ConfigurationOptions ThrowNotConnected() => + throw new InvalidOperationException("Not connected."); } - else + set { - for (int i = 0; i < ep.Count; i++) - { - if (ep[i] is DnsEndPoint { Host: "." or "localhost" } dns) - { - // unroll loopback - ep[i] = new IPEndPoint(IPAddress.Loopback, dns.Port); - } - } + if (value is null) throw new ArgumentNullException(nameof(Options)); + if (Interlocked.CompareExchange(ref _options, value, null) is not null) + throw new InvalidOperationException("Options have already been set."); } + } - ep.SetDefaultPorts(ServerType.Standalone, ssl: options.Ssl); + /// + public override string ToString() => GetType().Name; - // add nodes from the endpoints - var nodes = new Node[ep.Count]; - for (int i = 0; i < nodes.Length; i++) - { - nodes[i] = new Node(this, ep[i]); - } + public ValueTask DisposeAsync() => _connectionManager.DisposeAsync(); - _nodes = nodes; - _defaultDatabase = options.DefaultDatabase ?? 0; - } + public void Dispose() => _connectionManager.Dispose(); - public void Connect(string configuration = "", TextWriter? log = null) - { - // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract - var config = ConfigurationOptions.Parse(configuration ?? ""); - Connect(config, log); - } + public void Connect(string configurationString, TextWriter? log = null) + => Connect(ConfigurationOptions.Parse(configurationString), log); public void Connect(ConfigurationOptions options, TextWriter? log = null) - // use sync over async; reduce code-duplication, and sync wouldn't add anything - => ConnectAsync(options, log).Wait(Lifetime); - - public Task ConnectAsync(string configuration = "", TextWriter? log = null) { - // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract - if (string.IsNullOrWhiteSpace(configuration)) configuration = "."; // localhost by default - var config = ConfigurationOptions.Parse(configuration ?? ""); - return ConnectAsync(config, log); + Options = options; + var parsed = ParseOptions(options, out _clientName); + _connectionManager.Connect(parsed, GetEndpoints(options, out var oversized), log); + ArrayPool.Shared.Return(oversized); } + public Task ConnectAsync(string configurationString, TextWriter? log = null) + => ConnectAsync(ConfigurationOptions.Parse(configurationString), log); + public async Task ConnectAsync(ConfigurationOptions options, TextWriter? log = null) { - OnConnect(options); - var snapshot = _nodes; - log.LogLocked($"Connecting to {snapshot.Length} nodes..."); - Task[] pending = new Task[snapshot.Length]; - for (int i = 0; i < snapshot.Length; i++) - { - pending[i] = snapshot[i].ConnectAsync(log); - } - - await Task.WhenAll(pending).ConfigureAwait(false); - int success = 0; - foreach (var task in pending) - { - // note WhenAll ensures all connected - if (task.Result) success++; - } - - // configure our primary connection - OnNodesChanged(); - - log.LogLocked($"Connected to {success} of {snapshot.Length} nodes."); + Options = options; + var parsed = ParseOptions(options, out _clientName); + await _connectionManager.ConnectAsync(parsed, GetEndpoints(options, out var oversized), log); + ArrayPool.Shared.Return(oversized); } - public void Dispose() + private static RespConfiguration ParseOptions(ConfigurationOptions options, out string clientName) { - var routed = _routedConnection; - _routedConnection = null; - _defaultContext = NullConnection.Disposed.Context; - _lifetime.Cancel(); - routed?.Dispose(); - foreach (var node in _nodes) - { - node.Dispose(); - } + var config = RespConfiguration.Default.AsBuilder(); + clientName = options.ClientName ?? options.Defaults.ClientName; + config.SyncTimeout = TimeSpan.FromMilliseconds(options.SyncTimeout); + config.DefaultDatabase = options.DefaultDatabase ?? 0; + return config.CreateConfiguration(); } - public async ValueTask DisposeAsync() + private ReadOnlySpan GetEndpoints( + ConfigurationOptions options, + out RespConnectionManager.EndpointPair[] oversized) { - var routed = _routedConnection; - _routedConnection = null; - _defaultContext = NullConnection.Disposed.Context; -#if NET8_0_OR_GREATER - await _lifetime.CancelAsync().ConfigureAwait(false); -#else - _lifetime.Cancel(); -#endif - if (routed is not null) + oversized = ArrayPool.Shared.Rent(Math.Max(options.EndPoints.Count, 1)); + if (options.EndPoints.Count == 0) { - await routed.DisposeAsync().ConfigureAwait(false); + oversized[0] = new("127.0.0.1", 6379); + return oversized.AsSpan(0, 1); } - - foreach (var node in _nodes) + else { - await node.DisposeAsync().ConfigureAwait(false); + int count = 0; + foreach (var endpoint in options.EndPoints) + { + if (!_connectionManager.ConnectionFactory.TryParse(endpoint, out var host, out var port)) + { + throw new ArgumentException($"Could not parse host and port from {endpoint}", nameof(endpoint)); + } + + oversized[count++] = new(host, port); + } + + return oversized.AsSpan(0, count); } } - public string ClientName { get; private set; } = ""; + // ReSharper disable once ConvertToAutoProperty + string IConnectionMultiplexer.ClientName => _clientName; + string IConnectionMultiplexer.Configuration => Options.ToString(includePassword: false); - public int TimeoutMilliseconds => (int)Configuration.SyncTimeout.TotalMilliseconds; - public long OperationCount => 0; - public bool PreserveAsyncOrder + private int SyncTimeoutMilliseconds => Options.SyncTimeout; + int IConnectionMultiplexer.TimeoutMilliseconds => Options.SyncTimeout; + + long IConnectionMultiplexer.OperationCount => _connectionManager.OperationCount; + + bool IConnectionMultiplexer.PreserveAsyncOrder { get => false; - [Obsolete( - "Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue) + - " - this will be removed in 3.0.", - false)] set { } } - public bool IsConnected - { - get - { - foreach (var node in _nodes) - { - if (node.IsConnected) return true; - } + public bool IsConnected => _connectionManager.IsConnected; - return false; - } - } + bool IConnectionMultiplexer.IsConnecting => _connectionManager.IsConnecting; - public bool IsConnecting + bool IConnectionMultiplexer.IncludeDetailInExceptions { - get - { - foreach (var node in _nodes) - { - if (node.IsConnecting) return true; - } + get => Options.IncludeDetailInExceptions; + set => Options.IncludeDetailInExceptions = value; + } - return false; - } + int IConnectionMultiplexer.StormLogThreshold + { + get => 0; + set { } } - public bool IncludeDetailInExceptions { get; set; } - public int StormLogThreshold { get; set; } + void IConnectionMultiplexer.RegisterProfiler(Func profilingSessionProvider) { } - public void RegisterProfiler(Func profilingSessionProvider) => - throw new NotImplementedException(); + ServerCounters IConnectionMultiplexer.GetCounters() => throw new NotImplementedException(); - public ServerCounters GetCounters() => throw new NotImplementedException(); +#pragma warning disable CS0067 // Event is never used + private event EventHandler? ErrorMessage; - public event EventHandler? ConnectionFailed; - public event EventHandler? ConnectionRestored; - internal EventHandler? DirectConnectionFailed => ConnectionFailed; - internal EventHandler? DirectConnectionRestored => ConnectionRestored; + private event EventHandler? ConnectionFailed, ConnectionRestored; + private event EventHandler? InternalError; + private event EventHandler? ConfigurationChanged, ConfigurationChangedBroadcast; + private event EventHandler? ServerMaintenanceEvent; + private event EventHandler? HashSlotMoved; +#pragma warning restore CS0067 // Event is never used - public event EventHandler? ErrorMessage; - public event EventHandler? InternalError; - public event EventHandler? ConfigurationChanged; - public event EventHandler? ConfigurationChangedBroadcast; - public event EventHandler? ServerMaintenanceEvent; + event EventHandler? IConnectionMultiplexer.ErrorMessage + { + add => ErrorMessage += value; + remove => ErrorMessage -= value; + } - internal void OnErrorMessage(RedisErrorEventArgs e) => ErrorMessage?.Invoke(this, e); - internal void OnInternalError(InternalErrorEventArgs e) => InternalError?.Invoke(this, e); - internal void OnConfigurationChanged(EndPointEventArgs e) => ConfigurationChanged?.Invoke(this, e); + event EventHandler? IConnectionMultiplexer.ConnectionFailed + { + add => ConnectionFailed += value; + remove => ConnectionFailed -= value; + } - internal void OnConfigurationChangedBroadcast(EndPointEventArgs e) => - ConfigurationChangedBroadcast?.Invoke(this, e); + event EventHandler? IConnectionMultiplexer.InternalError + { + add => InternalError += value; + remove => InternalError -= value; + } - internal void OnServerMaintenanceEvent(ServerMaintenanceEvent e) => ServerMaintenanceEvent?.Invoke(this, e); + event EventHandler? IConnectionMultiplexer.ConnectionRestored + { + add => ConnectionRestored += value; + remove => ConnectionRestored -= value; + } - public EndPoint[] GetEndPoints(bool configuredOnly = false) => configuredOnly - ? Options.EndPoints.ToArray() - : Array.ConvertAll(_nodes, x => x.EndPoint); + event EventHandler? IConnectionMultiplexer.ConfigurationChanged + { + add => ConfigurationChanged += value; + remove => ConfigurationChanged -= value; + } - public bool TryWait(Task task) => task.Wait(Configuration.SyncTimeout); + event EventHandler? IConnectionMultiplexer.ConfigurationChangedBroadcast + { + add => ConfigurationChangedBroadcast += value; + remove => ConfigurationChangedBroadcast -= value; + } - public void Wait(Task task) + event EventHandler? IConnectionMultiplexer.ServerMaintenanceEvent { - bool timeout; - try - { - timeout = !task.Wait(Configuration.SyncTimeout); - } - catch (AggregateException ex) when (ex.InnerExceptions.Count == 1) + add => ServerMaintenanceEvent += value; + remove => ServerMaintenanceEvent -= value; + } + + public EndPoint[] GetEndPoints(bool configuredOnly = false) + { + throw new NotImplementedException(); + } + + void IConnectionMultiplexer.Wait(Task task) + { + if (!task.Wait(SyncTimeoutMilliseconds)) { - throw ex.InnerException ?? ex; + ThrowTimeout(); } - if (timeout) ThrowTimeout(); + task.GetAwaiter().GetResult(); } private static void ThrowTimeout() => throw new TimeoutException(); - public T Wait(Task task) + T IConnectionMultiplexer.Wait(Task task) { - Wait((Task)task); - return task.Result; + if (!task.Wait(SyncTimeoutMilliseconds)) + { + ThrowTimeout(); + } + + return task.GetAwaiter().GetResult(); } - public void WaitAll(params Task[] tasks) => throw new NotImplementedException(); + void IConnectionMultiplexer.WaitAll(params Task[] tasks) + { + if (!Task.WaitAll(tasks, SyncTimeoutMilliseconds)) + { + ThrowTimeout(); + } + } + + event EventHandler? IConnectionMultiplexer.HashSlotMoved + { + add => HashSlotMoved += value; + remove => HashSlotMoved -= value; + } - public event EventHandler? HashSlotMoved; - internal void OnHashSlotMoved(HashSlotMovedEventArgs e) => HashSlotMoved?.Invoke(this, e); - public int HashSlot(RedisKey key) => throw new NotImplementedException(); + int IConnectionMultiplexer.HashSlot(RedisKey key) => throw new NotImplementedException(); - public ISubscriber GetSubscriber(object? asyncState = null) => throw new NotImplementedException(); + ISubscriber IConnectionMultiplexer.GetSubscriber(object? asyncState) => throw new NotImplementedException(); public IDatabase GetDatabase(int db = -1, object? asyncState = null) { - if (db < 0) db = _defaultDatabase; - if (db < LowDatabaseCount) return _lowDatabases[db] ??= new RespContextDatabase(this, db); - return new RespContextDatabase(this, db); + if (db < 0) db = Options.DefaultDatabase ?? 0; + return new RespContextDatabase(this, _connectionManager, db); } - private const int LowDatabaseCount = 16; - private readonly IDatabase?[] _lowDatabases = new IDatabase?[LowDatabaseCount]; + IServer IConnectionMultiplexer.GetServer(string host, int port, object? asyncState) => + GetServer(_connectionManager.GetNode(host, port), asyncState); - public IServer GetServer(string host, int port, object? asyncState = null) - => GetServer(Format.ParseEndPoint(host, port), asyncState); + IServer IConnectionMultiplexer.GetServer(string hostAndPort, object? asyncState) => + GetServer(_connectionManager.GetNode(hostAndPort), asyncState); - public IServer GetServer(string hostAndPort, object? asyncState = null) => - Format.TryParseEndPoint(hostAndPort, out var ep) - ? GetServer(ep, asyncState) - : throw new ArgumentException($"The specified host and port could not be parsed: {hostAndPort}", - nameof(hostAndPort)); + IServer IConnectionMultiplexer.GetServer(IPAddress host, int port) => + GetServer(_connectionManager.GetNode(host.ToString(), port), null); - public IServer GetServer(IPAddress host, int port) + public IServer GetServer(EndPoint endpoint, object? asyncState = null) { - foreach (var node in _nodes) + if (!_connectionManager.ConnectionFactory.TryParse(endpoint, out var host, out var port)) { - if (node.EndPoint is IPEndPoint ep && ep.Address.Equals(host) && ep.Port == port) - { - return node.AsServer(); - } + throw new ArgumentException($"Could not parse host and port from {endpoint}", nameof(endpoint)); } - throw new ArgumentException("The specified endpoint is not defined", nameof(host)); + return GetServer(_connectionManager.GetNode(host, port), asyncState); } - public IServer GetServer(EndPoint endpoint, object? asyncState = null) + private IServer GetServer(Node node, object? asyncState) { - foreach (var node in _nodes) + if (asyncState is not null) ThrowNotSupported(); + if (node.UserObject is not IServer server) { - if (node.EndPoint.Equals(endpoint)) - { - return node.AsServer(); - } + server = new RespContextServer(this, node); + node.UserObject = server; } - throw new ArgumentException("The specified endpoint is not defined", nameof(endpoint)); + return server; + static void ThrowNotSupported() => throw new NotSupportedException($"{nameof(asyncState)} is not supported"); } + IServer[] IConnectionMultiplexer.GetServers() => throw new NotImplementedException(); - public IServer[] GetServers() => Array.ConvertAll(_nodes, static x => x.AsServer()); -*/ public Task ConfigureAsync(TextWriter? log = null) => throw new NotImplementedException(); public bool Configure(TextWriter? log = null) => throw new NotImplementedException(); diff --git a/src/RESPite/Connections/Internal/Node.cs b/src/RESPite/Connections/Internal/Node.cs index c3521a6fa..d58aab3ca 100644 --- a/src/RESPite/Connections/Internal/Node.cs +++ b/src/RESPite/Connections/Internal/Node.cs @@ -20,6 +20,7 @@ public Node(RespConnectionManager manager, string endPoint, int port) _interactive = new(this, false); } + internal object? UserObject { get; set; } public bool IsConnected => _interactive.IsConnected; public bool IsConnecting => _interactive.IsConnecting; public bool IsReplica { get; private set; } diff --git a/src/RESPite/Connections/RespConnectionFactory.cs b/src/RESPite/Connections/RespConnectionFactory.cs index ab2e48d99..9077f6d86 100644 --- a/src/RESPite/Connections/RespConnectionFactory.cs +++ b/src/RESPite/Connections/RespConnectionFactory.cs @@ -1,4 +1,5 @@ -using System.Net; +using System.Globalization; +using System.Net; using System.Net.Sockets; namespace RESPite.Connections; @@ -69,7 +70,7 @@ protected virtual ValueTask AuthenticateAsync(Stream stream, Cancellatio return new(stream); } - protected virtual EndPoint GetEndPoint(string endpoint, int port) + protected internal virtual EndPoint GetEndPoint(string endpoint, int port) { if (port == 0) port = DefaultPort; if (string.IsNullOrWhiteSpace(endpoint)) @@ -85,4 +86,50 @@ protected virtual EndPoint GetEndPoint(string endpoint, int port) _ => new DnsEndPoint(endpoint, port), }; } + + public virtual bool TryParse(EndPoint endpoint, out string host, out int port) + { + if (endpoint is DnsEndPoint dns) + { + host = dns.Host switch + { + "localhost" or "." => "127.0.0.1", + _ => dns.Host, + }; + port = dns.Port; + return true; + } + + if (endpoint is IPEndPoint ip) + { + host = ip.Address.ToString(); + port = ip.Port; + return true; + } + + host = ""; + port = 0; + return false; + } + + public virtual bool TryParse(string hostAndPort, out string host, out int port) + { + int i = hostAndPort.LastIndexOf(':'); + if (i < 0) + { + host = hostAndPort; + port = 0; + return true; + } + + host = hostAndPort.Substring(0, i); + if (int.TryParse(hostAndPort.Substring(i + 1), NumberStyles.Integer, CultureInfo.InvariantCulture, out port)) + { + return true; + } + + host = hostAndPort; + port = 0; + return false; + } } diff --git a/src/RESPite/Connections/RespConnectionManager.cs b/src/RESPite/Connections/RespConnectionManager.cs index 41e06d1f6..d3072ed37 100644 --- a/src/RESPite/Connections/RespConnectionManager.cs +++ b/src/RESPite/Connections/RespConnectionManager.cs @@ -1,6 +1,7 @@ using System.Buffers; using System.Diagnostics.CodeAnalysis; using RESPite.Connections.Internal; +using RESPite.Internal; namespace RESPite.Connections; @@ -84,29 +85,13 @@ private void OnConnect(RespConfiguration options, ReadOnlySpan end _nodes = nodes; } - /* - public void Connect(string configuration = "", TextWriter? log = null) - { - // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract - var config = ConfigurationOptions.Parse(configuration ?? ""); - Connect(config, log); - } - - public void Connect(ConfigurationOptions options, TextWriter? log = null) + internal void Connect(RespConfiguration options, ReadOnlySpan endpoints, TextWriter? log = null) // use sync over async; reduce code-duplication, and sync wouldn't add anything - => ConnectAsync(options, log).Wait(Lifetime); - - public Task ConnectAsync(string configuration = "", TextWriter? log = null) - { - // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract - if (string.IsNullOrWhiteSpace(configuration)) configuration = "."; // localhost by default - var config = ConfigurationOptions.Parse(configuration ?? ""); - return ConnectAsync(config, log); - } + => ConnectAsync(options, endpoints, log).Wait(Lifetime); - public async Task ConnectAsync(ConfigurationOptions options, TextWriter? log = null) + internal Task ConnectAsync(RespConfiguration options, ReadOnlySpan endpoints, TextWriter? log = null) { - OnConnect(options); + OnConnect(options, endpoints); var snapshot = _nodes; log.LogLocked($"Connecting to {snapshot.Length} nodes..."); Task[] pending = new Task[snapshot.Length]; @@ -114,7 +99,11 @@ public async Task ConnectAsync(ConfigurationOptions options, TextWriter? log = n { pending[i] = snapshot[i].ConnectAsync(log); } + return ConnectAsyncAwaited(pending, log, snapshot.Length); + } + private async Task ConnectAsyncAwaited(Task[] pending, TextWriter? log, int nodeCount) + { await Task.WhenAll(pending).ConfigureAwait(false); int success = 0; foreach (var task in pending) @@ -126,9 +115,8 @@ public async Task ConnectAsync(ConfigurationOptions options, TextWriter? log = n // configure our primary connection OnNodesChanged(); - log.LogLocked($"Connected to {success} of {snapshot.Length} nodes."); + log.LogLocked($"Connected to {success} of {nodeCount} nodes."); } - */ public void Dispose() { @@ -228,4 +216,18 @@ private ref readonly RespContext BuildRouted(Node[] nodes) ArrayPool.Shared.Return(oversized); return ref conn.Context; } + + internal Node GetNode(string host, int port) + { + foreach (var node in _nodes) + { + if (node.EndPoint == host && node.Port == port) return node; + } + + throw new KeyNotFoundException($"No node found for {host}:{port}"); + } + + internal Node GetNode(string hostAndPort) => ConnectionFactory.TryParse(hostAndPort, out var host, out var port) + ? GetNode(host, port) + : throw new ArgumentException($"Could not parse host and port from '{hostAndPort}'", nameof(hostAndPort)); } diff --git a/src/RESPite/Connections/RespConnectionPool.cs b/src/RESPite/Connections/RespConnectionPool.cs index 47364cfeb..a1e4bd191 100644 --- a/src/RESPite/Connections/RespConnectionPool.cs +++ b/src/RESPite/Connections/RespConnectionPool.cs @@ -30,7 +30,9 @@ private void OnConnectionError(object? sender, RespConnection.RespConnectionErro private readonly EventHandler _onConnectionError; - public RespConnectionPool() : this(RespContext.Null, "127.0.0.1", 6379) { } + public RespConnectionPool(int count = DefaultCount) : this(RespContext.Null, "127.0.0.1", 6379, count) + { + } public RespConnectionPool( in RespContext template, diff --git a/tests/RESPite.Tests/ConnectionFixture.cs b/tests/RESPite.Tests/ConnectionFixture.cs index 4eb910431..fbaa8c5f4 100644 --- a/tests/RESPite.Tests/ConnectionFixture.cs +++ b/tests/RESPite.Tests/ConnectionFixture.cs @@ -9,7 +9,7 @@ namespace RESPite.Tests; public class ConnectionFixture : IDisposable { - private readonly RespConnectionPool _pool = new(new IPEndPoint(IPAddress.Loopback, 6379)); + private readonly RespConnectionPool _pool = new(); public void Dispose() => _pool.Dispose(); diff --git a/tests/RESPite.Tests/RespMultiplexerTests.cs b/tests/RESPite.Tests/RespMultiplexerTests.cs index 2770073ad..f616366d2 100644 --- a/tests/RESPite.Tests/RespMultiplexerTests.cs +++ b/tests/RESPite.Tests/RespMultiplexerTests.cs @@ -13,18 +13,17 @@ public class RespMultiplexerTests(ITestOutputHelper log) public async Task CanConnect() { await using var muxer = new RespMultiplexer(); - await muxer.ConnectAsync(log: logWriter); + await muxer.ConnectAsync("localhost:6379", log: logWriter); Assert.True(muxer.IsConnected); var server = muxer.GetServer(muxer.GetEndPoints().Single()); - Assert.IsType(server); // we expect this to *not* use routing + Assert.IsType(server); // we expect this to *not* use routing server.Ping(); await server.PingAsync(); var db = muxer.GetDatabase(); var proxied = Assert.IsType(db); // since this is a single-node instance, we expect the proxied database to use the interactive connection - Assert.Equal(RespContextProxyKind.ConnectionInteractive, proxied.RespContextProxyKind); db.Ping(); await db.PingAsync(); } From 999fb84e45a28edbb7c521d0eaf3a6809bf6ae1f Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 19 Sep 2025 15:10:17 +0100 Subject: [PATCH 067/108] hset --- .../RespContextDatabase.Connection.cs | 6 +- .../RespContextDatabase.Hash.cs | 44 +++++++--- .../RespContextDatabase.String.cs | 24 +++++- .../RespContextDatabase.cs | 6 ++ src/RESPite/RespContextExtensions.cs | 18 ++++ src/RESPite/RespFormatters.cs | 22 +++++ src/StackExchange.Redis/RedisBase.cs | 85 +++++++++++-------- src/StackExchange.Redis/RedisDatabase.cs | 30 +++---- 8 files changed, 170 insertions(+), 65 deletions(-) diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs index 0eb13978e..abacfa80d 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs @@ -10,11 +10,13 @@ internal partial class RespContextDatabase public bool IsConnected(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + private static readonly byte[] PingRaw = "*1\r\n$4\r\nping\r\n"u8.ToArray(); + public Task PingAsync(CommandFlags flags = CommandFlags.None) => - Context(flags).Send("ping"u8, DateTime.UtcNow, PingParser.Default).AsTask(); + Context(flags).Send("ping"u8, DateTime.UtcNow, PingParser.Default, PingRaw).AsTask(); public TimeSpan Ping(CommandFlags flags = CommandFlags.None) => - Context(flags).Send("ping"u8, DateTime.UtcNow, PingParser.Default).Wait(SyncTimeout); + Context(flags).Send("ping"u8, DateTime.UtcNow, PingParser.Default, PingRaw).Wait(SyncTimeout); private sealed class PingParser : IRespParser { diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs index 7c68f470a..8d590b13c 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs @@ -1,4 +1,5 @@ -using StackExchange.Redis; +using RESPite.Messages; +using StackExchange.Redis; namespace RESPite.StackExchange.Redis; @@ -19,9 +20,6 @@ public Task HashDecrementAsync( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - public Task HashDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -237,8 +235,14 @@ public Task HashSetAsync( RedisValue hashField, RedisValue value, When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + { + when.AlwaysOrNotExists(); + if (value.IsNull) return HashDeleteAsync(key, hashField, flags); + return when == When.Always + ? HashSetCoreAsync(key, hashField, value, flags) + : HashSetNXCoreAsync(key, hashField, value, flags); + } public Task HashStringLengthAsync( RedisKey key, @@ -264,8 +268,8 @@ public double HashDecrement( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public bool HashDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("hdel")] + public partial bool HashDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); public long HashDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -476,8 +480,28 @@ public bool HashSet( RedisValue hashField, RedisValue value, When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + { + when.AlwaysOrNotExists(); + if (value.IsNull) return HashDelete(key, hashField, flags); + return when == When.Always + ? HashSetCore(key, hashField, value, flags) + : HashSetNXCore(key, hashField, value, flags); + } + + [RespCommand("hset")] + private partial bool HashSetCore( + RedisKey key, + RedisValue hashField, + RedisValue value, + CommandFlags flags = CommandFlags.None); + + [RespCommand("hsetnx")] + private partial bool HashSetNXCore( + RedisKey key, + RedisValue hashField, + RedisValue value, + CommandFlags flags = CommandFlags.None); public long HashStringLength(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.String.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.String.cs index 10d3aea9b..655ee7045 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.String.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.String.cs @@ -269,8 +269,30 @@ public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When whe public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => StringSet(key, value, expiry, false, when, flags); + public bool StringSet( + RedisKey key, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) + => value.IsNull + ? KeyDelete(key, flags) + : StringSetCore(key, value, expiry.NullIfMaxValue(), keepTtl, when, flags); + + public Task StringSetAsync( + RedisKey key, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) + => value.IsNull + ? KeyDeleteAsync(key, flags) + : StringSetCoreAsync(key, value, expiry.NullIfMaxValue(), keepTtl, when, flags); + [RespCommand("set", Formatter = StringSetFormatter.Formatter)] - public partial bool StringSet( + private partial bool StringSetCore( RedisKey key, RedisValue value, TimeSpan? expiry = null, diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.cs index 9de355428..302ffaa56 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.cs @@ -59,3 +59,9 @@ private RespContext Context(CommandFlags flags) public void WaitAll(params Task[] tasks) => _muxer.WaitAll(tasks); } + +internal static class MiscExtensions +{ + internal static TimeSpan? NullIfMaxValue(this TimeSpan? value) + => value == TimeSpan.MaxValue ? null : value; +} diff --git a/src/RESPite/RespContextExtensions.cs b/src/RESPite/RespContextExtensions.cs index 8e1eb4e75..6272a39a1 100644 --- a/src/RESPite/RespContextExtensions.cs +++ b/src/RESPite/RespContextExtensions.cs @@ -135,6 +135,24 @@ public static RespOperation Send( return op; } + /// + /// Creates an operation and synchronously writes it to the connection. + /// + /// The type of state data required by the parser. + /// The type of the response data being received. + /// The raw payload is the entire RESP fragment, only used if there is not a command-map. + internal static RespOperation Send( + this in RespContext context, + ReadOnlySpan command, + in TState state, + IRespParser parser, + byte[] rawPayload) + { + var op = CreateOperation(context, command, rawPayload, RespFormatters.Raw, in state, parser); + context.Connection.Write(op); + return op; + } + /// /// Creates an operation and asynchronously writes it to the connection, awaiting the completion of the underlying write. /// diff --git a/src/RESPite/RespFormatters.cs b/src/RESPite/RespFormatters.cs index 3ac332fda..979891cfc 100644 --- a/src/RESPite/RespFormatters.cs +++ b/src/RESPite/RespFormatters.cs @@ -13,6 +13,7 @@ public static class RespFormatters public static IRespFormatter Int64 => Value.Formatter.Default; public static IRespFormatter Single => Value.Formatter.Default; public static IRespFormatter Double => Value.Formatter.Default; + internal static IRespFormatter Raw => RawFormatter.Instance; public static class Key { @@ -136,4 +137,25 @@ public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in writer.WriteCommand(command, 0); } } + + private sealed class RawFormatter : IRespFormatter + { + private RawFormatter() { } + public static readonly RawFormatter Instance = new(); + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in byte[] value) + { + if (writer.CommandMap is null) + { + writer.WriteRaw(value); + } + else + { + writer.WriteCommand(command, 0); + } + } + } } diff --git a/src/StackExchange.Redis/RedisBase.cs b/src/StackExchange.Redis/RedisBase.cs index 095835efd..8286a8837 100644 --- a/src/StackExchange.Redis/RedisBase.cs +++ b/src/StackExchange.Redis/RedisBase.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; +using StackExchange.Redis; namespace StackExchange.Redis { @@ -69,43 +70,6 @@ internal virtual RedisFeatures GetFeatures(in RedisKey key, CommandFlags flags, return new RedisFeatures(version); } - protected static void WhenAlwaysOrExists(When when) - { - switch (when) - { - case When.Always: - case When.Exists: - break; - default: - throw new ArgumentException(when + " is not valid in this context; the permitted values are: Always, Exists"); - } - } - - protected static void WhenAlwaysOrExistsOrNotExists(When when) - { - switch (when) - { - case When.Always: - case When.Exists: - case When.NotExists: - break; - default: - throw new ArgumentException(when + " is not valid in this context; the permitted values are: Always, Exists, NotExists"); - } - } - - protected static void WhenAlwaysOrNotExists(When when) - { - switch (when) - { - case When.Always: - case When.NotExists: - break; - default: - throw new ArgumentException(when + " is not valid in this context; the permitted values are: Always, NotExists"); - } - } - private ResultProcessor.TimingProcessor.TimerMessage GetTimerMessage(CommandFlags flags) { // do the best we can with available commands @@ -137,3 +101,50 @@ internal static bool IsNil(in RedisValue pattern) } } } + +internal static class WhenExtensions +{ + internal static void AlwaysOrExists(this When when) + { + switch (when) + { + case When.Always: + case When.Exists: + break; + default: + Throw(when); + break; + } + static void Throw(When when) => throw new ArgumentException(when + " is not valid in this context; the permitted values are: Always, Exists"); + } + + internal static void AlwaysOrExistsOrNotExists(this When when) + { + switch (when) + { + case When.Always: + case When.Exists: + case When.NotExists: + break; + default: + Throw(when); + break; + } + static void Throw(When when) + => throw new ArgumentException(when + " is not valid in this context; the permitted values are: Always, Exists, NotExists"); + } + + internal static void AlwaysOrNotExists(this When when) + { + switch (when) + { + case When.Always: + case When.NotExists: + break; + default: + Throw(when); + break; + } + static void Throw(When when) => throw new ArgumentException(when + " is not valid in this context; the permitted values are: Always, NotExists"); + } +} diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index bf69f25f3..8e4a20bcd 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -938,7 +938,7 @@ private CursorEnumerable HashScanNoValuesAsync(RedisKey key, RedisVa public bool HashSet(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) { - WhenAlwaysOrNotExists(when); + when.AlwaysOrNotExists(); var msg = value.IsNull ? Message.Create(Database, flags, RedisCommand.HDEL, key, hashField) : Message.Create(Database, flags, when == When.Always ? RedisCommand.HSET : RedisCommand.HSETNX, key, hashField, value); @@ -960,7 +960,7 @@ public long HashStringLength(RedisKey key, RedisValue hashField, CommandFlags fl public Task HashSetAsync(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) { - WhenAlwaysOrNotExists(when); + when.AlwaysOrNotExists(); var msg = value.IsNull ? Message.Create(Database, flags, RedisCommand.HDEL, key, hashField) : Message.Create(Database, flags, when == When.Always ? RedisCommand.HSET : RedisCommand.HSETNX, key, hashField, value); @@ -1398,14 +1398,14 @@ public Task KeyRandomAsync(CommandFlags flags = CommandFlags.None) public bool KeyRename(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None) { - WhenAlwaysOrNotExists(when); + when.AlwaysOrNotExists(); var msg = Message.Create(Database, flags, when == When.Always ? RedisCommand.RENAME : RedisCommand.RENAMENX, key, newKey); return ExecuteSync(msg, ResultProcessor.Boolean); } public Task KeyRenameAsync(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None) { - WhenAlwaysOrNotExists(when); + when.AlwaysOrNotExists(); var msg = Message.Create(Database, flags, when == When.Always ? RedisCommand.RENAME : RedisCommand.RENAMENX, key, newKey); return ExecuteAsync(msg, ResultProcessor.Boolean); } @@ -1558,14 +1558,14 @@ public Task ListPositionsAsync(RedisKey key, RedisValue element, long co public long ListLeftPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) { - WhenAlwaysOrExists(when); + when.AlwaysOrExists(); var msg = Message.Create(Database, flags, when == When.Always ? RedisCommand.LPUSH : RedisCommand.LPUSHX, key, value); return ExecuteSync(msg, ResultProcessor.Int64); } public long ListLeftPush(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) { - WhenAlwaysOrExists(when); + when.AlwaysOrExists(); if (values == null) throw new ArgumentNullException(nameof(values)); var command = when == When.Always ? RedisCommand.LPUSH : RedisCommand.LPUSHX; var msg = values.Length == 0 ? Message.Create(Database, flags, RedisCommand.LLEN, key) : Message.Create(Database, flags, command, key, values); @@ -1581,14 +1581,14 @@ public long ListLeftPush(RedisKey key, RedisValue[] values, CommandFlags flags = public Task ListLeftPushAsync(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) { - WhenAlwaysOrExists(when); + when.AlwaysOrExists(); var msg = Message.Create(Database, flags, when == When.Always ? RedisCommand.LPUSH : RedisCommand.LPUSHX, key, value); return ExecuteAsync(msg, ResultProcessor.Int64); } public Task ListLeftPushAsync(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) { - WhenAlwaysOrExists(when); + when.AlwaysOrExists(); if (values == null) throw new ArgumentNullException(nameof(values)); var command = when == When.Always ? RedisCommand.LPUSH : RedisCommand.LPUSHX; var msg = values.Length == 0 ? Message.Create(Database, flags, RedisCommand.LLEN, key) : Message.Create(Database, flags, command, key, values); @@ -1700,14 +1700,14 @@ public Task ListRightPopLeftPushAsync(RedisKey source, RedisKey dest public long ListRightPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) { - WhenAlwaysOrExists(when); + when.AlwaysOrExists(); var msg = Message.Create(Database, flags, when == When.Always ? RedisCommand.RPUSH : RedisCommand.RPUSHX, key, value); return ExecuteSync(msg, ResultProcessor.Int64); } public long ListRightPush(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) { - WhenAlwaysOrExists(when); + when.AlwaysOrExists(); if (values == null) throw new ArgumentNullException(nameof(values)); var command = when == When.Always ? RedisCommand.RPUSH : RedisCommand.RPUSHX; var msg = values.Length == 0 ? Message.Create(Database, flags, RedisCommand.LLEN, key) : Message.Create(Database, flags, command, key, values); @@ -1723,14 +1723,14 @@ public long ListRightPush(RedisKey key, RedisValue[] values, CommandFlags flags public Task ListRightPushAsync(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) { - WhenAlwaysOrExists(when); + when.AlwaysOrExists(); var msg = Message.Create(Database, flags, when == When.Always ? RedisCommand.RPUSH : RedisCommand.RPUSHX, key, value); return ExecuteAsync(msg, ResultProcessor.Int64); } public Task ListRightPushAsync(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) { - WhenAlwaysOrExists(when); + when.AlwaysOrExists(); if (values == null) throw new ArgumentNullException(nameof(values)); var command = when == When.Always ? RedisCommand.RPUSH : RedisCommand.RPUSHX; var msg = values.Length == 0 ? Message.Create(Database, flags, RedisCommand.LLEN, key) : Message.Create(Database, flags, command, key, values); @@ -5025,7 +5025,7 @@ private Message GetStringGetWithExpiryMessage(RedisKey key, CommandFlags flags, case 0: return null; case 1: return GetStringSetMessage(values[0].Key, values[0].Value, null, false, when, flags); default: - WhenAlwaysOrNotExists(when); + when.AlwaysOrNotExists(); int slot = ServerSelectionStrategy.NoSlot, offset = 0; var args = new RedisValue[values.Length * 2]; var serverSelectionStrategy = multiplexer.ServerSelectionStrategy; @@ -5047,7 +5047,7 @@ private Message GetStringSetMessage( When when = When.Always, CommandFlags flags = CommandFlags.None) { - WhenAlwaysOrExistsOrNotExists(when); + when.AlwaysOrExists(); if (value.IsNull) return Message.Create(Database, flags, RedisCommand.DEL, key); if (expiry == null || expiry.Value == TimeSpan.MaxValue) @@ -5096,7 +5096,7 @@ private Message GetStringSetAndGetMessage( When when = When.Always, CommandFlags flags = CommandFlags.None) { - WhenAlwaysOrExistsOrNotExists(when); + when.AlwaysOrExists(); if (value.IsNull) return Message.Create(Database, flags, RedisCommand.GETDEL, key); if (expiry == null || expiry.Value == TimeSpan.MaxValue) From 5d1bde0e22c80824b887bb2ba66057d477f93b1f Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 19 Sep 2025 15:34:18 +0100 Subject: [PATCH 068/108] zadd --- .../RespContextDatabase.SortedSet.cs | 96 +++++++++++++------ .../RespContextDatabase.String.cs | 2 +- 2 files changed, 68 insertions(+), 30 deletions(-) diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs index 39eee1088..3c8ddf6b5 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs @@ -1,32 +1,89 @@ -using StackExchange.Redis; +using RESPite.Messages; +using StackExchange.Redis; namespace RESPite.StackExchange.Redis; internal partial class RespContextDatabase { // Async SortedSet methods - public Task SortedSetAddAsync( + [RespCommand("zadd")] + public partial bool SortedSetAdd( RedisKey key, RedisValue member, double score, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags); - public Task SortedSetAddAsync( + public bool SortedSetAdd( RedisKey key, RedisValue member, double score, When when, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags) => when == When.Always + ? SortedSetAdd(key, member, score, flags) // simple mode + : SortedSetAdd(key, member, score, SortedSetWhenExtensions.Parse(when), flags); public Task SortedSetAddAsync( RedisKey key, RedisValue member, double score, - SortedSetWhen when = SortedSetWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + When when, + CommandFlags flags) => when == When.Always + ? SortedSetAddAsync(key, member, score, flags) // simple mode + : SortedSetAddAsync(key, member, score, SortedSetWhenExtensions.Parse(when), flags); + + [RespCommand("zadd", Formatter = SortedSetAddFormatter.Formatter)] + public partial bool SortedSetAdd( + RedisKey key, + RedisValue member, + double score, + SortedSetWhen when, + CommandFlags flags); + + private sealed class + SortedSetAddFormatter : IRespFormatter<(RedisKey Key, RedisValue Member, double Score, SortedSetWhen When)> + { + public const string Formatter = $"{nameof(SortedSetAddFormatter)}.{nameof(Instance)}"; + public static readonly SortedSetAddFormatter Instance = new(); + private SortedSetAddFormatter() { } + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, RedisValue Member, double Score, SortedSetWhen When) request) + { + static int Throw(SortedSetWhen when) => throw new ArgumentOutOfRangeException( + paramName: nameof(when), + message: $"Invalid {nameof(SortedSetWhen)} value for ZADD: {when}"); + + // ZADD key [NX | XX] [GT | LT] score member + var argCount = 3 + request.When switch + { + SortedSetWhen.Always => 0, + SortedSetWhen.Exists or SortedSetWhen.NotExists => 1, + SortedSetWhen.GreaterThan or SortedSetWhen.LessThan => 1, + SortedSetWhen.GreaterThan | SortedSetWhen.Exists => 2, + SortedSetWhen.GreaterThan | SortedSetWhen.NotExists => 2, + SortedSetWhen.LessThan | SortedSetWhen.Exists => 2, + SortedSetWhen.LessThan | SortedSetWhen.NotExists => 2, + _ => Throw(request.When), + }; + + writer.WriteCommand(command, argCount); + writer.Write(request.Key); + switch (request.When & (SortedSetWhen.Exists | SortedSetWhen.NotExists)) + { + case SortedSetWhen.Exists: + writer.WriteBulkString("XX"u8); + break; + case SortedSetWhen.NotExists: + writer.WriteBulkString("NX"u8); + break; + } + + writer.WriteBulkString(request.Score); + writer.Write(request.Member); + } + } public Task SortedSetAddAsync( RedisKey key, @@ -298,25 +355,6 @@ public Task SortedSetUpdateAsync( throw new NotImplementedException(); // Synchronous SortedSet methods - public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool SortedSetAdd( - RedisKey key, - RedisValue member, - double score, - When when, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool SortedSetAdd( - RedisKey key, - RedisValue member, - double score, - SortedSetWhen when = SortedSetWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.String.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.String.cs index 655ee7045..cf1d5f182 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.String.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.String.cs @@ -305,7 +305,7 @@ private sealed class StringSetFormatter : IRespFormatter<(RedisKey Key, RedisVal When When)> { public const string Formatter = $"{nameof(StringSetFormatter)}.{nameof(Instance)}"; - public static readonly StringSetFormatter Instance = new StringSetFormatter(); + public static readonly StringSetFormatter Instance = new(); private StringSetFormatter() { } public void Format( From 2259a34ef18d7b606f9eea61dba6bd601d0bc04d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 19 Sep 2025 16:42:43 +0100 Subject: [PATCH 069/108] MSET[NX], LRANGE, ZPOP{MIN|MAX} --- .../RespCommandGenerator.cs | 1 + src/RESPite.Benchmark/OldCoreBenchmarkBase.cs | 8 ++- .../RespContextDatabase.List.cs | 17 ++--- .../RespContextDatabase.SortedSet.cs | 32 ++++++++- .../RespContextDatabase.String.cs | 65 ++++++++++++++++--- .../RespParsers.cs | 41 +++++++----- 6 files changed, 124 insertions(+), 40 deletions(-) diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index c3af3cfa2..902d330c1 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -1022,6 +1022,7 @@ private static int DataParameterCount( "global::RESPite.RespParsers.ResponseSummary" => RespParsersPrefix + "ResponseSummary.Parser", "global::StackExchange.Redis.RedisKey" => "global::RESPite.StackExchange.Redis.RespParsers.RedisKey", "global::StackExchange.Redis.RedisValue" => "global::RESPite.StackExchange.Redis.RespParsers.RedisValue", + "global::StackExchange.Redis.RedisValue[]" => "global::RESPite.StackExchange.Redis.RespParsers.RedisValueArray", "global::StackExchange.Redis.Lease" => "global::RESPite.StackExchange.Redis.RespParsers.BytesLease", _ => null, }; diff --git a/src/RESPite.Benchmark/OldCoreBenchmarkBase.cs b/src/RESPite.Benchmark/OldCoreBenchmarkBase.cs index 0cbf0bed6..cf37734fc 100644 --- a/src/RESPite.Benchmark/OldCoreBenchmarkBase.cs +++ b/src/RESPite.Benchmark/OldCoreBenchmarkBase.cs @@ -225,7 +225,13 @@ private ValueTask ZAdd(IDatabaseAsync client) => client.SortedSetAddAsync(SortedSetKey, "element:__rand_int__", 0).AsValueTask(); [DisplayName("ZPOPMIN")] - private ValueTask ZPopMin(IDatabaseAsync client) => CountAsync(client.SortedSetPopAsync(SortedSetKey, 1)); + private ValueTask ZPopMin(IDatabaseAsync client) => HasSortedSetElement(client.SortedSetPopAsync(SortedSetKey)); + + private async ValueTask HasSortedSetElement(Task pending) + { + var result = await pending.ConfigureAwait(false); + return result.HasValue ? 1 : 0; + } private async ValueTask ZPopMinInit(IDatabaseAsync client) { diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.List.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.List.cs index 0ef20774c..b261080e5 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.List.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.List.cs @@ -77,13 +77,6 @@ public Task ListMoveAsync( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task ListRangeAsync( - RedisKey key, - long start = 0, - long stop = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - public Task ListRemoveAsync( RedisKey key, RedisValue value, @@ -222,12 +215,12 @@ public RedisValue ListMove( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue[] ListRange( + [RespCommand("lrange")] + public partial RedisValue[] ListRange( RedisKey key, - long start = 0, - long stop = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + long start, + long stop, + CommandFlags flags = CommandFlags.None); public long ListRemove( RedisKey key, diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs index 3c8ddf6b5..9e3de9135 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs @@ -323,7 +323,9 @@ public IAsyncEnumerable SortedSetScanAsync( RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + order == Order.Ascending + ? SortedSetPopMinCoreAsync(key, flags) + : SortedSetPopMaxCoreAsync(key, flags); public Task SortedSetPopAsync( RedisKey key, @@ -332,6 +334,30 @@ public Task SortedSetPopAsync( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + [RespCommand("zpopmin", Parser = SortedSetEntryParser.Parser)] + private partial SortedSetEntry? SortedSetPopMinCore(RedisKey key, CommandFlags flags); + + [RespCommand("zpopmax", Parser = SortedSetEntryParser.Parser)] + private partial SortedSetEntry? SortedSetPopMaxCore(RedisKey key, CommandFlags flags); + + private sealed class SortedSetEntryParser : IRespParser + { + public const string Parser = $"{nameof(SortedSetEntryParser)}.{nameof(Instance)}"; + public static readonly SortedSetEntryParser Instance = new(); + + public SortedSetEntry? Parse(ref RespReader reader) + { + if (reader.IsNull) return null; + reader.DemandAggregate(); + if (reader.AggregateLength() < 2) return null; + reader.MoveNext(); + var member = RespParsers.ReadRedisValue(ref reader); + reader.MoveNext(); + var score = reader.ReadDouble(); + return new SortedSetEntry(member, score); + } + } + public Task SortedSetPopAsync( RedisKey[] keys, long count, @@ -582,7 +608,9 @@ public IEnumerable SortedSetScan( RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + order == Order.Ascending + ? SortedSetPopMinCore(key, flags) + : SortedSetPopMaxCore(key, flags); public SortedSetEntry[] SortedSetPop( RedisKey key, diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.String.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.String.cs index cf1d5f182..73c96649b 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.String.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.String.cs @@ -118,12 +118,6 @@ public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expir public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => StringSetAsync(key, value, expiry, false, when, flags); - public Task StringSetAsync( - KeyValuePair[] values, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - public Task StringSetAndGetAsync( RedisKey key, RedisValue value, @@ -302,7 +296,7 @@ private partial bool StringSetCore( private sealed class StringSetFormatter : IRespFormatter<(RedisKey Key, RedisValue Value, TimeSpan? Expiry, bool KeepTtl, - When When)> + When When)>, IRespFormatter[]> { public const string Formatter = $"{nameof(StringSetFormatter)}.{nameof(Instance)}"; public static readonly StringSetFormatter Instance = new(); @@ -354,13 +348,66 @@ public void Format( writer.WriteBulkString("KEEPTTL"u8); } } + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in KeyValuePair[] request) + { + writer.WriteCommand(command, 2 * request.Length); + foreach (var pair in request) + { + writer.Write(pair.Key); + writer.Write(pair.Value); + } + } } public bool StringSet( KeyValuePair[] values, When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + { + switch (values.Length) + { + case 0: return false; + case 1: return StringSet(values[0].Key, values[0].Value, null, false, when, flags); + default: + when.AlwaysOrNotExists(); + return when == When.Always + ? StringMSetCore(values, flags) + : StringMSetNXCore(values, flags); + } + } + + public Task StringSetAsync( + KeyValuePair[] values, + When when = When.Always, + CommandFlags flags = CommandFlags.None) + { + switch (values.Length) + { + case 0: return FalseTask; + case 1: return StringSetAsync(values[0].Key, values[0].Value, null, false, when, flags); + default: + when.AlwaysOrNotExists(); + return when == When.Always + ? StringMSetCoreAsync(values, flags) + : StringMSetNXCoreAsync(values, flags); + } + } + + private static readonly Task FalseTask = Task.FromResult(false); + + [RespCommand("mset", Formatter = StringSetFormatter.Formatter)] + private partial bool StringMSetCore( + KeyValuePair[] values, + CommandFlags flags = CommandFlags.None); + + [RespCommand("msetnx", Formatter = StringSetFormatter.Formatter)] + private partial bool StringMSetNXCore( + KeyValuePair[] values, + CommandFlags flags = CommandFlags.None); public RedisValue StringSetAndGet( RedisKey key, diff --git a/src/RESPite.StackExchange.Redis/RespParsers.cs b/src/RESPite.StackExchange.Redis/RespParsers.cs index 1ffed4dec..fe17301a0 100644 --- a/src/RESPite.StackExchange.Redis/RespParsers.cs +++ b/src/RESPite.StackExchange.Redis/RespParsers.cs @@ -6,33 +6,40 @@ namespace RESPite.StackExchange.Redis; public static class RespParsers { public static IRespParser RedisValue => DefaultParser.Instance; + public static IRespParser RedisValueArray => DefaultParser.Instance; public static IRespParser RedisKey => DefaultParser.Instance; public static IRespParser> BytesLease => DefaultParser.Instance; + public static RedisValue ReadRedisValue(ref RespReader reader) + { + reader.DemandScalar(); + if (reader.IsNull) return global::StackExchange.Redis.RedisValue.Null; + if (reader.TryReadInt64(out var i64)) return i64; + if (reader.TryReadDouble(out var f64)) return f64; + + if (reader.UnsafeTryReadShortAscii(out var s)) return s; + return reader.ReadByteArray(); + } + + public static RedisKey ReadRedisKey(ref RespReader reader) + { + reader.DemandScalar(); + if (reader.IsNull) return global::StackExchange.Redis.RedisKey.Null; + if (reader.UnsafeTryReadShortAscii(out var s)) return s; + return reader.ReadByteArray(); + } + private sealed class DefaultParser : IRespParser, IRespParser, - IRespParser> + IRespParser>, IRespParser { private DefaultParser() { } public static readonly DefaultParser Instance = new(); RedisValue IRespParser.Parse(ref RespReader reader) - { - reader.DemandScalar(); - if (reader.IsNull) return global::StackExchange.Redis.RedisValue.Null; - if (reader.TryReadInt64(out var i64)) return i64; - if (reader.TryReadDouble(out var f64)) return f64; - - if (reader.UnsafeTryReadShortAscii(out var s)) return s; - return reader.ReadByteArray(); - } + => ReadRedisValue(ref reader); RedisKey IRespParser.Parse(ref RespReader reader) - { - reader.DemandScalar(); - if (reader.IsNull) return global::StackExchange.Redis.RedisKey.Null; - if (reader.UnsafeTryReadShortAscii(out var s)) return s; - return reader.ReadByteArray(); - } + => ReadRedisKey(ref reader); Lease IRespParser>.Parse(ref RespReader reader) { @@ -43,5 +50,7 @@ Lease IRespParser>.Parse(ref RespReader reader) reader.CopyTo(lease.Span); return lease; } + + public RedisValue[] Parse(ref RespReader reader) => reader.ReadArray(ReadRedisValue)!; } } From e3bb6d9b851441c3ecb92a99c15897c9db7f82c9 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 19 Sep 2025 17:05:22 +0100 Subject: [PATCH 070/108] xadd incomplete --- .../RespContextDatabase.Stream.cs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Stream.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Stream.cs index f00d5a296..e4522a257 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Stream.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Stream.cs @@ -43,7 +43,16 @@ public Task StreamAddAsync( int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + (messageId is null & maxLength is null & !useApproximateMaxLength) + ? StreamAddSimpleCoreAsync(key, streamField, streamValue, flags) + : throw new NotImplementedException(); + + [RespCommand("xadd")] + private partial RedisValue StreamAddSimpleCore( + RedisKey key, + RedisValue streamField, + RedisValue streamValue, + CommandFlags flags = CommandFlags.None); public Task StreamAddAsync( RedisKey key, @@ -64,7 +73,10 @@ public Task StreamAddAsync( long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + (messageId is null & maxLength is null & !useApproximateMaxLength + & limit is null & trimMode == StreamTrimMode.KeepReferences) + ? StreamAddSimpleCoreAsync(key, streamField, streamValue, flags) + : throw new NotImplementedException(); public Task StreamAddAsync( RedisKey key, @@ -327,7 +339,9 @@ public RedisValue StreamAdd( int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + (messageId is null & maxLength is null & !useApproximateMaxLength) + ? StreamAddSimpleCore(key, streamField, streamValue, flags) + : throw new NotImplementedException(); public RedisValue StreamAdd( RedisKey key, @@ -348,7 +362,10 @@ public RedisValue StreamAdd( long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + (messageId is null & maxLength is null & !useApproximateMaxLength + & limit is null & trimMode == StreamTrimMode.KeepReferences) + ? StreamAddSimpleCore(key, streamField, streamValue, flags) + : throw new NotImplementedException(); public RedisValue StreamAdd( RedisKey key, From 7660a89c2879ef47584f1b762b6e344262aa8b4e Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 22 Sep 2025 10:04:52 +0100 Subject: [PATCH 071/108] fix xadd --- .../RespCommandGenerator.cs | 54 ++++++++++++++----- .../RespContextDatabase.Stream.cs | 1 + src/RESPite/Messages/RespWriter.cs | 4 ++ 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index 902d330c1..c39d23774 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -525,7 +525,7 @@ private void Generate( // find the unique param types, so we can build helpers Dictionary, (string Name, - bool Shared)> + int ShareCount, string Command)> formatters = new(FormatterComparer.Default); @@ -551,11 +551,11 @@ private void Generate( var key = method.Parameters; if (!formatters.TryGetValue(key, out var existing)) { - formatters.Add(key, ($"__RespFormatter_{formatters.Count}", false)); + formatters.Add(key, ($"__RespFormatter_{formatters.Count}", 1, method.Command)); } - else if (!existing.Shared) + else { - formatters[key] = (existing.Name, true); // mark shared + formatters[key] = (existing.Name, existing.ShareCount + 1, ""); // incr share count } } @@ -630,7 +630,7 @@ private void Generate( if (formatter is null && formatters.TryGetValue(method.Parameters, out var tmp)) { formatter = $"{tmp.Name}.Default"; - isSharedFormatter = tmp.Shared; + isSharedFormatter = tmp.ShareCount > 1; } // perform string escaping on the generated value (this includes the quotes, note) @@ -771,9 +771,18 @@ void WriteMethod(bool asAsync) { var parameters = tuple.Key; var name = tuple.Value.Name; - var names = tuple.Value.Shared ? TupleMode.SyntheticNames : TupleMode.NamedTuple; + var names = tuple.Value.ShareCount > 1 ? TupleMode.SyntheticNames : TupleMode.NamedTuple; NewLine(); + if (tuple.Value.ShareCount > 1) + { + NewLine().Append("// shared by ").Append(tuple.Value.ShareCount).Append(" methods"); + } + else if (tuple.Value.Command is { Length: > 0 }) + { + NewLine().Append("// for command: ").Append(tuple.Value.Command); + } + sb = NewLine().Append("sealed file class ").Append(name) .Append(" : global::RESPite.Messages.IRespFormatter<"); WriteTuple(parameters, sb, names); @@ -790,9 +799,29 @@ void WriteMethod(bool asAsync) sb.Append(" request)"); NewLine().Append("{"); indent++; - var count = DataParameterCount(parameters, out int literalCount); - sb = NewLine().Append("writer.WriteCommand(command, ").Append(count + literalCount); - sb.Append(");"); + var argCount = DataParameterCount(parameters, out int literalCount); + if (tuple.Value.Command is { Length: > 0 } cmd + && Encoding.UTF8.GetByteCount(cmd) == cmd.Length) // check pure ASCII + { + // only used by one command; allow optimization + NewLine().Append("if(writer.CommandMap is null) // optimize single-command case for ").Append(cmd); + NewLine().Append("{"); + indent++; + string raw = $"*{argCount + literalCount + 1}\r\n${cmd.Length}\r\n{tuple.Value.Command}\r\n"; + sb = NewLine().Append("writer.WriteRaw(").Append(CodeLiteral(raw)).Append("u8);"); + indent--; + NewLine().Append("}"); + NewLine().Append("else"); + NewLine().Append("{"); + indent++; + NewLine().Append("writer.WriteCommand(command, ").Append(argCount + literalCount).Append(");"); + indent--; + NewLine().Append("}"); + } + else + { + NewLine().Append("writer.WriteCommand(command, ").Append(argCount + literalCount).Append(");"); + } void WritePrefix(ParameterTuple p) => WriteLiteral(p, false); void WriteSuffix(ParameterTuple p) => WriteLiteral(p, true); @@ -804,12 +833,13 @@ void WriteLiteral(ParameterTuple p, bool suffix) { if ((literal.Flags & LiteralFlags.Suffix) == match) { - sb = NewLine().Append("writer.WriteBulkString(").Append(CodeLiteral(literal.Token)).Append("u8);"); + sb = NewLine().Append("writer.WriteBulkString(").Append(CodeLiteral(literal.Token)) + .Append("u8);"); } } } - if (count == 1) + if (argCount == 1) { var p = FirstDataParameter(parameters); WritePrefix(p); @@ -861,7 +891,7 @@ void WriteLiteral(ParameterTuple p, bool suffix) } } - Debug.Assert(index == count, "wrote all parameters"); + Debug.Assert(index == argCount, "wrote all parameters"); } indent--; diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Stream.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Stream.cs index e4522a257..8fbdd8cea 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Stream.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Stream.cs @@ -50,6 +50,7 @@ public Task StreamAddAsync( [RespCommand("xadd")] private partial RedisValue StreamAddSimpleCore( RedisKey key, + [RespPrefix("*")] RedisValue streamField, RedisValue streamValue, CommandFlags flags = CommandFlags.None); diff --git a/src/RESPite/Messages/RespWriter.cs b/src/RESPite/Messages/RespWriter.cs index 17cc35cbd..4a10f3816 100644 --- a/src/RESPite/Messages/RespWriter.cs +++ b/src/RESPite/Messages/RespWriter.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Buffers.Text; +using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -178,6 +179,7 @@ private static void ThrowFixedBufferExceeded() => /// /// Write raw RESP data to the output; no validation will occur. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public void WriteRaw(scoped ReadOnlySpan buffer) { const int MAX_TO_DOUBLE_BUFFER = 128; @@ -569,6 +571,7 @@ public void WriteBulkString(string value) [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] // ReSharper disable once NotResolvedInText private static void ThrowNull() => + // ReSharper disable once NotResolvedInText throw new ArgumentNullException("value", "Null values cannot be sent from client to server"); internal void WriteBulkStringUnoptimized(string? value) @@ -685,6 +688,7 @@ private void WriteUtf8Slow(scoped ReadOnlySpan value, int remaining) enc.Convert(value, Tail, true, out charsUsed, out bytesUsed, out completed); Debug.Assert(charsUsed == 0 && completed); _index += bytesUsed; + // ReSharper disable once RedundantAssignment - it is in debug! remaining -= bytesUsed; } From 918e84dd50fec2fc9825200a9785552824cc5d62 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 22 Sep 2025 10:38:43 +0100 Subject: [PATCH 072/108] use WriteRaw for literals --- eng/StackExchange.Redis.Build/RespCommandGenerator.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index c39d23774..5dd51a7d2 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -804,11 +804,12 @@ void WriteMethod(bool asAsync) && Encoding.UTF8.GetByteCount(cmd) == cmd.Length) // check pure ASCII { // only used by one command; allow optimization - NewLine().Append("if(writer.CommandMap is null) // optimize single-command case for ").Append(cmd); + NewLine().Append("if(writer.CommandMap is null)"); NewLine().Append("{"); indent++; string raw = $"*{argCount + literalCount + 1}\r\n${cmd.Length}\r\n{tuple.Value.Command}\r\n"; - sb = NewLine().Append("writer.WriteRaw(").Append(CodeLiteral(raw)).Append("u8);"); + sb = NewLine().Append("writer.WriteRaw(").Append(CodeLiteral(raw)).Append("u8); // ") + .Append(cmd).Append(" with ").Append(argCount + literalCount).Append(" args"); indent--; NewLine().Append("}"); NewLine().Append("else"); @@ -833,8 +834,9 @@ void WriteLiteral(ParameterTuple p, bool suffix) { if ((literal.Flags & LiteralFlags.Suffix) == match) { - sb = NewLine().Append("writer.WriteBulkString(").Append(CodeLiteral(literal.Token)) - .Append("u8);"); + var len = Encoding.UTF8.GetByteCount(literal.Token); + var resp = $"${len}\r\n{literal.Token}\r\n"; + NewLine().Append("writer.WriteRaw(").Append(CodeLiteral(resp)).Append("u8); // ").Append(literal.Token); } } } From 45a8a215d89dab103c0ceb0f6a7567c15596057d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 22 Sep 2025 10:44:12 +0100 Subject: [PATCH 073/108] copyright stamp --- src/RESPite.Benchmark/RESPite.Benchmark.csproj | 1 + .../RESPite.StackExchange.Redis.csproj | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/RESPite.Benchmark/RESPite.Benchmark.csproj b/src/RESPite.Benchmark/RESPite.Benchmark.csproj index 42ba2c533..273e383d4 100644 --- a/src/RESPite.Benchmark/RESPite.Benchmark.csproj +++ b/src/RESPite.Benchmark/RESPite.Benchmark.csproj @@ -14,6 +14,7 @@ True false false + 2025 - $([System.DateTime]::Now.Year) Marc Gravell diff --git a/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj b/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj index c912996e5..bb4dea30b 100644 --- a/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj +++ b/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj @@ -6,9 +6,9 @@ enable enable $(NoWarn);CS1591 - 2025 - $([System.DateTime]::Now.Year) Marc Gravell readme.md false + 2025 - $([System.DateTime]::Now.Year) Marc Gravell From 78c037c4936661572efd7e1e2dde69dd4745a0f5 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 24 Sep 2025 12:07:35 +0100 Subject: [PATCH 074/108] Teach generator how to handle collections and optional parameters --- .../RespCommandGenerator.cs | 329 +++++++++++++++--- .../RespContextDatabase.Geo.cs | 10 +- 2 files changed, 273 insertions(+), 66 deletions(-) diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index 5dd51a7d2..7eb0b1d0a 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -54,7 +54,9 @@ private readonly record struct ParameterTuple( string Name, string Modifiers, ParameterFlags Flags, - EasyArray Literals); + EasyArray Literals, + string? ElementType, + int ArgIndex); private readonly record struct MethodTuple( string Namespace, @@ -367,10 +369,14 @@ static bool IsIndirectRespContext(ITypeSymbol type, out string memberName) } } + int nextArgIndex = 0; foreach (var param in method.Parameters) { var flags = ParameterFlags.Parameter; if (IsKey(param)) flags |= ParameterFlags.Key; + var elementType = param.Type; + flags |= GetTypeFlags(ref elementType); + string? elementTypeName = ReferenceEquals(elementType, param.Type) ? null : GetFullName(elementType); if (IsSERedis(param.Type, SERedis.CommandFlags)) { flags |= ParameterFlags.CommandFlags; @@ -440,7 +446,8 @@ void AddLiteral(string token, LiteralFlags literalFlags) } var literalArray = literals is null ? EasyArray.Empty : new(literals.ToArray()); - parameters.Add(new(GetFullName(param.Type), param.Name, modifiers, flags, literalArray)); + var argIndex = (flags & ParameterFlags.Data) != 0 ? nextArgIndex++ : -1; + parameters.Add(new(GetFullName(param.Type), param.Name, modifiers, flags, literalArray, elementTypeName, argIndex)); } var syntax = (MethodDeclarationSyntax)ctx.Node; @@ -479,6 +486,72 @@ static string TypeModifiers(ITypeSymbol type) } } + private static ParameterFlags GetTypeFlags(ref ITypeSymbol paramType) + { + var flags = ParameterFlags.None; + if (paramType.IsValueType) flags |= ParameterFlags.ValueType; + switch (paramType.NullableAnnotation) + { + case NullableAnnotation.Annotated: + flags |= ParameterFlags.Nullable; + break; + case NullableAnnotation.None: + if (paramType.IsReferenceType) flags |= ParameterFlags.Nullable; + break; + } + + if (paramType is IArrayTypeSymbol arr) + { + if (arr.Rank == 1 && arr.ElementType.SpecialType != SpecialType.System_Byte) + { + flags |= ParameterFlags.Collection; + paramType = arr.ElementType; + } + } + + if (paramType is INamedTypeSymbol { IsGenericType: true, Arity: 1 } gen) + { + switch (gen.ConstructedFrom.SpecialType) + { + case SpecialType.System_Collections_Generic_ICollection_T: + case SpecialType.System_Collections_Generic_IList_T: + case SpecialType.System_Collections_Generic_IReadOnlyCollection_T: + case SpecialType.System_Collections_Generic_IReadOnlyList_T: + flags |= ParameterFlags.Collection | ParameterFlags.CollectionWithCount; + paramType = gen.TypeArguments[0]; + break; + default: + if (IsSystemCollections(gen.ConstructedFrom, "List")) + { + flags |= ParameterFlags.Collection; + paramType = gen.TypeArguments[0]; + } + if (IsSystemCollections(gen.ConstructedFrom, "ImmutableArray", "Immutable")) + { + flags |= ParameterFlags.Collection | ParameterFlags.ImmutableArray; + paramType = gen.TypeArguments[0]; + } + break; + } + } + + return flags; + + static bool IsSystemCollections(INamedTypeSymbol type, string name, string ns = "Generic") + => type.Name == name && type.ContainingNamespace is { } actualNs && actualNs.Name == ns + && actualNs.ContainingNamespace is + { + Name: + "Collections", + ContainingNamespace: + { + Name: + "System", + ContainingNamespace.IsGlobalNamespace: true, + } + }; + } + private bool IsKey(IParameterSymbol param) { if (param.Name.EndsWith("key", StringComparison.InvariantCultureIgnoreCase)) @@ -799,29 +872,141 @@ void WriteMethod(bool asAsync) sb.Append(" request)"); NewLine().Append("{"); indent++; - var argCount = DataParameterCount(parameters, out int literalCount); - if (tuple.Value.Command is { Length: > 0 } cmd + var argCount = DataParameterCount(parameters, out int constantCount, out bool isVariable); + + void WriteParameterName(in ParameterTuple p, StringBuilder? target = null) + { + target ??= sb; + if (argCount == 1) + { + target.Append("request"); + } + else + { + target.Append("request."); + if (names == TupleMode.SyntheticNames) + { + target.Append("Arg").Append(p.ArgIndex); + } + else + { + target.Append(p.Name); + } + } + } + + int index; + if (isVariable) + { + sb = NewLine().Append("writer.WriteCommand(command,"); + bool firstVariableItem = true; + if (constantCount != 0) + { + sb.Append(" ").Append(constantCount).Append(" // constant args"); + firstVariableItem = false; + } + indent++; + index = 0; + foreach (var parameter in parameters.Span) + { + var match = parameter.Flags & (ParameterFlags.Collection | ParameterFlags.Nullable); + if (match != 0) + { + sb = NewLine(); + if (firstVariableItem) + { + firstVariableItem = false; + } + else + { + sb.Append("+ "); + } + + var literalCount = parameter.Literals.Length; + switch (match) + { + case ParameterFlags.Nullable: + sb.Append("("); + WriteParameterName(parameter); + sb.Append(" is null ? 0 : ").Append(1 + literalCount).Append(")"); + break; + case ParameterFlags.Collection: + // non-nullable collection; literals already handled + switch (parameter.Flags & (ParameterFlags.CollectionWithCount | ParameterFlags.ImmutableArray)) + { + case ParameterFlags.CollectionWithCount: + WriteParameterName(parameter); + sb.Append(".Count"); + break; + case ParameterFlags.ImmutableArray: // needs special care because of default (breaks .Length) + sb.Append("("); + WriteParameterName(parameter); + sb.Append(".IsDefaultOrEmpty ? 0 : "); + WriteParameterName(parameter); + sb.Append(".Length)"); + break; + default: + WriteParameterName(parameter); + sb.Append(".Length"); + break; + } + break; + case ParameterFlags.Collection | ParameterFlags.Nullable: + sb.Append("("); + WriteParameterName(parameter); + sb.Append(" is null ? 0 : "); + if (literalCount != 0) sb.Append("("); + switch (parameter.Flags & ParameterFlags.CollectionWithCount) + { + case ParameterFlags.CollectionWithCount: + WriteParameterName(parameter); + sb.Append(".Count"); + break; + case ParameterFlags.ImmutableArray: // needs special care because of default (breaks .Length) + sb.Append("("); + WriteParameterName(parameter); + sb.Append(".GetValueOrDefault().IsDefaultOrEmpty ? 0 : "); + WriteParameterName(parameter); + sb.Append(".GetValueOrDefault().Length)"); + break; + default: + WriteParameterName(parameter); + sb.Append(".Length"); + break; + } + if (literalCount != 0) sb.Append(" + ").Append(literalCount).Append(")"); + sb.Append(")"); + break; + } + sb.Append(" // ").Append(match); + } + index++; + } + NewLine().Append(");"); + indent--; + } + else if (tuple.Value.Command is { Length: > 0 } cmd && Encoding.UTF8.GetByteCount(cmd) == cmd.Length) // check pure ASCII { // only used by one command; allow optimization NewLine().Append("if(writer.CommandMap is null)"); NewLine().Append("{"); indent++; - string raw = $"*{argCount + literalCount + 1}\r\n${cmd.Length}\r\n{tuple.Value.Command}\r\n"; + string raw = $"*{constantCount + 1}\r\n${cmd.Length}\r\n{tuple.Value.Command}\r\n"; sb = NewLine().Append("writer.WriteRaw(").Append(CodeLiteral(raw)).Append("u8); // ") - .Append(cmd).Append(" with ").Append(argCount + literalCount).Append(" args"); + .Append(cmd).Append(" with ").Append(constantCount).Append(" args"); indent--; NewLine().Append("}"); NewLine().Append("else"); NewLine().Append("{"); indent++; - NewLine().Append("writer.WriteCommand(command, ").Append(argCount + literalCount).Append(");"); + NewLine().Append("writer.WriteCommand(command, ").Append(constantCount).Append(");"); indent--; NewLine().Append("}"); } else { - NewLine().Append("writer.WriteCommand(command, ").Append(argCount + literalCount).Append(");"); + NewLine().Append("writer.WriteCommand(command, ").Append(constantCount).Append(");"); } void WritePrefix(ParameterTuple p) => WriteLiteral(p, false); @@ -836,66 +1021,79 @@ void WriteLiteral(ParameterTuple p, bool suffix) { var len = Encoding.UTF8.GetByteCount(literal.Token); var resp = $"${len}\r\n{literal.Token}\r\n"; - NewLine().Append("writer.WriteRaw(").Append(CodeLiteral(resp)).Append("u8); // ").Append(literal.Token); + NewLine().Append("writer.WriteRaw(").Append(CodeLiteral(resp)).Append("u8); // ") + .Append(literal.Token); } } } - if (argCount == 1) + index = 0; + foreach (var parameter in parameters.Span) { - var p = FirstDataParameter(parameters); - WritePrefix(p); - sb = NewLine().Append("writer."); - if (p.Type is "global::StackExchange.Redis.RedisValue" or "global::StackExchange.Redis.RedisKey") - { - sb.Append("Write"); - } - else + if ((parameter.Flags & ParameterFlags.DataParameter) == ParameterFlags.DataParameter) { - sb.Append((p.Flags & ParameterFlags.Key) == 0 ? "WriteBulkString" : "WriteKey"); - } + bool isNullable = (parameter.Flags & ParameterFlags.Nullable) != 0; + bool isCollection = (parameter.Flags & ParameterFlags.Collection) != 0; + if (isNullable) + { + sb = NewLine().Append("if ("); + WriteParameterName(parameter); + sb.Append(" is not null)"); + NewLine().Append("{"); + indent++; + } - sb.Append("(request);"); - WriteSuffix(p); - } - else - { - int index = 0; - foreach (var parameter in parameters.Span) - { - if ((parameter.Flags & ParameterFlags.DataParameter) == ParameterFlags.DataParameter) + WritePrefix(parameter); + var elementType = parameter.ElementType ?? parameter.Type; + if (isCollection) { - WritePrefix(parameter); - sb = NewLine().Append("writer."); - if (parameter.Type is "global::StackExchange.Redis.RedisValue" - or "global::StackExchange.Redis.RedisKey") - { - sb.Append("Write"); - } - else - { - sb.Append((parameter.Flags & ParameterFlags.Key) == 0 ? "WriteBulkString" : "WriteKey"); - } + sb = NewLine().Append("foreach (").Append(elementType).Append(" val in "); + WriteParameterName(parameter); + sb.Append(")"); + NewLine().Append("{"); + indent++; + } - sb.Append("(request."); - if (names == TupleMode.SyntheticNames) - { - sb.Append("Arg").Append(index); - } - else - { - sb.Append(parameter.Name); - } + sb = NewLine().Append("writer."); + if (elementType is "global::StackExchange.Redis.RedisValue" + or "global::StackExchange.Redis.RedisKey") + { + sb.Append("Write"); + } + else + { + sb.Append((parameter.Flags & ParameterFlags.Key) == 0 ? "WriteBulkString" : "WriteKey"); + } - sb.Append(");"); - index++; - WriteSuffix(parameter); + sb.Append("("); + if (isCollection) + { + sb.Append("val"); + } + else + { + WriteParameterName(parameter); + } + sb.Append(");"); + + if (isCollection) + { + indent--; + NewLine().Append("}"); } - } - Debug.Assert(index == argCount, "wrote all parameters"); + WriteSuffix(parameter); + if (isNullable) + { + indent--; + NewLine().Append("}"); + } + index++; + } } + Debug.Assert(index == argCount, "wrote all parameters"); + indent--; NewLine().Append("}"); indent--; @@ -1000,20 +1198,30 @@ private static ParameterTuple FirstDataParameter( private static int DataParameterCount( EasyArray parameters) - => DataParameterCount(parameters, out _); + => DataParameterCount(parameters, out _, out _); private static int DataParameterCount( - EasyArray parameters, out int literalCount) + EasyArray parameters, out int constantCount, out bool isVariable) { - literalCount = 0; + // note: constantCount includes literals + constantCount = 0; + isVariable = false; if (parameters.IsEmpty) return 0; int count = 0; foreach (var parameter in parameters.Span) { if ((parameter.Flags & ParameterFlags.DataParameter) == ParameterFlags.DataParameter) { - if (!parameter.Literals.IsEmpty) literalCount += parameter.Literals.Length; count++; + if ((parameter.Flags & (ParameterFlags.Collection | ParameterFlags.Nullable)) != 0) + { + isVariable = true; // variable if either collection or nullable + } + + if ((parameter.Flags & ParameterFlags.Nullable) == 0 & !parameter.Literals.IsEmpty) + { + constantCount += parameter.Literals.Length; // we include literals if not nullable + } } } @@ -1086,6 +1294,11 @@ private enum ParameterFlags DataParameter = Data | Parameter, Key = 1 << 2, CommandFlags = 1 << 3, + ValueType = 1 << 4, + Nullable = 1 << 5, + Collection = 1 << 6, + CollectionWithCount = 1 << 7, // has .Count, otherwise assumed to have .Length + ImmutableArray = 1 << 8, } // compares whether a formatter can be shared, which depends on the key index and types (not names) diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Geo.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Geo.cs index 97296b0a5..87d28ab47 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Geo.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Geo.cs @@ -36,12 +36,6 @@ public Task GeoRemoveAsync(RedisKey key, RedisValue member, CommandFlags f public Task GeoHashAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task GeoPositionAsync( - RedisKey key, - RedisValue[] members, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - public Task GeoPositionAsync( RedisKey key, RedisValue member, @@ -151,8 +145,8 @@ public bool GeoRemove(RedisKey key, RedisValue member, CommandFlags flags = Comm public string? GeoHash(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public GeoPosition?[] GeoPosition(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("geopos")] + public partial GeoPosition?[] GeoPosition(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); public GeoPosition? GeoPosition(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); From 15f7eef2414346a7cff971e33ffa270078b12e0b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 24 Sep 2025 12:11:33 +0100 Subject: [PATCH 075/108] account for basic parameters! --- eng/StackExchange.Redis.Build/RespCommandGenerator.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index 7eb0b1d0a..e58147602 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -1217,6 +1217,10 @@ private static int DataParameterCount( { isVariable = true; // variable if either collection or nullable } + else + { + constantCount++; + } if ((parameter.Flags & ParameterFlags.Nullable) == 0 & !parameter.Literals.IsEmpty) { From cc3558f411e057de6497273cd3027bb5d2c0472f Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 25 Sep 2025 14:00:31 +0100 Subject: [PATCH 076/108] hash tests (for memory repro) --- .../RespCommandGenerator.cs | 1 + .../RespContextDatabase.Hash.cs | 71 ++----- .../RespParsers.cs | 47 ++++- .../RespReader.AggregateEnumerator.cs | 21 +++ src/RESPite/Messages/RespReader.cs | 57 +++++- tests/RESPite.Tests/ConnectionFixture.cs | 174 ++++++++++++++++++ tests/RESPite.Tests/IntegrationTestBase.cs | 13 ++ tests/RESPite.Tests/RedisDatabaseTests.cs | 41 +++++ 8 files changed, 363 insertions(+), 62 deletions(-) create mode 100644 tests/RESPite.Tests/RedisDatabaseTests.cs diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index e58147602..61eb63f8a 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -1267,6 +1267,7 @@ private static int DataParameterCount( "global::StackExchange.Redis.RedisKey" => "global::RESPite.StackExchange.Redis.RespParsers.RedisKey", "global::StackExchange.Redis.RedisValue" => "global::RESPite.StackExchange.Redis.RespParsers.RedisValue", "global::StackExchange.Redis.RedisValue[]" => "global::RESPite.StackExchange.Redis.RespParsers.RedisValueArray", + "global::StackExchange.Redis.HashEntry[]" => "global::RESPite.StackExchange.Redis.RespParsers.HashEntryArray", "global::StackExchange.Redis.Lease" => "global::RESPite.StackExchange.Redis.RespParsers.BytesLease", _ => null, }; diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs index 8d590b13c..1465b4cd5 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs @@ -174,41 +174,6 @@ public Task HashFieldSetAndSetExpiryAsync( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashGetAllAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashIncrementAsync( - RedisKey key, - RedisValue hashField, - long value = 1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashIncrementAsync( - RedisKey key, - RedisValue hashField, - double value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashKeysAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashRandomFieldAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashRandomFieldsAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashRandomFieldsWithValuesAsync( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - public IAsyncEnumerable HashScanAsync( RedisKey key, RedisValue pattern = default, @@ -419,37 +384,37 @@ public RedisValue HashFieldSetAndSetExpiry( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public HashEntry[] HashGetAll(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("hgetall")] + public partial HashEntry[] HashGetAll(RedisKey key, CommandFlags flags = CommandFlags.None); - public long HashIncrement( + [RespCommand("hincrby")] + public partial long HashIncrement( RedisKey key, RedisValue hashField, long value = 1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None); - public double HashIncrement( + [RespCommand("hincrbyfloat")] + public partial double HashIncrement( RedisKey key, RedisValue hashField, double value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None); - public RedisValue[] HashKeys(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("hkeys")] + public partial RedisValue[] HashKeys(RedisKey key, CommandFlags flags = CommandFlags.None); - public long HashLength(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("hlen")] + public partial long HashLength(RedisKey key, CommandFlags flags = CommandFlags.None); - public RedisValue HashRandomField(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("hrandfield")] + public partial RedisValue HashRandomField(RedisKey key, CommandFlags flags = CommandFlags.None); - public RedisValue[] HashRandomFields(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("hrandfield")] + public partial RedisValue[] HashRandomFields(RedisKey key, long count, CommandFlags flags = CommandFlags.None); - public HashEntry[] HashRandomFieldsWithValues(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("hrandfield")] + public partial HashEntry[] HashRandomFieldsWithValues(RedisKey key, [RespSuffix("WITHVALUES")] long count, CommandFlags flags = CommandFlags.None); public IEnumerable HashScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) => throw new NotImplementedException(); diff --git a/src/RESPite.StackExchange.Redis/RespParsers.cs b/src/RESPite.StackExchange.Redis/RespParsers.cs index fe17301a0..cff1bb209 100644 --- a/src/RESPite.StackExchange.Redis/RespParsers.cs +++ b/src/RESPite.StackExchange.Redis/RespParsers.cs @@ -9,6 +9,7 @@ public static class RespParsers public static IRespParser RedisValueArray => DefaultParser.Instance; public static IRespParser RedisKey => DefaultParser.Instance; public static IRespParser> BytesLease => DefaultParser.Instance; + public static IRespParser HashEntryArray => DefaultParser.Instance; public static RedisValue ReadRedisValue(ref RespReader reader) { @@ -29,17 +30,19 @@ public static RedisKey ReadRedisKey(ref RespReader reader) return reader.ReadByteArray(); } + private static readonly RespReader.Projection SharedReadRedisValue = ReadRedisValue; + private static readonly RespReader.Projection SharedReadRedisKey = ReadRedisKey; + private sealed class DefaultParser : IRespParser, IRespParser, - IRespParser>, IRespParser + IRespParser>, IRespParser, IRespParser, + IRespParser { private DefaultParser() { } public static readonly DefaultParser Instance = new(); - RedisValue IRespParser.Parse(ref RespReader reader) - => ReadRedisValue(ref reader); + RedisValue IRespParser.Parse(ref RespReader reader) => ReadRedisValue(ref reader); - RedisKey IRespParser.Parse(ref RespReader reader) - => ReadRedisKey(ref reader); + RedisKey IRespParser.Parse(ref RespReader reader) => ReadRedisKey(ref reader); Lease IRespParser>.Parse(ref RespReader reader) { @@ -51,6 +54,38 @@ Lease IRespParser>.Parse(ref RespReader reader) return lease; } - public RedisValue[] Parse(ref RespReader reader) => reader.ReadArray(ReadRedisValue)!; + RedisValue[] IRespParser.Parse(ref RespReader reader) + => reader.ReadArray(SharedReadRedisValue, scalar: true)!; + + RedisKey[] IRespParser.Parse(ref RespReader reader) + => reader.ReadArray(SharedReadRedisKey, scalar: true)!; + + HashEntry[] IRespParser.Parse(ref RespReader reader) + { + return reader.ReadPairArray( + SharedReadRedisValue, + SharedReadRedisValue, + static (x, y) => new HashEntry(x, y), + scalar: true)!; + + /* we could also do this locally: + reader.DemandAggregate(); + if (reader.IsNull) return null!; + var len = reader.AggregateLength() / 2; + if (len == 0) return []; + + var result = new HashEntry[len]; + for (int i = 0; i < result.Length; i++) + { + reader.MoveNextScalar(); + var x = ReadRedisValue(ref reader); + reader.MoveNextScalar(); + var y = ReadRedisValue(ref reader); + result[i] = new HashEntry(x, y); + } + + return result; + */ + } } } diff --git a/src/RESPite/Messages/RespReader.AggregateEnumerator.cs b/src/RESPite/Messages/RespReader.AggregateEnumerator.cs index d325789f6..1853d2ee6 100644 --- a/src/RESPite/Messages/RespReader.AggregateEnumerator.cs +++ b/src/RESPite/Messages/RespReader.AggregateEnumerator.cs @@ -164,6 +164,27 @@ public void FillAll(scoped Span target, Projection projection) target[i] = projection(ref Value); } } + + public void FillAll( + scoped Span target, + Projection first, + Projection second, + Func combine) + { + for (int i = 0; i < target.Length; i++) + { + if (!MoveNext()) ThrowEof(); + + Value.MoveNext(); // skip any attributes etc + var x = first(ref Value); + + if (!MoveNext()) ThrowEof(); + + Value.MoveNext(); // skip any attributes etc + var y = second(ref Value); + target[i] = combine(x, y); + } + } } internal void TrimToTotal(long length) => TrimToRemaining(length - BytesConsumed); diff --git a/src/RESPite/Messages/RespReader.cs b/src/RESPite/Messages/RespReader.cs index 62eb7383c..0b2bd6764 100644 --- a/src/RESPite/Messages/RespReader.cs +++ b/src/RESPite/Messages/RespReader.cs @@ -1666,14 +1666,65 @@ public readonly T ReadEnum(T unknownValue = default) where T : struct, Enum #endif } - public T[]? ReadArray(Projection projection) + public TResult[]? ReadArray(Projection projection, bool scalar = false) { DemandAggregate(); if (IsNull) return null; var len = AggregateLength(); if (len == 0) return []; - T[] result = new T[len]; - FillAll(result, projection); + var result = new TResult[len]; + if (scalar) + { + // if the data to be consumed is simple (scalar), we can use + // a simpler path that doesn't need to worry about RESP subtrees + for (int i = 0; i < result.Length; i++) + { + MoveNextScalar(); + result[i] = projection(ref this); + } + } + else + { + var agg = AggregateChildren(); + agg.FillAll(result, projection); + agg.MovePast(out this); + } + + return result; + } + + public TResult[]? ReadPairArray( + Projection first, + Projection second, + Func combine, + bool scalar = true) + { + DemandAggregate(); + if (IsNull) return null; + int sourceLength = AggregateLength(); + if (sourceLength is 0 or 1) return []; + var result = new TResult[sourceLength >> 1]; + if (scalar) + { + // if the data to be consumed is simple (scalar), we can use + // a simpler path that doesn't need to worry about RESP subtrees + for (int i = 0; i < result.Length; i++) + { + MoveNextScalar(); + var x = first(ref this); + MoveNextScalar(); + var y = second(ref this); + result[i] = combine(x, y); + } + // if we have an odd number of source elements, skip the last one + if ((sourceLength & 1) != 0) MoveNextScalar(); + } + else + { + var agg = AggregateChildren(); + agg.FillAll(result, first, second, combine); + agg.MovePast(out this); + } return result; } } diff --git a/tests/RESPite.Tests/ConnectionFixture.cs b/tests/RESPite.Tests/ConnectionFixture.cs index fbaa8c5f4..d1b9b5dab 100644 --- a/tests/RESPite.Tests/ConnectionFixture.cs +++ b/tests/RESPite.Tests/ConnectionFixture.cs @@ -1,6 +1,13 @@ using System; +using System.IO; using System.Net; +using System.Threading.Tasks; +using Microsoft.Testing.Platform.Extensions.Messages; using RESPite.Connections; +using RESPite.StackExchange.Redis; +using StackExchange.Redis; +using StackExchange.Redis.Maintenance; +using StackExchange.Redis.Profiling; using Xunit; [assembly: AssemblyFixture(typeof(RESPite.Tests.ConnectionFixture))] @@ -9,8 +16,14 @@ namespace RESPite.Tests; public class ConnectionFixture : IDisposable { + private readonly IConnectionMultiplexer _muxer; private readonly RespConnectionPool _pool = new(); + public ConnectionFixture() + { + _muxer = new DummyMultiplexer(this); + } + public void Dispose() => _pool.Dispose(); public RespConnection GetConnection() @@ -18,4 +31,165 @@ public RespConnection GetConnection() var template = _pool.Template.WithCancellationToken(TestContext.Current.CancellationToken); return _pool.GetConnection(template); } + + public IConnectionMultiplexer Multiplexer => _muxer; +} + +internal sealed class DummyMultiplexer(ConnectionFixture fixture) : IConnectionMultiplexer +{ + public override string ToString() => nameof(DummyMultiplexer); + private readonly ConnectionFixture _fixture = fixture; + private readonly string clientName = ""; + private readonly string configuration = ""; +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + private int timeoutMilliseconds; + private long operationCount; + private bool preserveAsyncOrder; + private bool isConnected; + private bool isConnecting; + private bool includeDetailInExceptions; + private int stormLogThreshold; +#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value + + void IDisposable.Dispose() { } + + ValueTask IAsyncDisposable.DisposeAsync() => default; + + string IConnectionMultiplexer.ClientName => clientName; + + string IConnectionMultiplexer.Configuration => configuration; + + int IConnectionMultiplexer.TimeoutMilliseconds => timeoutMilliseconds; + + long IConnectionMultiplexer.OperationCount => operationCount; + + bool IConnectionMultiplexer.PreserveAsyncOrder + { + get => preserveAsyncOrder; + set => preserveAsyncOrder = value; + } + + bool IConnectionMultiplexer.IsConnected => isConnected; + + bool IConnectionMultiplexer.IsConnecting => isConnecting; + + bool IConnectionMultiplexer.IncludeDetailInExceptions + { + get => includeDetailInExceptions; + set => includeDetailInExceptions = value; + } + + int IConnectionMultiplexer.StormLogThreshold + { + get => stormLogThreshold; + set => stormLogThreshold = value; + } + + void IConnectionMultiplexer.RegisterProfiler(Func profilingSessionProvider) => + throw new NotImplementedException(); + + ServerCounters IConnectionMultiplexer.GetCounters() => throw new NotImplementedException(); + + event EventHandler? IConnectionMultiplexer.ErrorMessage + { + add => throw new NotImplementedException(); + remove => throw new NotImplementedException(); + } + + event EventHandler? IConnectionMultiplexer.ConnectionFailed + { + add => throw new NotImplementedException(); + remove => throw new NotImplementedException(); + } + + event EventHandler? IConnectionMultiplexer.InternalError + { + add => throw new NotImplementedException(); + remove => throw new NotImplementedException(); + } + + event EventHandler? IConnectionMultiplexer.ConnectionRestored + { + add => throw new NotImplementedException(); + remove => throw new NotImplementedException(); + } + + event EventHandler? IConnectionMultiplexer.ConfigurationChanged + { + add => throw new NotImplementedException(); + remove => throw new NotImplementedException(); + } + + event EventHandler? IConnectionMultiplexer.ConfigurationChangedBroadcast + { + add => throw new NotImplementedException(); + remove => throw new NotImplementedException(); + } + + event EventHandler? IConnectionMultiplexer.ServerMaintenanceEvent + { + add => throw new NotImplementedException(); + remove => throw new NotImplementedException(); + } + + EndPoint[] IConnectionMultiplexer.GetEndPoints(bool configuredOnly) => throw new NotImplementedException(); + + void IConnectionMultiplexer.Wait(Task task) => throw new NotImplementedException(); + + T IConnectionMultiplexer.Wait(Task task) => throw new NotImplementedException(); + + void IConnectionMultiplexer.WaitAll(params Task[] tasks) => throw new NotImplementedException(); + + event EventHandler? IConnectionMultiplexer.HashSlotMoved + { + add => throw new NotImplementedException(); + remove => throw new NotImplementedException(); + } + + int IConnectionMultiplexer.HashSlot(RedisKey key) => throw new NotImplementedException(); + + ISubscriber IConnectionMultiplexer.GetSubscriber(object? asyncState) => throw new NotImplementedException(); + + IDatabase IConnectionMultiplexer.GetDatabase(int db, object? asyncState) => throw new NotImplementedException(); + + IServer IConnectionMultiplexer.GetServer(string host, int port, object? asyncState) => + throw new NotImplementedException(); + + IServer IConnectionMultiplexer.GetServer(string hostAndPort, object? asyncState) => + throw new NotImplementedException(); + + IServer IConnectionMultiplexer.GetServer(IPAddress host, int port) => throw new NotImplementedException(); + + IServer IConnectionMultiplexer.GetServer(EndPoint endpoint, object? asyncState) => + throw new NotImplementedException(); + + IServer[] IConnectionMultiplexer.GetServers() => throw new NotImplementedException(); + + Task IConnectionMultiplexer.ConfigureAsync(TextWriter? log) => throw new NotImplementedException(); + + bool IConnectionMultiplexer.Configure(TextWriter? log) => throw new NotImplementedException(); + + string IConnectionMultiplexer.GetStatus() => throw new NotImplementedException(); + + void IConnectionMultiplexer.GetStatus(TextWriter log) => throw new NotImplementedException(); + + void IConnectionMultiplexer.Close(bool allowCommandsToComplete) => throw new NotImplementedException(); + + Task IConnectionMultiplexer.CloseAsync(bool allowCommandsToComplete) => throw new NotImplementedException(); + + string? IConnectionMultiplexer.GetStormLog() => throw new NotImplementedException(); + + void IConnectionMultiplexer.ResetStormLog() => throw new NotImplementedException(); + + long IConnectionMultiplexer.PublishReconfigure(CommandFlags flags) => throw new NotImplementedException(); + + Task IConnectionMultiplexer.PublishReconfigureAsync(CommandFlags flags) => + throw new NotImplementedException(); + + int IConnectionMultiplexer.GetHashSlot(RedisKey key) => throw new NotImplementedException(); + + void IConnectionMultiplexer.ExportConfiguration(Stream destination, ExportOptions options) => + throw new NotImplementedException(); + + void IConnectionMultiplexer.AddLibraryNameSuffix(string suffix) => throw new NotImplementedException(); } diff --git a/tests/RESPite.Tests/IntegrationTestBase.cs b/tests/RESPite.Tests/IntegrationTestBase.cs index 22e2b1fb7..d1ef3c7e8 100644 --- a/tests/RESPite.Tests/IntegrationTestBase.cs +++ b/tests/RESPite.Tests/IntegrationTestBase.cs @@ -1,5 +1,8 @@ using System.Runtime.CompilerServices; +using System.Threading.Tasks; using RESPite.Redis.Alt; +using RESPite.StackExchange.Redis; +using StackExchange.Redis; using Xunit; namespace RESPite.Tests; @@ -14,6 +17,16 @@ public RespConnection GetConnection([CallerMemberName] string caller = "") return conn; } + public async ValueTask GetConnectionAsync([CallerMemberName] string caller = "") + { + var conn = fixture.GetConnection(); // includes cancellation from the test + // most of the time, they'll be using a key from Me(), so: pre-emptively nuke it + await conn.Context.AsKeys().DelAsync(caller).ConfigureAwait(false); + return conn; + } + + public IDatabase AsDatabase(RespConnection conn, int db = 0) => new RespContextDatabase(fixture.Multiplexer, conn, db); + public void Log(string message) => log?.WriteLine(message); protected string Me([CallerMemberName] string caller = "") => caller; diff --git a/tests/RESPite.Tests/RedisDatabaseTests.cs b/tests/RESPite.Tests/RedisDatabaseTests.cs new file mode 100644 index 000000000..a2df1ade1 --- /dev/null +++ b/tests/RESPite.Tests/RedisDatabaseTests.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using StackExchange.Redis; +using Xunit; + +namespace RESPite.Tests; + +public class RedisDatabaseTests(ConnectionFixture fixture, ITestOutputHelper log) + : IntegrationTestBase(fixture, log) +{ + [Fact] + public void HashSetGetAll() + { + var key = Me(); + + using var conn = GetConnection(); + var db = AsDatabase(conn); + db.HashSet(key, "abc", "xyz"); + db.HashSet(key, "def", "uvw"); + + var all = db.HashGetAll(key); + Assert.Equal(2, all.Length); + Assert.Contains(new HashEntry("abc", "xyz"), all); + Assert.Contains(new HashEntry("def", "uvw"), all); + } + + [Fact] + public async Task HashSetGetAllAsync() + { + var key = Me(); + + await using var conn = await GetConnectionAsync(); + var db = AsDatabase(conn); + await db.HashSetAsync(key, "abc", "xyz"); + await db.HashSetAsync(key, "def", "uvw"); + + var all = await db.HashGetAllAsync(key); + Assert.Equal(2, all.Length); + Assert.Contains(new HashEntry("abc", "xyz"), all); + Assert.Contains(new HashEntry("def", "uvw"), all); + } +} From d770e3d3d113f70126cf3219d48c0b1fa91e4b3f Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 26 Sep 2025 10:00:39 +0100 Subject: [PATCH 077/108] Update ReleaseNotes.md bad merge --- docs/ReleaseNotes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 43133e0f8..8b24ea1b8 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -25,6 +25,8 @@ Current package versions: - Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2638)) - Add support for XPENDING Idle time filter ([#2822 by david-brink-talogy](https://github.com/StackExchange/StackExchange.Redis/pull/2822)) - Improve `double` formatting performance on net8+ ([#2928 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2928)) +- Add `GetServer(RedisKey, ...)` API ([#2936 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2936)) +- Fix error constructing `StreamAdd` message ([#2941 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2941)) ## 2.8.58 From e7800aba79cc1d917e2381574f1f3aebe101f24c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 26 Sep 2025 10:06:16 +0100 Subject: [PATCH 078/108] post-merge fixups --- .../RespContextDatabase.VectorSets.cs | 123 ++++++++++++++++++ tests/BasicTest/SlowConfig.cs | 2 +- tests/RESPite.Tests/OperationUnitTests.cs | 2 +- 3 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 src/RESPite.StackExchange.Redis/RespContextDatabase.VectorSets.cs diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.VectorSets.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.VectorSets.cs new file mode 100644 index 000000000..83e48ce83 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.VectorSets.cs @@ -0,0 +1,123 @@ +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal partial class RespContextDatabase +{ + // Vector Set operations + public Task VectorSetAddAsync( + RedisKey key, + VectorSetAddRequest request, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task?> VectorSetGetApproximateVectorAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task VectorSetGetAttributesJsonAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task?> VectorSetGetLinksAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task?> VectorSetGetLinksWithScoresAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task VectorSetRandomMembersAsync( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task VectorSetSetAttributesJsonAsync( + RedisKey key, + RedisValue member, + string attributesJson, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task?> VectorSetSimilaritySearchAsync( + RedisKey key, + VectorSetSimilaritySearchRequest query, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool VectorSetAdd(RedisKey key, VectorSetAddRequest request, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Lease? VectorSetGetApproximateVector( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public string? + VectorSetGetAttributesJson(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Lease? + VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Lease? VectorSetGetLinksWithScores( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public bool VectorSetSetAttributesJson( + RedisKey key, + RedisValue member, + string attributesJson, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Lease? VectorSetSimilaritySearch( + RedisKey key, + VectorSetSimilaritySearchRequest query, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); +} diff --git a/tests/BasicTest/SlowConfig.cs b/tests/BasicTest/SlowConfig.cs index 1d7e7cb1e..cc5aa4537 100644 --- a/tests/BasicTest/SlowConfig.cs +++ b/tests/BasicTest/SlowConfig.cs @@ -2,7 +2,7 @@ namespace BasicTest; -internal class SlowConfig : CustomConfig +internal sealed class SlowConfig : CustomConfig { protected override Job Configure(Job j) => j.WithLaunchCount(1) diff --git a/tests/RESPite.Tests/OperationUnitTests.cs b/tests/RESPite.Tests/OperationUnitTests.cs index 9160c92ac..27c877628 100644 --- a/tests/RESPite.Tests/OperationUnitTests.cs +++ b/tests/RESPite.Tests/OperationUnitTests.cs @@ -192,7 +192,7 @@ public void CoreValueTaskToTaskSupportsCancellation() #endif } - private class TestAwaitable : IValueTaskSource + private sealed class TestAwaitable : IValueTaskSource { private ManualResetValueTaskSourceCore _core; public ValueTask AsValueTask() => new(this, _core.Version); From 6e6db7111fb69ed740c9018349e3c17e07ec9571 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 26 Sep 2025 10:45:57 +0100 Subject: [PATCH 079/108] more post-merge fixups --- .../RespContextServer.cs | 6 +++++ .../RespMultiplexer.cs | 10 +++++++++ .../Connections/RespConnectionManager.cs | 20 +++++++++++++++++ .../ConnectionMultiplexer.cs | 20 ++++++++++++++--- .../Interfaces/IConnectionMultiplexer.cs | 13 +++++++++++ src/StackExchange.Redis/Interfaces/IServer.cs | 22 ++++++++++++++++++- src/StackExchange.Redis/RedisServer.cs | 18 +++++++++++++-- src/StackExchange.Redis/ServerEndPoint.cs | 6 +++++ tests/BasicTest/RedisBenchmarks.cs | 1 + tests/RESPite.Tests/ConnectionFixture.cs | 3 +++ tests/RESPite.Tests/RespMultiplexerTests.cs | 7 +++++- .../Helpers/SharedConnectionFixture.cs | 1 + 12 files changed, 120 insertions(+), 7 deletions(-) diff --git a/src/RESPite.StackExchange.Redis/RespContextServer.cs b/src/RESPite.StackExchange.Redis/RespContextServer.cs index d8433f4a7..e1392c676 100644 --- a/src/RESPite.StackExchange.Redis/RespContextServer.cs +++ b/src/RESPite.StackExchange.Redis/RespContextServer.cs @@ -159,6 +159,12 @@ public Task ExecuteAsync( ICollection args, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + public RedisResult Execute(int? database, string command, ICollection args, CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); + + public Task ExecuteAsync(int? database, string command, ICollection args, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); + public void FlushAllDatabases(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public Task FlushAllDatabasesAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); diff --git a/src/RESPite.StackExchange.Redis/RespMultiplexer.cs b/src/RESPite.StackExchange.Redis/RespMultiplexer.cs index 6d54be655..b80d6df1e 100644 --- a/src/RESPite.StackExchange.Redis/RespMultiplexer.cs +++ b/src/RESPite.StackExchange.Redis/RespMultiplexer.cs @@ -254,6 +254,16 @@ public IServer GetServer(EndPoint endpoint, object? asyncState = null) return GetServer(_connectionManager.GetNode(host, port), asyncState); } + public IServer GetServer(RedisKey key, object? asyncState = null, CommandFlags flags = CommandFlags.None) + { + if (key.IsNull) // just get anything + { + var node = _connectionManager.GetRandomNode(); + if (node is not null) return GetServer(node, asyncState); + } + throw new NotImplementedException(); + } + private IServer GetServer(Node node, object? asyncState) { if (asyncState is not null) ThrowNotSupported(); diff --git a/src/RESPite/Connections/RespConnectionManager.cs b/src/RESPite/Connections/RespConnectionManager.cs index d3072ed37..c8672f04a 100644 --- a/src/RESPite/Connections/RespConnectionManager.cs +++ b/src/RESPite/Connections/RespConnectionManager.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.Diagnostics.CodeAnalysis; +using System.Net; using RESPite.Connections.Internal; using RESPite.Internal; @@ -99,6 +100,7 @@ internal Task ConnectAsync(RespConfiguration options, ReadOnlySpan { pending[i] = snapshot[i].ConnectAsync(log); } + return ConnectAsyncAwaited(pending, log, snapshot.Length); } @@ -230,4 +232,22 @@ internal Node GetNode(string host, int port) internal Node GetNode(string hostAndPort) => ConnectionFactory.TryParse(hostAndPort, out var host, out var port) ? GetNode(host, port) : throw new ArgumentException($"Could not parse host and port from '{hostAndPort}'", nameof(hostAndPort)); + + internal Node? GetRandomNode() + { + var nodes = _nodes; + if (nodes is { Length: > 0 }) + { + var index = SharedRandom.Next(nodes.Length); + return nodes[index]; + } + + return null; + } + +#if NET5_0_OR_GREATER + private static Random SharedRandom => Random.Shared; +#else + private static Random SharedRandom { get; } = new(); +#endif } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 97cb5323b..06352092a 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -210,7 +210,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt { throw ExceptionFactory.AdminModeNotEnabled(RawConfig.IncludeDetailInExceptions, cmd, null, server); } - var srv = new RedisServer(this, server, null); + var srv = server.GetRedisServer(null); if (!srv.IsConnected) { throw ExceptionFactory.NoConnectionAvailable(this, null, server, GetServerSnapshot(), command: cmd); @@ -1229,7 +1229,21 @@ public IServer GetServer(EndPoint? endpoint, object? asyncState = null) throw new NotSupportedException($"The server API is not available via {RawConfig.Proxy}"); } var server = servers[endpoint] as ServerEndPoint ?? throw new ArgumentException("The specified endpoint is not defined", nameof(endpoint)); - return new RedisServer(this, server, asyncState); + return new RedisServer(server, asyncState); + } + + /// +#pragma warning disable RS0026 + public IServer GetServer(RedisKey key, object? asyncState = null, CommandFlags flags = CommandFlags.None) +#pragma warning restore RS0026 + { + // We'll spoof the GET command for this; we're not supporting ad-hoc access to the pub/sub channel, because: bad things. + // Any read-only-replica vs writable-primary concerns should be managed by the caller via "flags"; the default is PreferPrimary. + // Note that ServerSelectionStrategy treats "null" (default) keys as NoSlot, aka Any. + return (SelectServer(RedisCommand.GET, flags, key) ?? Throw()).GetRedisServer(asyncState); + + [DoesNotReturn] + static ServerEndPoint Throw() => throw new InvalidOperationException("It was not possible to resolve a connection to the server owning the specified key"); } /// @@ -1241,7 +1255,7 @@ public IServer[] GetServers() var result = new IServer[snapshot.Length]; for (var i = 0; i < snapshot.Length; i++) { - result[i] = new RedisServer(this, snapshot[i], null); + result[i] = snapshot[i].GetRedisServer(null); } return result; } diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs index b4bdb0950..15e6c1e2a 100644 --- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs @@ -213,6 +213,19 @@ public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable /// The async state to pass to the created . IServer GetServer(EndPoint endpoint, object? asyncState = null); + /// + /// Gets a server that would be used for a given key and flags. + /// + /// The endpoint to get a server for. In a non-cluster environment, this parameter is ignored. A key may be specified + /// on cluster, which will return a connection to an arbitrary server matching the specified flags. + /// The async state to pass to the created . + /// The command flags to use. + /// This method is particularly useful when communicating with a cluster environment, to obtain a connection to the server that owns the specified key + /// and ad-hoc commands with unusual routing requirements. Note that provides a connection that automatically routes commands by + /// looking for parameters, so this method is only necessary when used with commands that do not take a parameter, + /// but require consistent routing using key-like semantics. + IServer GetServer(RedisKey key, object? asyncState = null, CommandFlags flags = CommandFlags.None); + /// /// Obtain configuration APIs for all servers in this multiplexer. /// diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index 4971c7f18..952eafb9b 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -252,10 +252,13 @@ public partial interface IServer : IRedis /// Task EchoAsync(RedisValue message, CommandFlags flags = CommandFlags.None); +#pragma warning disable RS0026, RS0027 // multiple overloads /// /// Execute an arbitrary command against the server; this is primarily intended for /// executing modules, but may also be used to provide access to new features that lack - /// a direct API. + /// a direct API. The command is assumed to be not database-specific. If this is not the case, + /// should be used to + /// specify the database (using null to use the configured default database). /// /// The command to run. /// The arguments to pass for the command. @@ -280,6 +283,23 @@ public partial interface IServer : IRedis /// Task ExecuteAsync(string command, ICollection args, CommandFlags flags = CommandFlags.None); +#pragma warning restore RS0026, RS0027 + + /// + /// Execute an arbitrary database-specific command against the server; this is primarily intended for + /// executing modules, but may also be used to provide access to new features that lack + /// a direct API. + /// + /// The database ID; if , the configured default database is used. + /// The command to run. + /// The arguments to pass for the command. + /// The flags to use for this operation. + /// A dynamic representation of the command's result. + /// This API should be considered an advanced feature; inappropriate use can be harmful. + RedisResult Execute(int? database, string command, ICollection args, CommandFlags flags = CommandFlags.None); + + /// + Task ExecuteAsync(int? database, string command, ICollection args, CommandFlags flags = CommandFlags.None); /// /// Delete all the keys of all databases on the server. diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index af734b0f5..3bc306c69 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -16,9 +16,9 @@ internal sealed class RedisServer : RedisBase, IServer { private readonly ServerEndPoint server; - internal RedisServer(ConnectionMultiplexer multiplexer, ServerEndPoint server, object? asyncState) : base(multiplexer, asyncState) + internal RedisServer(ServerEndPoint server, object? asyncState) : base(server.Multiplexer, asyncState) { - this.server = server ?? throw new ArgumentNullException(nameof(server)); + this.server = server; // definitely can't be null because .Multiplexer in base call } int IServer.DatabaseCount => server.Databases; @@ -1045,6 +1045,20 @@ public Task ExecuteAsync(string command, ICollection args, return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } + public RedisResult Execute(int? database, string command, ICollection args, CommandFlags flags = CommandFlags.None) + { + var db = multiplexer.ApplyDefaultDatabase(database ?? -1); + var msg = new RedisDatabase.ExecuteMessage(multiplexer?.CommandMap, db, flags, command, args); + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + + public Task ExecuteAsync(int? database, string command, ICollection args, CommandFlags flags = CommandFlags.None) + { + var db = multiplexer.ApplyDefaultDatabase(database ?? -1); + var msg = new RedisDatabase.ExecuteMessage(multiplexer?.CommandMap, db, flags, command, args); + return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + /// /// For testing only. /// diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index b5f7cbb4c..06eaae4d9 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -71,6 +71,12 @@ public ServerEndPoint(ConnectionMultiplexer multiplexer, EndPoint endpoint) } } + private RedisServer? _defaultServer; + public RedisServer GetRedisServer(object? asyncState) + => asyncState is null + ? (_defaultServer ??= new RedisServer(this, null)) // reuse and memoize + : new RedisServer(this, asyncState); + public EndPoint EndPoint { get; } public ClusterConfiguration? ClusterConfiguration { get; private set; } diff --git a/tests/BasicTest/RedisBenchmarks.cs b/tests/BasicTest/RedisBenchmarks.cs index 0e6c02b93..240a3f471 100644 --- a/tests/BasicTest/RedisBenchmarks.cs +++ b/tests/BasicTest/RedisBenchmarks.cs @@ -191,6 +191,7 @@ public void StringGet() db.StringGet(StringKey_K); } } + #if !TEST_BASELINE /// /// Run StringSet lots of times. diff --git a/tests/RESPite.Tests/ConnectionFixture.cs b/tests/RESPite.Tests/ConnectionFixture.cs index d1b9b5dab..c80ddc022 100644 --- a/tests/RESPite.Tests/ConnectionFixture.cs +++ b/tests/RESPite.Tests/ConnectionFixture.cs @@ -163,6 +163,9 @@ IServer IConnectionMultiplexer.GetServer(string hostAndPort, object? asyncState) IServer IConnectionMultiplexer.GetServer(EndPoint endpoint, object? asyncState) => throw new NotImplementedException(); + public IServer GetServer(RedisKey key, object? asyncState = null, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); + IServer[] IConnectionMultiplexer.GetServers() => throw new NotImplementedException(); Task IConnectionMultiplexer.ConfigureAsync(TextWriter? log) => throw new NotImplementedException(); diff --git a/tests/RESPite.Tests/RespMultiplexerTests.cs b/tests/RESPite.Tests/RespMultiplexerTests.cs index f616366d2..b11e69b0d 100644 --- a/tests/RESPite.Tests/RespMultiplexerTests.cs +++ b/tests/RESPite.Tests/RespMultiplexerTests.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Threading.Tasks; using RESPite.StackExchange.Redis; +using StackExchange.Redis; using Xunit; namespace RESPite.Tests; @@ -16,7 +17,7 @@ public async Task CanConnect() await muxer.ConnectAsync("localhost:6379", log: logWriter); Assert.True(muxer.IsConnected); - var server = muxer.GetServer(muxer.GetEndPoints().Single()); + var server = muxer.GetServer(default(RedisKey)); Assert.IsType(server); // we expect this to *not* use routing server.Ping(); await server.PingAsync(); @@ -26,5 +27,9 @@ public async Task CanConnect() // since this is a single-node instance, we expect the proxied database to use the interactive connection db.Ping(); await db.PingAsync(); + + // ReSharper disable once MethodHasAsyncOverload + proxied.Ping(); + await proxied.PingAsync(); } } diff --git a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs index cf6c7d326..3eff986b9 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs @@ -197,6 +197,7 @@ public event EventHandler ServerMaintenanceEvent public IServer GetServer(IPAddress host, int port) => _inner.GetServer(host, port); public IServer GetServer(EndPoint endpoint, object? asyncState = null) => _inner.GetServer(endpoint, asyncState); + public IServer GetServer(RedisKey key, object? asyncState = null, CommandFlags flags = CommandFlags.None) => _inner.GetServer(key, asyncState, flags); public IServer[] GetServers() => _inner.GetServers(); From e19f4010172c1040d02e4025ac6f6cfeab10ee0a Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 26 Sep 2025 12:46:37 +0100 Subject: [PATCH 080/108] fix breaks --- .../RespContextDatabase.Connection.cs | 5 +++-- .../RespContextDatabase.cs | 15 +------------- .../RespContextExtensions.cs | 20 +++++++++++++++++++ .../RespContextServer.cs | 11 +++++++--- .../ConnectionMultiplexer.cs | 2 +- 5 files changed, 33 insertions(+), 20 deletions(-) create mode 100644 src/RESPite.StackExchange.Redis/RespContextExtensions.cs diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs index abacfa80d..f9ed5bf6d 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Connection.cs @@ -10,7 +10,7 @@ internal partial class RespContextDatabase public bool IsConnected(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - private static readonly byte[] PingRaw = "*1\r\n$4\r\nping\r\n"u8.ToArray(); + internal static readonly byte[] PingRaw = "*1\r\n$4\r\nping\r\n"u8.ToArray(); public Task PingAsync(CommandFlags flags = CommandFlags.None) => Context(flags).Send("ping"u8, DateTime.UtcNow, PingParser.Default, PingRaw).AsTask(); @@ -18,12 +18,13 @@ public Task PingAsync(CommandFlags flags = CommandFlags.None) => public TimeSpan Ping(CommandFlags flags = CommandFlags.None) => Context(flags).Send("ping"u8, DateTime.UtcNow, PingParser.Default, PingRaw).Wait(SyncTimeout); - private sealed class PingParser : IRespParser + internal sealed class PingParser : IRespParser { public static readonly PingParser Default = new(); private PingParser() { } public TimeSpan Parse(in DateTime state, ref RespReader reader) => DateTime.UtcNow - state; } + public Task IdentifyEndpointAsync(RedisKey key = default, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.cs index 302ffaa56..65e7d60b1 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.cs @@ -31,20 +31,7 @@ public RespContextDatabase(IConnectionMultiplexer muxer, IRespContextSource sour protected void SetSource(IRespContextSource source) => this._source = source; - // Question: cache this, or rebuild each time? the latter handles shutdown better. - // internal readonly RespContext Context = proxy.Context.WithDatabase(db); - private RespContext Context(CommandFlags flags) - { - // the flags intentionally align between CommandFlags and RespContextFlags - const RespContext.RespContextFlags flagMask = RespContext.RespContextFlags.DemandPrimary - | RespContext.RespContextFlags.DemandReplica - | RespContext.RespContextFlags.PreferReplica - | RespContext.RespContextFlags.NoRedirect - | RespContext.RespContextFlags.FireAndForget - | RespContext.RespContextFlags.NoScriptCache; - - return _source.Context.With(_db, (RespContext.RespContextFlags)flags, flagMask); - } + private RespContext Context(CommandFlags flags) => _source.Context.With(_db, flags); private TimeSpan SyncTimeout => _source.Context.SyncTimeout; public int Database => _db; diff --git a/src/RESPite.StackExchange.Redis/RespContextExtensions.cs b/src/RESPite.StackExchange.Redis/RespContextExtensions.cs new file mode 100644 index 000000000..b03b1019f --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RespContextExtensions.cs @@ -0,0 +1,20 @@ +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal static class RespContextExtensions +{ + // Question: cache this, or rebuild each time? the latter handles shutdown better. + // internal readonly RespContext Context = proxy.Context.WithDatabase(db); + internal static RespContext With(this in RespContext context, int db, CommandFlags flags) + { + // the flags intentionally align between CommandFlags and RespContextFlags + const RespContext.RespContextFlags FlagMask = RespContext.RespContextFlags.DemandPrimary + | RespContext.RespContextFlags.DemandReplica + | RespContext.RespContextFlags.PreferReplica + | RespContext.RespContextFlags.NoRedirect + | RespContext.RespContextFlags.FireAndForget + | RespContext.RespContextFlags.NoScriptCache; + return context.With(db, (RespContext.RespContextFlags)flags, FlagMask); + } +} diff --git a/src/RESPite.StackExchange.Redis/RespContextServer.cs b/src/RESPite.StackExchange.Redis/RespContextServer.cs index e1392c676..0e8caf5ee 100644 --- a/src/RESPite.StackExchange.Redis/RespContextServer.cs +++ b/src/RESPite.StackExchange.Redis/RespContextServer.cs @@ -12,10 +12,14 @@ namespace RESPite.StackExchange.Redis; internal sealed class RespContextServer(RespMultiplexer muxer, Node node) : IServer { // deliberately not caching this - if the connection changes, we want to know about it - internal ref readonly RespContext Context => ref node.Context; + internal RespContext Context(CommandFlags flags) => node.Context.With(-1, flags); + + private TimeSpan SyncTimeout => node.Context.SyncTimeout; public IConnectionMultiplexer Multiplexer => muxer; - public Task PingAsync(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public Task PingAsync(CommandFlags flags = CommandFlags.None) + => Context(flags).Send("ping"u8, DateTime.UtcNow, RespContextDatabase.PingParser.Default, RespContextDatabase.PingRaw).AsTask(); public bool TryWait(Task task) => task.Wait(Multiplexer.TimeoutMilliseconds); @@ -25,7 +29,8 @@ internal sealed class RespContextServer(RespMultiplexer muxer, Node node) : ISer public void WaitAll(params Task[] tasks) => Multiplexer.WaitAll(tasks); - public TimeSpan Ping(CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + public TimeSpan Ping(CommandFlags flags = CommandFlags.None) + => Context(flags).Send("ping"u8, DateTime.UtcNow, RespContextDatabase.PingParser.Default, RespContextDatabase.PingRaw).Wait(SyncTimeout); public ClusterConfiguration? ClusterConfiguration => throw new NotImplementedException(); public EndPoint EndPoint => node.Manager.ConnectionFactory.GetEndPoint(node.EndPoint, node.Port); diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 06352092a..cc766338a 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1229,7 +1229,7 @@ public IServer GetServer(EndPoint? endpoint, object? asyncState = null) throw new NotSupportedException($"The server API is not available via {RawConfig.Proxy}"); } var server = servers[endpoint] as ServerEndPoint ?? throw new ArgumentException("The specified endpoint is not defined", nameof(endpoint)); - return new RedisServer(server, asyncState); + return server.GetRedisServer(asyncState); } /// From ce086d9b777c46cb423e17242f7dfa954740f1df Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 26 Sep 2025 13:06:30 +0100 Subject: [PATCH 081/108] more bad merge --- tests/RedisConfigs/.docker/Redis/Dockerfile | 2 +- .../GetServerTests.cs | 150 ++++++++++++++++++ tests/StackExchange.Redis.Tests/KeyTests.cs | 4 +- .../StackExchange.Redis.Tests/StreamTests.cs | 22 +++ 4 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/GetServerTests.cs diff --git a/tests/RedisConfigs/.docker/Redis/Dockerfile b/tests/RedisConfigs/.docker/Redis/Dockerfile index 4de03f221..424abd1cd 100644 --- a/tests/RedisConfigs/.docker/Redis/Dockerfile +++ b/tests/RedisConfigs/.docker/Redis/Dockerfile @@ -1,4 +1,4 @@ -FROM redis:7.4.2 +FROM redis:8.2.0 COPY --from=configs ./Basic /data/Basic/ COPY --from=configs ./Failover /data/Failover/ diff --git a/tests/StackExchange.Redis.Tests/GetServerTests.cs b/tests/StackExchange.Redis.Tests/GetServerTests.cs new file mode 100644 index 000000000..50cb9e7ef --- /dev/null +++ b/tests/StackExchange.Redis.Tests/GetServerTests.cs @@ -0,0 +1,150 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace StackExchange.Redis.Tests; + +public abstract class GetServerTestsBase(ITestOutputHelper output, SharedConnectionFixture fixture) + : TestBase(output, fixture) +{ + protected abstract bool IsCluster { get; } + + [Fact] + public async Task GetServersMemoization() + { + await using var conn = Create(); + + var servers0 = conn.GetServers(); + var servers1 = conn.GetServers(); + + // different array, exact same contents + Assert.NotSame(servers0, servers1); + Assert.NotEmpty(servers0); + Assert.NotNull(servers0); + Assert.NotNull(servers1); + Assert.Equal(servers0.Length, servers1.Length); + for (int i = 0; i < servers0.Length; i++) + { + Assert.Same(servers0[i], servers1[i]); + } + } + + [Fact] + public async Task GetServerByEndpointMemoization() + { + await using var conn = Create(); + var ep = conn.GetEndPoints().First(); + + IServer x = conn.GetServer(ep), y = conn.GetServer(ep); + Assert.Same(x, y); + + object asyncState = "whatever"; + x = conn.GetServer(ep, asyncState); + y = conn.GetServer(ep, asyncState); + Assert.NotSame(x, y); + } + + [Fact] + public async Task GetServerByKeyMemoization() + { + await using var conn = Create(); + RedisKey key = Me(); + string value = $"{key}:value"; + await conn.GetDatabase().StringSetAsync(key, value); + + IServer x = conn.GetServer(key), y = conn.GetServer(key); + Assert.False(y.IsReplica, "IsReplica"); + Assert.Same(x, y); + + y = conn.GetServer(key, flags: CommandFlags.DemandMaster); + Assert.Same(x, y); + + // async state demands separate instance + y = conn.GetServer(key, "async state", flags: CommandFlags.DemandMaster); + Assert.NotSame(x, y); + + // primary and replica should be different + y = conn.GetServer(key, flags: CommandFlags.DemandReplica); + Assert.NotSame(x, y); + Assert.True(y.IsReplica, "IsReplica"); + + // replica again: same + var z = conn.GetServer(key, flags: CommandFlags.DemandReplica); + Assert.Same(y, z); + + // check routed correctly + var actual = (string?)await x.ExecuteAsync(null, "get", [key], CommandFlags.NoRedirect); + Assert.Equal(value, actual); // check value against primary + + // for replica, don't check the value, because of replication delay - just: no error + _ = y.ExecuteAsync(null, "get", [key], CommandFlags.NoRedirect); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GetServerWithDefaultKey(bool explicitNull) + { + await using var conn = Create(); + bool isCluster = conn.ServerSelectionStrategy.ServerType == ServerType.Cluster; + Assert.Equal(IsCluster, isCluster); // check our assumptions! + + // we expect explicit null and default to act the same, but: check + RedisKey key = explicitNull ? RedisKey.Null : default(RedisKey); + + IServer primary = conn.GetServer(key); + Assert.False(primary.IsReplica); + + IServer replica = conn.GetServer(key, flags: CommandFlags.DemandReplica); + Assert.True(replica.IsReplica); + + // check multiple calls + HashSet uniques = []; + for (int i = 0; i < 100; i++) + { + uniques.Add(conn.GetServer(key)); + } + + if (isCluster) + { + Assert.True(uniques.Count > 1); // should be able to get arbitrary servers + } + else + { + Assert.Single(uniques); + } + + uniques.Clear(); + for (int i = 0; i < 100; i++) + { + uniques.Add(conn.GetServer(key, flags: CommandFlags.DemandReplica)); + } + + if (isCluster) + { + Assert.True(uniques.Count > 1); // should be able to get arbitrary servers + } + else + { + Assert.Single(uniques); + } + } +} + +[RunPerProtocol] +public class GetServerTestsCluster(ITestOutputHelper output, SharedConnectionFixture fixture) : GetServerTestsBase(output, fixture) +{ + protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts; + + protected override bool IsCluster => true; +} + +[RunPerProtocol] +public class GetServerTestsStandalone(ITestOutputHelper output, SharedConnectionFixture fixture) : GetServerTestsBase(output, fixture) +{ + protected override string GetConfiguration() => // we want to test flags usage including replicas + TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; + + protected override bool IsCluster => false; +} diff --git a/tests/StackExchange.Redis.Tests/KeyTests.cs b/tests/StackExchange.Redis.Tests/KeyTests.cs index 31cd87d79..e956af4ff 100644 --- a/tests/StackExchange.Redis.Tests/KeyTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyTests.cs @@ -182,8 +182,8 @@ public async Task KeyEncoding() db.KeyDelete(key, CommandFlags.FireAndForget); db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - Assert.Equal("embstr", db.KeyEncoding(key)); - Assert.Equal("embstr", await db.KeyEncodingAsync(key)); + Assert.True(db.KeyEncoding(key) is "embstr" or "raw"); // server-version dependent + Assert.True(await db.KeyEncodingAsync(key) is "embstr" or "raw"); db.KeyDelete(key, CommandFlags.FireAndForget); db.ListLeftPush(key, "new value", flags: CommandFlags.FireAndForget); diff --git a/tests/StackExchange.Redis.Tests/StreamTests.cs b/tests/StackExchange.Redis.Tests/StreamTests.cs index 196913f40..58d2bb1fb 100644 --- a/tests/StackExchange.Redis.Tests/StreamTests.cs +++ b/tests/StackExchange.Redis.Tests/StreamTests.cs @@ -2155,6 +2155,28 @@ public async Task AddWithApproxCount(StreamTrimMode mode) db.StreamAdd(key, "field", "value", maxLength: 10, useApproximateMaxLength: true, trimMode: mode, flags: CommandFlags.None); } + [Theory] + [InlineData(StreamTrimMode.KeepReferences, 1)] + [InlineData(StreamTrimMode.DeleteReferences, 1)] + [InlineData(StreamTrimMode.Acknowledged, 1)] + [InlineData(StreamTrimMode.KeepReferences, 2)] + [InlineData(StreamTrimMode.DeleteReferences, 2)] + [InlineData(StreamTrimMode.Acknowledged, 2)] + public async Task AddWithMultipleApproxCount(StreamTrimMode mode, int count) + { + await using var conn = Create(require: ForMode(mode)); + + var db = conn.GetDatabase(); + var key = Me() + ":" + mode; + + var pairs = new NameValueEntry[count]; + for (var i = 0; i < count; i++) + { + pairs[i] = new NameValueEntry($"field{i}", $"value{i}"); + } + db.StreamAdd(key, maxLength: 10, useApproximateMaxLength: true, trimMode: mode, flags: CommandFlags.None, streamPairs: pairs); + } + [Fact] public async Task StreamReadGroupWithNoAckShowsNoPendingMessages() { From d6b76870b1cd3fc87e3daf0e21e3dcdf7725fa23 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 26 Sep 2025 13:13:12 +0100 Subject: [PATCH 082/108] more bad merge --- src/StackExchange.Redis/RedisDatabase.cs | 12 +++++++----- src/StackExchange.Redis/ServerEndPoint.cs | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 8e4a20bcd..227a7d127 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -4602,9 +4602,11 @@ private Message GetStreamAddMessage(RedisKey key, RedisValue entryId, long? maxL var includeApproxLen = maxLength.HasValue && useApproximateMaxLength ? 1 : 0; var totalLength = (streamPairs.Length * 2) // Room for the name/value pairs - + 1 // The stream entry ID - + includeMaxLen // 2 or 0 (MAXLEN keyword & the count) - + includeApproxLen; // 1 or 0 + + 1 // The stream entry ID + + (maxLength.HasValue ? 2 : 0) // MAXLEN N + + (maxLength.HasValue && useApproximateMaxLength ? 1 : 0) // ~ + + (mode == StreamTrimMode.KeepReferences ? 0 : 1) // relevant trim-mode keyword + + (limit.HasValue ? 2 : 0); // LIMIT N var values = new RedisValue[totalLength]; @@ -5047,7 +5049,7 @@ private Message GetStringSetMessage( When when = When.Always, CommandFlags flags = CommandFlags.None) { - when.AlwaysOrExists(); + when.AlwaysOrExistsOrNotExists(); if (value.IsNull) return Message.Create(Database, flags, RedisCommand.DEL, key); if (expiry == null || expiry.Value == TimeSpan.MaxValue) @@ -5096,7 +5098,7 @@ private Message GetStringSetAndGetMessage( When when = When.Always, CommandFlags flags = CommandFlags.None) { - when.AlwaysOrExists(); + when.AlwaysOrExistsOrNotExists(); if (value.IsNull) return Message.Create(Database, flags, RedisCommand.GETDEL, key); if (expiry == null || expiry.Value == TimeSpan.MaxValue) diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 06eaae4d9..f856a5b21 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -74,8 +74,8 @@ public ServerEndPoint(ConnectionMultiplexer multiplexer, EndPoint endpoint) private RedisServer? _defaultServer; public RedisServer GetRedisServer(object? asyncState) => asyncState is null - ? (_defaultServer ??= new RedisServer(this, null)) // reuse and memoize - : new RedisServer(this, asyncState); + ? (_defaultServer ??= new RedisServer(this, null)) // reuse and memoize + : new RedisServer(this, asyncState); public EndPoint EndPoint { get; } From 6bfab91019dd093084a85ddae943060cd3303498 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 26 Sep 2025 13:17:43 +0100 Subject: [PATCH 083/108] more drift --- .../Interfaces/IConnectionMultiplexer.cs | 613 +++++++++--------- src/StackExchange.Redis/Interfaces/IServer.cs | 10 +- 2 files changed, 311 insertions(+), 312 deletions(-) diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs index 15e6c1e2a..96b4ce8f6 100644 --- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs @@ -8,312 +8,311 @@ using StackExchange.Redis.Profiling; using static StackExchange.Redis.ConnectionMultiplexer; -namespace StackExchange.Redis +namespace StackExchange.Redis; + +internal interface IInternalConnectionMultiplexer : IConnectionMultiplexer { - internal interface IInternalConnectionMultiplexer : IConnectionMultiplexer - { - bool AllowConnect { get; set; } - - bool IgnoreConnect { get; set; } - - ReadOnlySpan GetServerSnapshot(); - ServerEndPoint GetServerEndPoint(EndPoint endpoint); - - ConfigurationOptions RawConfig { get; } - - long? GetConnectionId(EndPoint endPoint, ConnectionType type); - - ServerSelectionStrategy ServerSelectionStrategy { get; } - - int GetSubscriptionsCount(); - ConcurrentDictionary GetSubscriptions(); - - ConnectionMultiplexer UnderlyingMultiplexer { get; } - } - - /// - /// Represents the abstract multiplexer API. - /// - public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable - { - /// - /// Gets the client-name that will be used on all new connections. - /// - string ClientName { get; } - - /// - /// Gets the configuration of the connection. - /// - string Configuration { get; } - - /// - /// Gets the timeout associated with the connections. - /// - int TimeoutMilliseconds { get; } - - /// - /// The number of operations that have been performed on all connections. - /// - long OperationCount { get; } - - /// - /// Gets or sets whether asynchronous operations should be invoked in a way that guarantees their original delivery order. - /// - [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue), false)] - [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - bool PreserveAsyncOrder { get; set; } - - /// - /// Indicates whether any servers are connected. - /// - bool IsConnected { get; } - - /// - /// Indicates whether any servers are connecting. - /// - bool IsConnecting { get; } - - /// - /// Should exceptions include identifiable details? (key names, additional annotations). - /// - [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludeDetailInExceptions)} instead - this will be removed in 3.0.")] - [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - bool IncludeDetailInExceptions { get; set; } - - /// - /// Limit at which to start recording unusual busy patterns (only one log will be retained at a time. - /// Set to a negative value to disable this feature). - /// - int StormLogThreshold { get; set; } - - /// - /// Register a callback to provide an on-demand ambient session provider based on the calling context. - /// The implementing code is responsible for reliably resolving the same provider - /// based on ambient context, or returning null to not profile. - /// - /// The profiling session provider. - void RegisterProfiler(Func profilingSessionProvider); - - /// - /// Get summary statistics associates with this server. - /// - ServerCounters GetCounters(); - - /// - /// A server replied with an error message. - /// - event EventHandler ErrorMessage; - - /// - /// Raised whenever a physical connection fails. - /// - event EventHandler ConnectionFailed; - - /// - /// Raised whenever an internal error occurs (this is primarily for debugging). - /// - event EventHandler InternalError; - - /// - /// Raised whenever a physical connection is established. - /// - event EventHandler ConnectionRestored; - - /// - /// Raised when configuration changes are detected. - /// - event EventHandler ConfigurationChanged; - - /// - /// Raised when nodes are explicitly requested to reconfigure via broadcast. - /// This usually means primary/replica changes. - /// - event EventHandler ConfigurationChangedBroadcast; - - /// - /// Raised when server indicates a maintenance event is going to happen. - /// - event EventHandler ServerMaintenanceEvent; - - /// - /// Gets all endpoints defined on the multiplexer. - /// - /// Whether to return only the explicitly configured endpoints. - EndPoint[] GetEndPoints(bool configuredOnly = false); - - /// - /// Wait for a given asynchronous operation to complete (or timeout). - /// - /// The task to wait on. - void Wait(Task task); - - /// - /// Wait for a given asynchronous operation to complete (or timeout). - /// - /// The type in . - /// The task to wait on. - T Wait(Task task); - - /// - /// Wait for the given asynchronous operations to complete (or timeout). - /// - /// The tasks to wait on. - void WaitAll(params Task[] tasks); - - /// - /// Raised when a hash-slot has been relocated. - /// - event EventHandler HashSlotMoved; - - /// - /// Compute the hash-slot of a specified key. - /// - /// The key to get a slot ID for. - int HashSlot(RedisKey key); - - /// - /// Obtain a pub/sub subscriber connection to the specified server. - /// - /// The async state to pass to the created . - ISubscriber GetSubscriber(object? asyncState = null); - - /// - /// Obtain an interactive connection to a database inside redis. - /// - /// The database ID to get. - /// The async state to pass to the created . - IDatabase GetDatabase(int db = -1, object? asyncState = null); - - /// - /// Obtain a configuration API for an individual server. - /// - /// The host to get a server for. - /// The specific port for to get a server for. - /// The async state to pass to the created . - IServer GetServer(string host, int port, object? asyncState = null); - - /// - /// Obtain a configuration API for an individual server. - /// - /// The "host:port" string to get a server for. - /// The async state to pass to the created . - IServer GetServer(string hostAndPort, object? asyncState = null); - - /// - /// Obtain a configuration API for an individual server. - /// - /// The host to get a server for. - /// The specific port for to get a server for. - IServer GetServer(IPAddress host, int port); - - /// - /// Obtain a configuration API for an individual server. - /// - /// The endpoint to get a server for. - /// The async state to pass to the created . - IServer GetServer(EndPoint endpoint, object? asyncState = null); - - /// - /// Gets a server that would be used for a given key and flags. - /// - /// The endpoint to get a server for. In a non-cluster environment, this parameter is ignored. A key may be specified - /// on cluster, which will return a connection to an arbitrary server matching the specified flags. - /// The async state to pass to the created . - /// The command flags to use. - /// This method is particularly useful when communicating with a cluster environment, to obtain a connection to the server that owns the specified key - /// and ad-hoc commands with unusual routing requirements. Note that provides a connection that automatically routes commands by - /// looking for parameters, so this method is only necessary when used with commands that do not take a parameter, - /// but require consistent routing using key-like semantics. - IServer GetServer(RedisKey key, object? asyncState = null, CommandFlags flags = CommandFlags.None); - - /// - /// Obtain configuration APIs for all servers in this multiplexer. - /// - IServer[] GetServers(); - - /// - /// Reconfigure the current connections based on the existing configuration. - /// - /// The log to write output to. - Task ConfigureAsync(TextWriter? log = null); - - /// - /// Reconfigure the current connections based on the existing configuration. - /// - /// The log to write output to. - bool Configure(TextWriter? log = null); - - /// - /// Provides a text overview of the status of all connections. - /// - string GetStatus(); - - /// - /// Provides a text overview of the status of all connections. - /// - /// The log to write output to. - void GetStatus(TextWriter log); - - /// - /// See . - /// - string ToString(); - - /// - /// Close all connections and release all resources associated with this object. - /// - /// Whether to allow in-queue commands to complete first. - void Close(bool allowCommandsToComplete = true); - - /// - /// Close all connections and release all resources associated with this object. - /// - /// Whether to allow in-queue commands to complete first. - Task CloseAsync(bool allowCommandsToComplete = true); - - /// - /// Obtains the log of unusual busy patterns. - /// - string? GetStormLog(); - - /// - /// Resets the log of unusual busy patterns. - /// - void ResetStormLog(); - - /// - /// Request all compatible clients to reconfigure or reconnect. - /// - /// The command flags to use. - /// The number of instances known to have received the message (however, the actual number can be higher; returns -1 if the operation is pending). - long PublishReconfigure(CommandFlags flags = CommandFlags.None); - - /// - /// Request all compatible clients to reconfigure or reconnect. - /// - /// The command flags to use. - /// The number of instances known to have received the message (however, the actual number can be higher). - Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None); - - /// - /// Get the hash-slot associated with a given key, if applicable; this can be useful for grouping operations. - /// - /// The key to get a the slot for. - int GetHashSlot(RedisKey key); - - /// - /// Write the configuration of all servers to an output stream. - /// - /// The destination stream to write the export to. - /// The options to use for this export. - void ExportConfiguration(Stream destination, ExportOptions options = ExportOptions.All); - - /// - /// Append a usage-specific modifier to the advertised library name; suffixes are de-duplicated - /// and sorted alphabetically (so adding 'a', 'b' and 'a' will result in suffix '-a-b'). - /// Connections will be updated as necessary (RESP2 subscription - /// connections will not show updates until those connections next connect). - /// - void AddLibraryNameSuffix(string suffix); - } + bool AllowConnect { get; set; } + + bool IgnoreConnect { get; set; } + + ReadOnlySpan GetServerSnapshot(); + ServerEndPoint GetServerEndPoint(EndPoint endpoint); + + ConfigurationOptions RawConfig { get; } + + long? GetConnectionId(EndPoint endPoint, ConnectionType type); + + ServerSelectionStrategy ServerSelectionStrategy { get; } + + int GetSubscriptionsCount(); + ConcurrentDictionary GetSubscriptions(); + + ConnectionMultiplexer UnderlyingMultiplexer { get; } +} + +/// +/// Represents the abstract multiplexer API. +/// +public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable +{ + /// + /// Gets the client-name that will be used on all new connections. + /// + string ClientName { get; } + + /// + /// Gets the configuration of the connection. + /// + string Configuration { get; } + + /// + /// Gets the timeout associated with the connections. + /// + int TimeoutMilliseconds { get; } + + /// + /// The number of operations that have been performed on all connections. + /// + long OperationCount { get; } + + /// + /// Gets or sets whether asynchronous operations should be invoked in a way that guarantees their original delivery order. + /// + [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue), false)] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + bool PreserveAsyncOrder { get; set; } + + /// + /// Indicates whether any servers are connected. + /// + bool IsConnected { get; } + + /// + /// Indicates whether any servers are connecting. + /// + bool IsConnecting { get; } + + /// + /// Should exceptions include identifiable details? (key names, additional annotations). + /// + [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludeDetailInExceptions)} instead - this will be removed in 3.0.")] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + bool IncludeDetailInExceptions { get; set; } + + /// + /// Limit at which to start recording unusual busy patterns (only one log will be retained at a time. + /// Set to a negative value to disable this feature). + /// + int StormLogThreshold { get; set; } + + /// + /// Register a callback to provide an on-demand ambient session provider based on the calling context. + /// The implementing code is responsible for reliably resolving the same provider + /// based on ambient context, or returning null to not profile. + /// + /// The profiling session provider. + void RegisterProfiler(Func profilingSessionProvider); + + /// + /// Get summary statistics associates with this server. + /// + ServerCounters GetCounters(); + + /// + /// A server replied with an error message. + /// + event EventHandler ErrorMessage; + + /// + /// Raised whenever a physical connection fails. + /// + event EventHandler ConnectionFailed; + + /// + /// Raised whenever an internal error occurs (this is primarily for debugging). + /// + event EventHandler InternalError; + + /// + /// Raised whenever a physical connection is established. + /// + event EventHandler ConnectionRestored; + + /// + /// Raised when configuration changes are detected. + /// + event EventHandler ConfigurationChanged; + + /// + /// Raised when nodes are explicitly requested to reconfigure via broadcast. + /// This usually means primary/replica changes. + /// + event EventHandler ConfigurationChangedBroadcast; + + /// + /// Raised when server indicates a maintenance event is going to happen. + /// + event EventHandler ServerMaintenanceEvent; + + /// + /// Gets all endpoints defined on the multiplexer. + /// + /// Whether to return only the explicitly configured endpoints. + EndPoint[] GetEndPoints(bool configuredOnly = false); + + /// + /// Wait for a given asynchronous operation to complete (or timeout). + /// + /// The task to wait on. + void Wait(Task task); + + /// + /// Wait for a given asynchronous operation to complete (or timeout). + /// + /// The type in . + /// The task to wait on. + T Wait(Task task); + + /// + /// Wait for the given asynchronous operations to complete (or timeout). + /// + /// The tasks to wait on. + void WaitAll(params Task[] tasks); + + /// + /// Raised when a hash-slot has been relocated. + /// + event EventHandler HashSlotMoved; + + /// + /// Compute the hash-slot of a specified key. + /// + /// The key to get a slot ID for. + int HashSlot(RedisKey key); + + /// + /// Obtain a pub/sub subscriber connection to the specified server. + /// + /// The async state to pass to the created . + ISubscriber GetSubscriber(object? asyncState = null); + + /// + /// Obtain an interactive connection to a database inside redis. + /// + /// The database ID to get. + /// The async state to pass to the created . + IDatabase GetDatabase(int db = -1, object? asyncState = null); + + /// + /// Obtain a configuration API for an individual server. + /// + /// The host to get a server for. + /// The specific port for to get a server for. + /// The async state to pass to the created . + IServer GetServer(string host, int port, object? asyncState = null); + + /// + /// Obtain a configuration API for an individual server. + /// + /// The "host:port" string to get a server for. + /// The async state to pass to the created . + IServer GetServer(string hostAndPort, object? asyncState = null); + + /// + /// Obtain a configuration API for an individual server. + /// + /// The host to get a server for. + /// The specific port for to get a server for. + IServer GetServer(IPAddress host, int port); + + /// + /// Obtain a configuration API for an individual server. + /// + /// The endpoint to get a server for. + /// The async state to pass to the created . + IServer GetServer(EndPoint endpoint, object? asyncState = null); + + /// + /// Gets a server that would be used for a given key and flags. + /// + /// The endpoint to get a server for. In a non-cluster environment, this parameter is ignored. A key may be specified + /// on cluster, which will return a connection to an arbitrary server matching the specified flags. + /// The async state to pass to the created . + /// The command flags to use. + /// This method is particularly useful when communicating with a cluster environment, to obtain a connection to the server that owns the specified key + /// and ad-hoc commands with unusual routing requirements. Note that provides a connection that automatically routes commands by + /// looking for parameters, so this method is only necessary when used with commands that do not take a parameter, + /// but require consistent routing using key-like semantics. + IServer GetServer(RedisKey key, object? asyncState = null, CommandFlags flags = CommandFlags.None); + + /// + /// Obtain configuration APIs for all servers in this multiplexer. + /// + IServer[] GetServers(); + + /// + /// Reconfigure the current connections based on the existing configuration. + /// + /// The log to write output to. + Task ConfigureAsync(TextWriter? log = null); + + /// + /// Reconfigure the current connections based on the existing configuration. + /// + /// The log to write output to. + bool Configure(TextWriter? log = null); + + /// + /// Provides a text overview of the status of all connections. + /// + string GetStatus(); + + /// + /// Provides a text overview of the status of all connections. + /// + /// The log to write output to. + void GetStatus(TextWriter log); + + /// + /// See . + /// + string ToString(); + + /// + /// Close all connections and release all resources associated with this object. + /// + /// Whether to allow in-queue commands to complete first. + void Close(bool allowCommandsToComplete = true); + + /// + /// Close all connections and release all resources associated with this object. + /// + /// Whether to allow in-queue commands to complete first. + Task CloseAsync(bool allowCommandsToComplete = true); + + /// + /// Obtains the log of unusual busy patterns. + /// + string? GetStormLog(); + + /// + /// Resets the log of unusual busy patterns. + /// + void ResetStormLog(); + + /// + /// Request all compatible clients to reconfigure or reconnect. + /// + /// The command flags to use. + /// The number of instances known to have received the message (however, the actual number can be higher; returns -1 if the operation is pending). + long PublishReconfigure(CommandFlags flags = CommandFlags.None); + + /// + /// Request all compatible clients to reconfigure or reconnect. + /// + /// The command flags to use. + /// The number of instances known to have received the message (however, the actual number can be higher). + Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None); + + /// + /// Get the hash-slot associated with a given key, if applicable; this can be useful for grouping operations. + /// + /// The key to get a the slot for. + int GetHashSlot(RedisKey key); + + /// + /// Write the configuration of all servers to an output stream. + /// + /// The destination stream to write the export to. + /// The options to use for this export. + void ExportConfiguration(Stream destination, ExportOptions options = ExportOptions.All); + + /// + /// Append a usage-specific modifier to the advertised library name; suffixes are de-duplicated + /// and sorted alphabetically (so adding 'a', 'b' and 'a' will result in suffix '-a-b'). + /// Connections will be updated as necessary (RESP2 subscription + /// connections will not show updates until those connections next connect). + /// + void AddLibraryNameSuffix(string suffix); } diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index 952eafb9b..8e4178fc9 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -252,13 +252,10 @@ public partial interface IServer : IRedis /// Task EchoAsync(RedisValue message, CommandFlags flags = CommandFlags.None); -#pragma warning disable RS0026, RS0027 // multiple overloads /// /// Execute an arbitrary command against the server; this is primarily intended for /// executing modules, but may also be used to provide access to new features that lack - /// a direct API. The command is assumed to be not database-specific. If this is not the case, - /// should be used to - /// specify the database (using null to use the configured default database). + /// a direct API. /// /// The command to run. /// The arguments to pass for the command. @@ -269,10 +266,13 @@ public partial interface IServer : IRedis /// Task ExecuteAsync(string command, params object[] args); +#pragma warning disable RS0026, RS0027 // multiple overloads /// /// Execute an arbitrary command against the server; this is primarily intended for /// executing modules, but may also be used to provide access to new features that lack - /// a direct API. + /// a direct API. The command is assumed to be not database-specific. If this is not the case, + /// should be used to + /// specify the database (using null to use the configured default database). /// /// The command to run. /// The arguments to pass for the command. From 07897d293b06f4fff58e4c926278e1e6dd6a2bcd Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 26 Sep 2025 13:22:15 +0100 Subject: [PATCH 084/108] more drift --- tests/StackExchange.Redis.Tests/LoggerTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/LoggerTests.cs b/tests/StackExchange.Redis.Tests/LoggerTests.cs index 079887765..682856baa 100644 --- a/tests/StackExchange.Redis.Tests/LoggerTests.cs +++ b/tests/StackExchange.Redis.Tests/LoggerTests.cs @@ -52,7 +52,7 @@ public class TestWrapperLoggerFactory(ILogger logger) : ILoggerFactory { public TestWrapperLogger Logger { get; } = new TestWrapperLogger(logger); - public void AddProvider(ILoggerProvider provider) => throw new NotImplementedException(); + public void AddProvider(ILoggerProvider provider) { } public ILogger CreateLogger(string categoryName) => Logger; public void Dispose() { } } @@ -81,9 +81,9 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except private sealed class TestMultiLogger(params ILogger[] loggers) : ILogger { #if NET8_0_OR_GREATER - public IDisposable? BeginScope(TState state) where TState : notnull => throw new NotImplementedException(); + public IDisposable? BeginScope(TState state) where TState : notnull => null; #else - public IDisposable BeginScope(TState state) => throw new NotImplementedException(); + public IDisposable BeginScope(TState state) => null!; #endif public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) @@ -105,9 +105,9 @@ public TestLogger(LogLevel logLevel, TextWriter output) => (_logLevel, _output) = (logLevel, output); #if NET8_0_OR_GREATER - public IDisposable? BeginScope(TState state) where TState : notnull => throw new NotImplementedException(); + public IDisposable? BeginScope(TState state) where TState : notnull => null; #else - public IDisposable BeginScope(TState state) => throw new NotImplementedException(); + public IDisposable BeginScope(TState state) => null!; #endif public bool IsEnabled(LogLevel logLevel) => logLevel >= _logLevel; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) From 1545618f0e2a1491d06de148d2c6425dbfa15621 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 26 Sep 2025 13:26:05 +0100 Subject: [PATCH 085/108] more noise --- .../StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs index 3eff986b9..9656ee45b 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs @@ -198,7 +198,6 @@ public event EventHandler ServerMaintenanceEvent public IServer GetServer(EndPoint endpoint, object? asyncState = null) => _inner.GetServer(endpoint, asyncState); public IServer GetServer(RedisKey key, object? asyncState = null, CommandFlags flags = CommandFlags.None) => _inner.GetServer(key, asyncState, flags); - public IServer[] GetServers() => _inner.GetServers(); public string GetStatus() => _inner.GetStatus(); From 14a9aa3b5b1cd975986186a8bb7ec36aa21d6950 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 26 Sep 2025 16:30:28 +0100 Subject: [PATCH 086/108] use fixed qty in zpopmininit/lrangeinit --- src/RESPite.Benchmark/BenchmarkBase.cs | 6 ++++++ src/RESPite.Benchmark/NewCoreBenchmark.cs | 14 ++++---------- src/RESPite.Benchmark/OldCoreBenchmarkBase.cs | 6 ++---- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/RESPite.Benchmark/BenchmarkBase.cs b/src/RESPite.Benchmark/BenchmarkBase.cs index 86a391a9a..4828323e5 100644 --- a/src/RESPite.Benchmark/BenchmarkBase.cs +++ b/src/RESPite.Benchmark/BenchmarkBase.cs @@ -21,6 +21,12 @@ protected const string SortedSetKey = "myzset", StreamKey = "mystream"; + // how many elements to add for the LRANGE tests + protected const int ListElements = 650; + + // how many elements to add for the ZPOPMIN tests + protected const int SortedSetElements = 650; + public PipelineStrategy PipelineMode { get; } = PipelineStrategy.Batch; // the default, for parity with how redis-benchmark works diff --git a/src/RESPite.Benchmark/NewCoreBenchmark.cs b/src/RESPite.Benchmark/NewCoreBenchmark.cs index b443fe925..be0667db4 100644 --- a/src/RESPite.Benchmark/NewCoreBenchmark.cs +++ b/src/RESPite.Benchmark/NewCoreBenchmark.cs @@ -189,7 +189,7 @@ private ValueTask LPopInit(RespContext ctx) => private async ValueTask ZPopMinInit(RespContext ctx) { - int ops = TotalOperations; + int ops = SortedSetElements; var rand = new Random(); for (int i = 0; i < ops; i++) { @@ -201,20 +201,14 @@ await ctx.ZAddAsync(SortedSetKey, (rand.NextDouble() * 2000) - 1000, "element:__ [DisplayName("SPOP")] private ValueTask SPop(RespContext ctx) => ctx.SPopAsync(SetKey); - private async ValueTask SPopInit(RespContext ctx) - { - int ops = TotalOperations; - for (int i = 0; i < ops; i++) - { - await ctx.SAddAsync(SetKey, "element:__rand_int__").ConfigureAwait(false); - } - } + private ValueTask SPopInit(RespContext ctx) + => ctx.SAddAsync(SetKey, "element:__rand_int__").AsUntypedValueTask(); [DisplayName("MSET"), Description("10 keys")] private ValueTask MSet(RespContext ctx) => ctx.MSetAsync(_pairs); private ValueTask LRangeInit(RespContext ctx) => - ctx.LPushAsync(ListKey, Payload, TotalOperations).AsUntypedValueTask(); + ctx.LPushAsync(ListKey, Payload, ListElements).AsUntypedValueTask(); [DisplayName("XADD")] private ValueTask XAdd(RespContext ctx) => diff --git a/src/RESPite.Benchmark/OldCoreBenchmarkBase.cs b/src/RESPite.Benchmark/OldCoreBenchmarkBase.cs index cf37734fc..5a9c93c10 100644 --- a/src/RESPite.Benchmark/OldCoreBenchmarkBase.cs +++ b/src/RESPite.Benchmark/OldCoreBenchmarkBase.cs @@ -235,9 +235,8 @@ private async ValueTask HasSortedSetElement(Task pending) private async ValueTask ZPopMinInit(IDatabaseAsync client) { - int ops = TotalOperations; var rand = new Random(); - for (int i = 0; i < ops; i++) + for (int i = 0; i < SortedSetElements; i++) { await client.SortedSetAddAsync(SortedSetKey, "element:__rand_int__", (rand.NextDouble() * 2000) - 1000) .ConfigureAwait(false); @@ -269,8 +268,7 @@ private static ValueTask CountAsync(Task task) => task.ContinueWith private async ValueTask LRangeInit(IDatabaseAsync client) { - var ops = TotalOperations; - for (int i = 0; i < ops; i++) + for (int i = 0; i < ListElements; i++) { await client.ListLeftPushAsync(ListKey, Payload); } From 7f5de939b9338d358fd3a861e6cf5b620ded7821 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 27 Sep 2025 10:29:45 +0100 Subject: [PATCH 087/108] bench: improve init mechanism --- src/RESPite.Benchmark/BenchmarkBase.cs | 156 ++++++++++++------ src/RESPite.Benchmark/NewCoreBenchmark.cs | 77 +++++---- src/RESPite.Benchmark/OldCoreBenchmarkBase.cs | 82 ++++----- .../RespContextDatabase.List.cs | 7 +- 4 files changed, 191 insertions(+), 131 deletions(-) diff --git a/src/RESPite.Benchmark/BenchmarkBase.cs b/src/RESPite.Benchmark/BenchmarkBase.cs index 4828323e5..a8d31cbea 100644 --- a/src/RESPite.Benchmark/BenchmarkBase.cs +++ b/src/RESPite.Benchmark/BenchmarkBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -21,12 +22,6 @@ protected const string SortedSetKey = "myzset", StreamKey = "mystream"; - // how many elements to add for the LRANGE tests - protected const int ListElements = 650; - - // how many elements to add for the ZPOPMIN tests - protected const int SortedSetElements = 650; - public PipelineStrategy PipelineMode { get; } = PipelineStrategy.Batch; // the default, for parity with how redis-benchmark works @@ -53,9 +48,10 @@ public enum PipelineStrategy public bool Loop { get; } public bool Quiet { get; } public int ClientCount { get; } = 50; - public int OperationsPerClient { get; } + private int _operationsPerClient; + public int OperationsPerClient(int divisor = 1) => _operationsPerClient / divisor; - public int TotalOperations => OperationsPerClient * ClientCount; + public int TotalOperations(int divisor = 1) => OperationsPerClient(divisor) * ClientCount; protected readonly byte[] Payload; @@ -119,7 +115,7 @@ protected BenchmarkBase(string[] args) } } - OperationsPerClient = operations / ClientCount; + _operationsPerClient = operations / ClientCount; Payload = "abc"u8.ToArray(); } @@ -184,9 +180,10 @@ protected virtual void PrepareBatch(TClient client, int count) { } private async Task PipelineUntyped( TClient client, - Func operation) + Func operation, + int divisor) { - var opsPerClient = OperationsPerClient; + var opsPerClient = OperationsPerClient(divisor); int i = 0; try { @@ -258,9 +255,9 @@ private async Task PipelineUntyped( return DBNull.Value; } - private async Task PipelineTyped(TClient client, Func> operation) + private async Task PipelineTyped(TClient client, Func> operation, int divisor) { - var opsPerClient = OperationsPerClient; + var opsPerClient = OperationsPerClient(divisor); int i = 0; T result = default!; try @@ -350,31 +347,62 @@ public async Task InitAsync() protected Task RunAsync( string? key, Func> action, - Func? init = null, - string format = "") + bool deleteKey, + int divisor = 1) => RunAsyncCore( key, - action, + GetNameCore(action, out var desc), + desc, + client => action(client).AsUntypedValueTask(), + client => PipelineTyped(client, action, divisor), + [], + deleteKey, + divisor); + + protected Task RunAsync( + string? key, + Func> action, + params string[] consumers) + => RunAsyncCore( + key, + GetNameCore(action, out var desc), + desc, client => action(client).AsUntypedValueTask(), - client => PipelineTyped(client, action), - init, - format); + client => PipelineTyped(client, action, 1), + consumers, + consumers.Length != 0, + 1); - // ReSharper disable once UnusedMember.Global protected Task RunAsync( string? key, Func action, - Func? init = null, - string format = "") - => RunAsyncCore(key, action, action, client => PipelineUntyped(client, action), init, format); + bool deleteKey, + int divisor = 1) + => RunAsyncCore( + key, + GetNameCore(action, out var desc), + desc, + action, + client => PipelineUntyped(client, action, divisor), + [], + deleteKey, + divisor); - private async Task RunAsyncCore( + protected Task RunAsync( string? key, - Delegate underlyingAction, - Func test, - Func> pipeline, - Func? init = null, - string format = "") + Func action, + params string[] consumers) + => RunAsyncCore( + key, + GetNameCore(action, out var desc), + desc, + action, + client => PipelineUntyped(client, action, 1), + consumers, + consumers.Length != 0, + 1); + + private static string GetNameCore(Delegate underlyingAction, out string description) { string name = underlyingAction.Method.Name; @@ -386,17 +414,44 @@ private async Task RunAsyncCore( name = dna.DisplayName; } - // skip test if not needed - if (!RunTest(name)) return; - - // include additional test metadata - string description = ""; + description = ""; if (underlyingAction.Method.GetCustomAttribute(typeof(DescriptionAttribute)) is DescriptionAttribute { Description: { Length: > 0 } } da) { - description = $" ({da.Description})"; + description = da.Description; + } + + return name; + } + + protected static string GetName(Func> action) => GetNameCore(action, out _); + protected static string GetName(Func action) => GetNameCore(action, out _); + + private async Task RunAsyncCore( + string? key, + string name, + string description, + Func test, + Func> pipeline, + string[] consumers, + bool deleteKey, + int divisor) + { + // skip test if not needed + string auxReason = ""; + if (!RunTest(name)) + { + auxReason = string.Join(", ", consumers.Where(x => RunTest(x))); + if (auxReason.Length == 0) return; // not needed by any consumers either + auxReason = $" (required for {auxReason})"; + } + + // include additional test metadata + if (description is { Length: > 0 }) + { + description = $" ({description})"; } if (Quiet) @@ -406,7 +461,7 @@ private async Task RunAsyncCore( else { Console.Write( - $"====== {name}{description} ====== (clients: {ClientCount:#,##0}, ops: {TotalOperations:#,##0}"); + $"====== {name}{description}{auxReason} ====== (clients: {ClientCount:#,##0}, ops: {TotalOperations(divisor):#,##0}"); if (Multiplexed) { Console.Write(", mux"); @@ -425,7 +480,7 @@ private async Task RunAsyncCore( bool didNotRun = false; try { - if (key is not null) + if (key is not null && deleteKey) { await DeleteAsync(GetClient(0), key).ConfigureAwait(false); } @@ -441,11 +496,6 @@ private async Task RunAsyncCore( return; } - if (init is not null) - { - await init(GetClient(0)).ConfigureAwait(false); - } - var pending = new Task[ClientCount]; int index = 0; #if DEBUG @@ -465,14 +515,17 @@ private async Task RunAsyncCore( client = CreateBatch(client); } - pending[index++] = Task.Run(() => pipeline(WithCancellation(client, cancellationToken)), cancellationToken); + pending[index++] = Task.Run( + () => pipeline(WithCancellation(client, cancellationToken)), + cancellationToken); } await Task.WhenAll(pending).ConfigureAwait(false); watch.Stop(); var seconds = watch.Elapsed.TotalSeconds; - var rate = TotalOperations / seconds; + // ReSharper disable once PossibleLossOfFraction + var rate = TotalOperations(divisor) / seconds; if (Quiet) { Console.WriteLine($"\t{rate:###,###,##0} requests per second"); @@ -481,15 +534,12 @@ private async Task RunAsyncCore( else { Console.WriteLine( - $"{TotalOperations:###,###,##0} requests completed in {seconds:0.00} seconds, {rate:###,###,##0} ops/sec"); + $"{TotalOperations(divisor):###,###,##0} requests completed in {seconds:0.00} seconds, {rate:###,###,##0} ops/sec"); } if (!Quiet & typeof(T) != typeof(DBNull)) { - if (string.IsNullOrWhiteSpace(format)) - { - format = "Typical result: {0}"; - } + const string format = "Typical result: {0}"; T result = await pending[^1]; Console.WriteLine(format, result); @@ -575,9 +625,13 @@ private async Task RunAsyncCore( if (counters.BatchBufferLeaseCount != 0 | counters.BatchMultiRootMessageCount != 0) { - Console.Write($"Multi-message batching: {counters.BatchMultiRootMessageCount:#,###,##0} batches, {counters.BatchMultiChildMessageCount:#,###,##0} sub-messages"); + Console.Write( + $"Multi-message batching: {counters.BatchMultiRootMessageCount:#,###,##0} batches, {counters.BatchMultiChildMessageCount:#,###,##0} sub-messages"); if (counters.BatchBufferLeaseCount != 0) - Console.Write($"; {counters.BatchBufferLeaseCount:#,###,##0} blocks leased, {counters.BatchBufferReturnCount:#,###,##0} blocks returned, {counters.BatchBufferElementsOutstanding:#,###,##0} elements outstanding"); + { + Console.Write( + $"; {counters.BatchBufferLeaseCount:#,###,##0} blocks leased, {counters.BatchBufferReturnCount:#,###,##0} blocks returned, {counters.BatchBufferElementsOutstanding:#,###,##0} elements outstanding"); + } Console.WriteLine(); } diff --git a/src/RESPite.Benchmark/NewCoreBenchmark.cs b/src/RESPite.Benchmark/NewCoreBenchmark.cs index be0667db4..2ab93790a 100644 --- a/src/RESPite.Benchmark/NewCoreBenchmark.cs +++ b/src/RESPite.Benchmark/NewCoreBenchmark.cs @@ -86,26 +86,40 @@ public override async Task RunAll() // await RunAsync(PingInline).ConfigureAwait(false); await RunAsync(null, PingBulk).ConfigureAwait(false); - await RunAsync(GetSetKey, Set).ConfigureAwait(false); - await RunAsync(GetSetKey, Get, GetInit).ConfigureAwait(false); - await RunAsync(CounterKey, Incr).ConfigureAwait(false); - await RunAsync(ListKey, LPush).ConfigureAwait(false); - await RunAsync(ListKey, RPush).ConfigureAwait(false); - await RunAsync(ListKey, LPop, LPopInit).ConfigureAwait(false); - await RunAsync(ListKey, RPop, LPopInit).ConfigureAwait(false); - await RunAsync(SetKey, SAdd).ConfigureAwait(false); + await RunAsync(GetSetKey, Set, GetName(Get)).ConfigureAwait(false); + await RunAsync(GetSetKey, Get).ConfigureAwait(false); + + await RunAsync(CounterKey, Incr, true).ConfigureAwait(false); + + await RunAsync(ListKey, LPush, GetName(LPop)).ConfigureAwait(false); + await RunAsync(ListKey, LPop).ConfigureAwait(false); + + await RunAsync(ListKey, RPush, GetName(RPop)).ConfigureAwait(false); + await RunAsync(ListKey, RPop).ConfigureAwait(false); + + await RunAsync(SetKey, SAdd, GetName(SPop)).ConfigureAwait(false); + await RunAsync(SetKey, SPop).ConfigureAwait(false); + await RunAsync(HashKey, HSet).ConfigureAwait(false); - await RunAsync(SetKey, SPop, SPopInit).ConfigureAwait(false); - await RunAsync(SortedSetKey, ZAdd).ConfigureAwait(false); - await RunAsync(SortedSetKey, ZPopMin, ZPopMinInit).ConfigureAwait(false); + + await RunAsync(SortedSetKey, ZAdd, GetName(ZPopMin)).ConfigureAwait(false); + await RunAsync(SortedSetKey, ZPopMin).ConfigureAwait(false); + await RunAsync(null, MSet).ConfigureAwait(false); await RunAsync(StreamKey, XAdd).ConfigureAwait(false); // leave until last, they're slower - await RunAsync(ListKey, LRange100, LRangeInit).ConfigureAwait(false); - await RunAsync(ListKey, LRange300, LRangeInit).ConfigureAwait(false); - await RunAsync(ListKey, LRange500, LRangeInit).ConfigureAwait(false); - await RunAsync(ListKey, LRange600, LRangeInit).ConfigureAwait(false); + if (RunTest(GetName(LRange100)) || + RunTest(GetName(LRange300)) || + RunTest(GetName(LRange500)) || + RunTest(GetName(LRange600))) + { + await LRangeInit650(GetClient(0)).ConfigureAwait(false); + await RunAsync(ListKey, LRange100, false, 10).ConfigureAwait(false); + await RunAsync(ListKey, LRange300, false, 10).ConfigureAwait(false); + await RunAsync(ListKey, LRange500, false, 10).ConfigureAwait(false); + await RunAsync(ListKey, LRange600, false, 10).ConfigureAwait(false); + } await CleanupAsync().ConfigureAwait(false); } @@ -143,8 +157,6 @@ protected override void PrepareBatch(RespContext client, int count) [DisplayName("GET")] private ValueTask Get(RespContext ctx) => ctx.GetAsync(GetSetKey); - private ValueTask GetInit(RespContext ctx) => ctx.SetAsync(GetSetKey, Payload).AsUntypedValueTask(); - [DisplayName("SET")] private ValueTask Set(RespContext ctx) => ctx.SetAsync(GetSetKey, Payload); @@ -172,9 +184,6 @@ protected override void PrepareBatch(RespContext client, int count) [DisplayName("RPOP")] private ValueTask RPop(RespContext ctx) => ctx.RPopAsync(ListKey); - private ValueTask LPopInit(RespContext ctx) => - ctx.LPushAsync(ListKey, Payload, TotalOperations).AsUntypedValueTask(); - [DisplayName("SADD")] private ValueTask SAdd(RespContext ctx) => ctx.SAddAsync(SetKey, "element:__rand_int__"); @@ -187,28 +196,21 @@ private ValueTask LPopInit(RespContext ctx) => [DisplayName("ZPOPMIN")] private ValueTask ZPopMin(RespContext ctx) => ctx.ZPopMinAsync(SortedSetKey); - private async ValueTask ZPopMinInit(RespContext ctx) - { - int ops = SortedSetElements; - var rand = new Random(); - for (int i = 0; i < ops; i++) - { - await ctx.ZAddAsync(SortedSetKey, (rand.NextDouble() * 2000) - 1000, "element:__rand_int__") - .ConfigureAwait(false); - } - } - [DisplayName("SPOP")] private ValueTask SPop(RespContext ctx) => ctx.SPopAsync(SetKey); - private ValueTask SPopInit(RespContext ctx) - => ctx.SAddAsync(SetKey, "element:__rand_int__").AsUntypedValueTask(); - [DisplayName("MSET"), Description("10 keys")] private ValueTask MSet(RespContext ctx) => ctx.MSetAsync(_pairs); - private ValueTask LRangeInit(RespContext ctx) => - ctx.LPushAsync(ListKey, Payload, ListElements).AsUntypedValueTask(); + private async ValueTask LRangeInit650(RespContext ctx) + { + await ctx.DelAsync(ListKey).ConfigureAwait(false); + await ctx.LPushAsync(ListKey, Payload, 650); + if (await ctx.LLenAsync(ListKey).ConfigureAwait(false) != 650) + { + throw new InvalidOperationException(); + } + } [DisplayName("XADD")] private ValueTask XAdd(RespContext ctx) => @@ -311,6 +313,9 @@ internal static partial class RedisCommands [RespCommand] internal static partial RespParsers.ResponseSummary Set(this in RespContext ctx, string key, byte[] payload); + [RespCommand] + internal static partial int LLen(this in RespContext ctx, string key); + [RespCommand] internal static partial int LPush(this in RespContext ctx, string key, byte[] payload); diff --git a/src/RESPite.Benchmark/OldCoreBenchmarkBase.cs b/src/RESPite.Benchmark/OldCoreBenchmarkBase.cs index 5a9c93c10..0bab0ea4c 100644 --- a/src/RESPite.Benchmark/OldCoreBenchmarkBase.cs +++ b/src/RESPite.Benchmark/OldCoreBenchmarkBase.cs @@ -52,26 +52,40 @@ public override async Task RunAll() // await RunAsync(PingInline).ConfigureAwait(false); await RunAsync(null, PingBulk).ConfigureAwait(false); - await RunAsync(GetSetKey, Set).ConfigureAwait(false); - await RunAsync(GetSetKey, Get, GetInit).ConfigureAwait(false); - await RunAsync(CounterKey, Incr).ConfigureAwait(false); - await RunAsync(ListKey, LPush).ConfigureAwait(false); - await RunAsync(ListKey, RPush).ConfigureAwait(false); - await RunAsync(ListKey, LPop, LPopInit).ConfigureAwait(false); - await RunAsync(ListKey, RPop, LPopInit).ConfigureAwait(false); - await RunAsync(SetKey, SAdd).ConfigureAwait(false); + await RunAsync(GetSetKey, Set, GetName(Get)).ConfigureAwait(false); + await RunAsync(GetSetKey, Get).ConfigureAwait(false); + + await RunAsync(CounterKey, Incr, true).ConfigureAwait(false); + + await RunAsync(ListKey, LPush, GetName(LPop)).ConfigureAwait(false); + await RunAsync(ListKey, LPop).ConfigureAwait(false); + + await RunAsync(ListKey, RPush, GetName(RPop)).ConfigureAwait(false); + await RunAsync(ListKey, RPop).ConfigureAwait(false); + + await RunAsync(SetKey, SAdd, GetName(SPop)).ConfigureAwait(false); + await RunAsync(SetKey, SPop).ConfigureAwait(false); + await RunAsync(HashKey, HSet).ConfigureAwait(false); - await RunAsync(SetKey, SPop, SPopInit).ConfigureAwait(false); - await RunAsync(SortedSetKey, ZAdd).ConfigureAwait(false); - await RunAsync(SortedSetKey, ZPopMin, ZPopMinInit).ConfigureAwait(false); + + await RunAsync(SortedSetKey, ZAdd, GetName(ZPopMin)).ConfigureAwait(false); + await RunAsync(SortedSetKey, ZPopMin).ConfigureAwait(false); + await RunAsync(null, MSet).ConfigureAwait(false); await RunAsync(StreamKey, XAdd).ConfigureAwait(false); // leave until last, they're slower - await RunAsync(ListKey, LRange100, LRangeInit).ConfigureAwait(false); - await RunAsync(ListKey, LRange300, LRangeInit).ConfigureAwait(false); - await RunAsync(ListKey, LRange500, LRangeInit).ConfigureAwait(false); - await RunAsync(ListKey, LRange600, LRangeInit).ConfigureAwait(false); + if (RunTest(GetName(LRange100)) || + RunTest(GetName(LRange300)) || + RunTest(GetName(LRange500)) || + RunTest(GetName(LRange600))) + { + await LRangeInit650(GetClient(0)).ConfigureAwait(false); + await RunAsync(ListKey, LRange100, false, 10).ConfigureAwait(false); + await RunAsync(ListKey, LRange300, false, 10).ConfigureAwait(false); + await RunAsync(ListKey, LRange500, false, 10).ConfigureAwait(false); + await RunAsync(ListKey, LRange600, false, 10).ConfigureAwait(false); + } await CleanupAsync().ConfigureAwait(false); } @@ -180,11 +194,6 @@ private async ValueTask GetAndMeasureString(IDatabaseAsync client) [DisplayName("SET")] private ValueTask Set(IDatabaseAsync client) => client.StringSetAsync(GetSetKey, Payload).AsValueTask(); - private ValueTask GetInit(IDatabaseAsync client) => - client.StringSetAsync(GetSetKey, Payload).AsUntypedValueTask(); - - private ValueTask PingInline(IDatabaseAsync client) => client.PingAsync().AsValueTask(); - [DisplayName("PING_BULK")] private ValueTask PingBulk(IDatabaseAsync client) => client.PingAsync().AsValueTask(); @@ -211,15 +220,9 @@ private ValueTask SAdd(IDatabaseAsync client) => [DisplayName("RPOP")] private ValueTask RPop(IDatabaseAsync client) => client.ListRightPopAsync(ListKey).AsValueTask(); - private ValueTask LPopInit(IDatabaseAsync client) => - client.ListLeftPushAsync(ListKey, Payload).AsUntypedValueTask(); - [DisplayName("SPOP")] private ValueTask SPop(IDatabaseAsync client) => client.SetPopAsync(SetKey).AsValueTask(); - private ValueTask SPopInit(IDatabaseAsync client) => - client.SetAddAsync(SetKey, "element:__rand_int__").AsUntypedValueTask(); - [DisplayName("ZADD")] private ValueTask ZAdd(IDatabaseAsync client) => client.SortedSetAddAsync(SortedSetKey, "element:__rand_int__", 0).AsValueTask(); @@ -233,16 +236,6 @@ private async ValueTask HasSortedSetElement(Task pending) return result.HasValue ? 1 : 0; } - private async ValueTask ZPopMinInit(IDatabaseAsync client) - { - var rand = new Random(); - for (int i = 0; i < SortedSetElements; i++) - { - await client.SortedSetAddAsync(SortedSetKey, "element:__rand_int__", (rand.NextDouble() * 2000) - 1000) - .ConfigureAwait(false); - } - } - [DisplayName("MSET")] private ValueTask MSet(IDatabaseAsync client) => client.StringSetAsync(_pairs).AsValueTask(); @@ -266,11 +259,19 @@ private ValueTask LRange600(IDatabaseAsync client) => private static ValueTask CountAsync(Task task) => task.ContinueWith( t => t.Result.Length, TaskContinuationOptions.ExecuteSynchronously).AsValueTask(); - private async ValueTask LRangeInit(IDatabaseAsync client) + private async ValueTask LRangeInit650(IDatabaseAsync client) { - for (int i = 0; i < ListElements; i++) + var batch = CreateBatch(client); + _ = batch.KeyDeleteAsync(ListKey, flags: CommandFlags.FireAndForget); + for (int i = 0; i < 650; i++) { - await client.ListLeftPushAsync(ListKey, Payload); + _ = batch.ListLeftPushAsync(ListKey, Payload, flags: CommandFlags.FireAndForget); + } + + await Flush(batch).ConfigureAwait(false); + if (await client.ListLengthAsync(ListKey).ConfigureAwait(false) != 650) + { + throw new InvalidOperationException(); } } } @@ -278,8 +279,11 @@ private async ValueTask LRangeInit(IDatabaseAsync client) internal static class TaskExtensions { public static ValueTask AsValueTask(this Task task) => new(task); + + /* public static ValueTask AsUntypedValueTask(this Task task) => new(task); public static ValueTask AsValueTask(this Task task) => new(task); + */ public static ValueTask AsUntypedValueTask(this ValueTask task) { diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.List.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.List.cs index b261080e5..b39d72749 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.List.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.List.cs @@ -66,9 +66,6 @@ public Task ListLeftPushAsync( public Task ListLeftPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task ListLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - public Task ListMoveAsync( RedisKey sourceKey, RedisKey destinationKey, @@ -204,8 +201,8 @@ public long ListLeftPush( public long ListLeftPush(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public long ListLength(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [RespCommand("LLEN")] + public partial long ListLength(RedisKey key, CommandFlags flags = CommandFlags.None); public RedisValue ListMove( RedisKey sourceKey, From 2aa0fe7f92e20004f94befc311b027287faf34fc Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 1 Oct 2025 16:14:50 +0100 Subject: [PATCH 088/108] key commands --- .../RespCommandGenerator.cs | 77 ++++++- .../RedisCommands.KeyCommands.cs | 202 ++++++++++++++++++ .../RedisCommands.Server.cs | 19 -- .../RedisCommands.ServerCommands.cs | 19 ++ .../RedisCommands.StringCommands.cs | 22 ++ .../RedisCommands.Strings.cs | 19 -- .../RespContextDatabase.Key.cs | 92 ++++---- .../RespFormatters.cs | 12 +- .../RespParsers.cs | 31 ++- src/RESPite/RespKeyAttribute.cs | 10 + src/StackExchange.Redis/RedisBase.cs | 6 + src/StackExchange.Redis/RedisDatabase.cs | 2 +- 12 files changed, 419 insertions(+), 92 deletions(-) create mode 100644 src/RESPite.StackExchange.Redis/RedisCommands.KeyCommands.cs delete mode 100644 src/RESPite.StackExchange.Redis/RedisCommands.Server.cs create mode 100644 src/RESPite.StackExchange.Redis/RedisCommands.ServerCommands.cs create mode 100644 src/RESPite.StackExchange.Redis/RedisCommands.StringCommands.cs delete mode 100644 src/RESPite.StackExchange.Redis/RedisCommands.Strings.cs create mode 100644 src/RESPite/RespKeyAttribute.cs diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index 61eb63f8a..978fddcc7 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -70,7 +70,11 @@ private readonly record struct MethodTuple( string Context, string? Formatter, string? Parser, - string DebugNotes); + MethodFlags Flags, + string DebugNotes) + { + public bool IsRespOperation => (Flags & MethodFlags.RespOperation) != 0; + } private static string GetFullName(ITypeSymbol type) => type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); @@ -82,6 +86,7 @@ private enum RESPite RespKeyAttribute, RespPrefixAttribute, RespSuffixAttribute, + RespOperation, } private static bool IsRESPite(ITypeSymbol? symbol, RESPite type) @@ -93,6 +98,7 @@ private static bool IsRESPite(ITypeSymbol? symbol, RESPite type) RESPite.RespKeyAttribute => nameof(RESPite.RespKeyAttribute), RESPite.RespPrefixAttribute => nameof(RESPite.RespPrefixAttribute), RESPite.RespSuffixAttribute => nameof(RESPite.RespSuffixAttribute), + RESPite.RespOperation => nameof(RESPite.RespOperation), _ => type.ToString(), }; @@ -187,6 +193,7 @@ private MethodTuple Transform( if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not IMethodSymbol method) return default; if (!(method is { IsPartialDefinition: true, PartialImplementationPart: null })) return default; + MethodFlags methodFlags = 0; string returnType, debugNote = ""; if (method.ReturnsVoid) { @@ -199,7 +206,12 @@ private MethodTuple Transform( } else { - returnType = GetFullName(method.ReturnType); + ITypeSymbol? rt = method.ReturnType; + if (IsRespOperation(ref rt)) + { + methodFlags |= MethodFlags.RespOperation; + } + returnType = rt is null ? "" : GetFullName(rt); } string ns = "", parentType = ""; @@ -380,8 +392,16 @@ static bool IsIndirectRespContext(ITypeSymbol type, out string memberName) if (IsSERedis(param.Type, SERedis.CommandFlags)) { flags |= ParameterFlags.CommandFlags; - // magic pattern; we *demand* a method called Context that takes the flags - context = $"Context({param.Name})"; + // magic pattern; we *demand* a method called Context that takes the flags; if this is an extension + // method, assume it is on the first parameter + if ((methodFlags & MethodFlags.ExtensionMethod) != 0) + { + context = $"{method.Parameters[0].Name}.Context({param.Name})"; + } + else + { + context = $"Context({param.Name})"; + } } else if (IsRESPite(param.Type, RESPite.RespContext)) { @@ -407,6 +427,7 @@ static bool IsIndirectRespContext(ITypeSymbol type, out string memberName) if (param.Ordinal == 0 && method.IsExtensionMethod) { + methodFlags |= MethodFlags.ExtensionMethod; modifiers = "this " + modifiers; } @@ -463,6 +484,7 @@ void AddLiteral(string token, LiteralFlags literalFlags) context ?? "", formatter, parser, + methodFlags, debugNote); static string TypeModifiers(ITypeSymbol type) @@ -486,6 +508,24 @@ static string TypeModifiers(ITypeSymbol type) } } + private bool IsRespOperation(ref ITypeSymbol? type) // identify RespOperation[] + { + if (type is INamedTypeSymbol named && IsRESPite(type, RESPite.RespOperation)) + { + if (named.IsGenericType) + { + if (named.TypeArguments.Length != 1) return false; // unexpected + type = named.TypeArguments[0]; + } + else + { + type = null; + } + return true; + } + return false; + } + private static ParameterFlags GetTypeFlags(ref ITypeSymbol paramType) { var flags = ParameterFlags.None; @@ -710,7 +750,10 @@ private void Generate( var csValue = CodeLiteral(method.Command); WriteMethod(false); - WriteMethod(true); + if ((method.Flags & MethodFlags.RespOperation) == 0) + { + WriteMethod(true); // also write async half + } void WriteMethod(bool asAsync) { @@ -724,6 +767,14 @@ void WriteMethod(bool asAsync) sb.Append('<').Append(method.ReturnType).Append('>'); } } + else if (method.IsRespOperation) + { + sb.Append("global::RESPite.RespOperation"); + if (!string.IsNullOrWhiteSpace(method.ReturnType)) + { + sb.Append('<').Append(method.ReturnType).Append('>'); + } + } else { sb.Append(string.IsNullOrEmpty(method.ReturnType) ? "void" : method.ReturnType); @@ -768,8 +819,7 @@ void WriteMethod(bool asAsync) sb.Append(", ").Append(formatter); } } - - sb.Append(asAsync ? ").Send" : ").Wait"); + sb.Append(asAsync | method.IsRespOperation ? ").Send" : ").Wait"); if (!string.IsNullOrWhiteSpace(method.ReturnType)) { sb.Append('<').Append(method.ReturnType).Append('>'); @@ -806,6 +856,10 @@ void WriteMethod(bool asAsync) ? ".AsTask()" : ".AsValueTask()"); } + else if (method.IsRespOperation) + { + // nothing to do + } else { sb.Append(".Wait("); @@ -1244,6 +1298,7 @@ private static int DataParameterCount( "double" => RespFormattersPrefix + "Double", "" => RespFormattersPrefix + "Empty", "global::StackExchange.Redis.RedisKey" => "global::RESPite.StackExchange.Redis.RespFormatters.RedisKey", + "global::StackExchange.Redis.RedisKey[]" => "global::RESPite.StackExchange.Redis.RespFormatters.RedisKeyArray", "global::StackExchange.Redis.RedisValue" => "global::RESPite.StackExchange.Redis.RespFormatters.RedisValue", _ => null, }; @@ -1289,6 +1344,14 @@ private static string RemovePartial(string modifiers) return modifiers.Replace(" partial ", " "); } + [Flags] + private enum MethodFlags + { + None = 0, + RespOperation = 1 << 0, + ExtensionMethod = 1 << 1, + } + [Flags] private enum ParameterFlags { diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.KeyCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.KeyCommands.cs new file mode 100644 index 000000000..3616db10d --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RedisCommands.KeyCommands.cs @@ -0,0 +1,202 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.ComTypes; +using RESPite.Messages; +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal static partial class RedisCommands +{ + // this is just a "type pun" - it should be an invisible/magic pointer cast to the JIT + public static ref readonly KeyCommands Keys(this in RespContext context) + => ref Unsafe.As(ref Unsafe.AsRef(in context)); +} + +public readonly struct KeyCommands(in RespContext context) +{ + public readonly RespContext Context = context; // important: this is the only field +} + +internal static partial class KeyCommandsExtensions +{ + [RespCommand] + public static partial RespOperation Del(this in KeyCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation Del(this in KeyCommands context, [RespKey] RedisKey[] keys); + + [RespCommand] + public static partial RespOperation Dump(this in KeyCommands context, RedisKey key); + + [RespCommand("object")] + public static partial RespOperation ObjectEncoding(this in KeyCommands context, [RespPrefix("ENCODING")] RedisKey key); + + [RespCommand("object", Parser = "RespParsers.TimeSpanFromSeconds")] + public static partial RespOperation ObjectIdleTime(this in KeyCommands context, [RespPrefix("IDLETIME")] RedisKey key); + + [RespCommand("object")] + public static partial RespOperation ObjectRefCount(this in KeyCommands context, [RespPrefix("REFCOUNT")] RedisKey key); + + [RespCommand("object")] + public static partial RespOperation ObjectFreq(this in KeyCommands context, [RespPrefix("FREQ")] RedisKey key); + + [RespCommand(Parser = "RespParsers.TimeSpanFromSeconds")] + public static partial RespOperation Ttl(this in KeyCommands context, RedisKey key); + + [RespCommand(Parser = "RespParsers.TimeSpanFromMilliseconds")] + public static partial RespOperation Pttl(this in KeyCommands context, RedisKey key); + + [RespCommand(Parser = "RespParsers.DateTimeFromSeconds")] + public static partial RespOperation ExpireTime(this in KeyCommands context, RedisKey key); + + [RespCommand(Parser = "RespParsers.DateTimeFromMilliseconds")] + public static partial RespOperation PExpireTime(this in KeyCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation Exists(this in KeyCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation Move(this in KeyCommands context, RedisKey key, int db); + + [RespCommand] + public static partial RespOperation Exists(this in KeyCommands context, [RespKey] RedisKey[] keys); + + public static RespOperation Expire(this in KeyCommands context, RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always) + { + if (expiry is null || expiry == TimeSpan.MaxValue) + { + if (when != ExpireWhen.Always) Throw(when); + return Persist(context, key); + static void Throw(ExpireWhen when) => throw new ArgumentException($"PERSIST cannot be used with {when}."); + } + var millis = (long)expiry.GetValueOrDefault().TotalMilliseconds; + if (millis % 1000 == 0) // use seconds + { + return Expire(context, key, millis / 1000, when); + } + return PExpire(context, key, millis, when); + } + + public static RespOperation ExpireAt(this in KeyCommands context, RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always) + { + if (expiry is null || expiry == DateTime.MaxValue) + { + if (when != ExpireWhen.Always) Throw(when); + return Persist(context, key); + static void Throw(ExpireWhen when) => throw new ArgumentException($"PERSIST cannot be used with {when}."); + } + var millis = RedisDatabase.GetUnixTimeMilliseconds(expiry.GetValueOrDefault()); + if (millis % 1000 == 0) // use seconds + { + return ExpireAt(context, key, millis / 1000, when); + } + return PExpireAt(context, key, millis, when); + } + + [RespCommand] + public static partial RespOperation Persist(this in KeyCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation Touch(this in KeyCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation Touch(this in KeyCommands context, [RespKey] RedisKey[] keys); + + [RespCommand(Parser = "RedisTypeParser.Instance")] + public static partial RespOperation Type(this in KeyCommands context, RedisKey key); + + private sealed class RedisTypeParser : IRespParser + { + public static readonly RedisTypeParser Instance = new(); + private RedisTypeParser() { } + + public RedisType Parse(ref RespReader reader) + { + if (reader.IsNull) return RedisType.None; + if (reader.Is("zset"u8)) return RedisType.SortedSet; + return reader.ReadEnum(RedisType.Unknown); + } + } + + [RespCommand] + public static partial RespOperation Rename(this in KeyCommands context, RedisKey key, RedisKey newKey); + + [RespCommand(Formatter = "RestoreFormatter.Instance")] + public static partial RespOperation Restore(this in KeyCommands context, RedisKey key, TimeSpan? ttl, byte[] serializedValue); + + private sealed class RestoreFormatter : IRespFormatter<(RedisKey Key, TimeSpan? Ttl, byte[] SerializedValue)> + { + public static readonly RestoreFormatter Instance = new(); + private RestoreFormatter() { } + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, TimeSpan? Ttl, byte[] SerializedValue) request) + { + writer.WriteCommand(command, 3); + writer.Write(request.Key); + if (request.Ttl.HasValue) + { + writer.WriteBulkString((long)request.Ttl.Value.TotalMilliseconds); + } + else + { + writer.WriteRaw("$1\r\n0\r\n"u8); + } + writer.WriteBulkString(request.SerializedValue); + } + } + + [RespCommand] + public static partial RespOperation RandomKey(this in KeyCommands context); + + [RespCommand(Formatter = "ExpireFormatter.Instance")] + public static partial RespOperation Expire(this in KeyCommands context, RedisKey key, long seconds, ExpireWhen when = ExpireWhen.Always); + + [RespCommand(Formatter = "ExpireFormatter.Instance")] + public static partial RespOperation PExpire(this in KeyCommands context, RedisKey key, long milliseconds, ExpireWhen when = ExpireWhen.Always); + + [RespCommand(Formatter = "ExpireFormatter.Instance")] + public static partial RespOperation ExpireAt(this in KeyCommands context, RedisKey key, long seconds, ExpireWhen when = ExpireWhen.Always); + + [RespCommand(Formatter = "ExpireFormatter.Instance")] + public static partial RespOperation PExpireAt(this in KeyCommands context, RedisKey key, long milliseconds, ExpireWhen when = ExpireWhen.Always); + + private sealed class ExpireFormatter : IRespFormatter<(RedisKey Key, long Value, ExpireWhen When)> + { + public static readonly ExpireFormatter Instance = new(); + private ExpireFormatter() { } + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, long Value, ExpireWhen When) request) + { + writer.WriteCommand(command, request.When == ExpireWhen.Always ? 2 : 3); + writer.Write(request.Key); + writer.Write(request.Value); + switch (request.When) + { + case ExpireWhen.Always: + break; + case ExpireWhen.HasExpiry: + writer.WriteRaw("$2\r\nXX\r\n"u8); + break; + case ExpireWhen.HasNoExpiry: + writer.WriteRaw("$2\r\nNX\r\n"u8); + break; + case ExpireWhen.GreaterThanCurrentExpiry: + writer.WriteRaw("$2\r\nGT\r\n"u8); + break; + case ExpireWhen.LessThanCurrentExpiry: + writer.WriteRaw("$2\r\nLT\r\n"u8); + break; + default: + Throw(); + static void Throw() => throw new ArgumentOutOfRangeException(nameof(request.When)); + break; + } + } + } +} diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.Server.cs b/src/RESPite.StackExchange.Redis/RedisCommands.Server.cs deleted file mode 100644 index cdb98c566..000000000 --- a/src/RESPite.StackExchange.Redis/RedisCommands.Server.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace RESPite.StackExchange.Redis; - -internal static partial class RedisCommands -{ - // this is just a "type pun" - it should be an invisible/magic pointer cast to the JIT - public static ref readonly Servers Servers(this in RespContext context) - => ref Unsafe.As(ref Unsafe.AsRef(in context)); -} -internal readonly struct Servers(in RespContext context) -{ - public readonly RespContext Context = context; // important: this is the only field -} -internal static partial class ServerCommands -{ - [RespCommand] - internal static partial void Ping(this in Servers ctx); -} diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.ServerCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.ServerCommands.cs new file mode 100644 index 000000000..2313f3039 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RedisCommands.ServerCommands.cs @@ -0,0 +1,19 @@ +using System.Runtime.CompilerServices; + +namespace RESPite.StackExchange.Redis; + +internal static partial class RedisCommands +{ + // this is just a "type pun" - it should be an invisible/magic pointer cast to the JIT + public static ref readonly ServerCommands Servers(this in RespContext context) + => ref Unsafe.As(ref Unsafe.AsRef(in context)); +} +internal readonly struct ServerCommands(in RespContext context) +{ + public readonly RespContext Context = context; // important: this is the only field +} +internal static partial class ServerCommandsExtensions +{ + [RespCommand] + public static partial void Ping(this in ServerCommands context); +} diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.StringCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.StringCommands.cs new file mode 100644 index 000000000..00d1cfdf4 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RedisCommands.StringCommands.cs @@ -0,0 +1,22 @@ +using System.Runtime.CompilerServices; +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +internal static partial class RedisCommands +{ + // this is just a "type pun" - it should be an invisible/magic pointer cast to the JIT + public static ref readonly StringCommands Strings(this in RespContext context) + => ref Unsafe.As(ref Unsafe.AsRef(in context)); +} + +internal readonly struct StringCommands(in RespContext context) +{ + public readonly RespContext Context = context; // important: this is the only field +} + +internal static partial class StringCommandsExtensions +{ + [RespCommand("get")] + public static partial RespOperation Get(this in StringCommands context, RedisKey key); +} diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.Strings.cs b/src/RESPite.StackExchange.Redis/RedisCommands.Strings.cs deleted file mode 100644 index fb865efcb..000000000 --- a/src/RESPite.StackExchange.Redis/RedisCommands.Strings.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace RESPite.StackExchange.Redis; - -internal static partial class RedisCommands -{ - // this is just a "type pun" - it should be an invisible/magic pointer cast to the JIT - public static ref readonly Strings Strings(this in RespContext context) - => ref Unsafe.As(ref Unsafe.AsRef(in context)); -} - -internal readonly struct Strings(in RespContext context) -{ - public readonly RespContext Context = context; // important: this is the only field -} - -internal static partial class StringCommands -{ -} diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs index c8288e9ea..aa4cd397f 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs @@ -1,4 +1,5 @@ -using StackExchange.Redis; +using System.Runtime.CompilerServices; +using StackExchange.Redis; namespace RESPite.StackExchange.Redis; @@ -13,87 +14,90 @@ public Task KeyCopyAsync( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private KeyCommands Keys(CommandFlags flags) => Context(flags).Keys(); - public Task KeyDumpAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) + => Keys(flags).Del(keys).AsTask(); - public Task KeyEncodingAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task KeyDumpAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).Dump(key).AsTask(); - public Task KeyExistsAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task KeyEncodingAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).ObjectEncoding(key).AsTask(); - public Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task KeyExistsAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).Exists(key).AsTask(); - public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags) => - throw new NotImplementedException(); + public Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) + => Keys(flags).Exists(keys).AsTask(); + + public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags) + => Keys(flags).Expire(key, expiry).AsTask(); public Task KeyExpireAsync( RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Keys(flags).Expire(key, expiry, when).AsTask(); - public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags) => - throw new NotImplementedException(); + public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags) + => Keys(flags).ExpireAt(key, expiry).AsTask(); public Task KeyExpireAsync( RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Keys(flags).ExpireAt(key, expiry, when).AsTask(); - public Task KeyExpireTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task KeyExpireTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).PExpireTime(key).AsTask(); - public Task KeyFrequencyAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task KeyFrequencyAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).ObjectFreq(key).AsTask(); - public Task KeyIdleTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task KeyIdleTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).ObjectIdleTime(key).AsTask(); - public Task KeyMoveAsync(RedisKey key, int database, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task KeyMoveAsync(RedisKey key, int database, CommandFlags flags = CommandFlags.None) + => Keys(flags).Move(key, database).AsTask(); - public Task KeyPersistAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task KeyPersistAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).Persist(key).AsTask(); - public Task KeyRandomAsync(CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task KeyRandomAsync(CommandFlags flags = CommandFlags.None) + => Keys(flags).RandomKey().AsTask(); - public Task KeyRefCountAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task KeyRefCountAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).ObjectRefCount(key).AsTask(); public Task KeyRenameAsync( RedisKey key, RedisKey newKey, When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Keys(flags).Rename(key, newKey).AsTask(); public Task KeyRestoreAsync( RedisKey key, byte[] value, TimeSpan? expiry = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Keys(flags).Restore(key, expiry, value).AsTask(); - public Task KeyTimeToLiveAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task KeyTimeToLiveAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).Pttl(key).AsTask(); - public Task KeyTouchAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task KeyTouchAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).Touch(key).AsTask(); - public Task KeyTouchAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task KeyTouchAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) + => Keys(flags).Touch(keys).AsTask(); - public Task KeyTypeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task KeyTypeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).Type(key).AsTask(); // Synchronous Key methods public bool KeyCopy( diff --git a/src/RESPite.StackExchange.Redis/RespFormatters.cs b/src/RESPite.StackExchange.Redis/RespFormatters.cs index 374aeeca4..b6537f15e 100644 --- a/src/RESPite.StackExchange.Redis/RespFormatters.cs +++ b/src/RESPite.StackExchange.Redis/RespFormatters.cs @@ -9,8 +9,9 @@ public static class RespFormatters { public static IRespFormatter RedisValue => DefaultFormatter.Instance; public static IRespFormatter RedisKey => DefaultFormatter.Instance; + public static IRespFormatter RedisKeyArray => DefaultFormatter.Instance; - private sealed class DefaultFormatter : IRespFormatter, IRespFormatter + private sealed class DefaultFormatter : IRespFormatter, IRespFormatter, IRespFormatter { public static readonly DefaultFormatter Instance = new(); private DefaultFormatter() { } @@ -26,6 +27,15 @@ public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in writer.WriteCommand(command, 1); writer.Write(request); } + + public void Format(scoped ReadOnlySpan command, ref RespWriter writer, in RedisKey[] request) + { + writer.WriteCommand(command, 1 + request.Length); + foreach (var key in request) + { + writer.Write(key); + } + } } // ReSharper disable once MemberCanBePrivate.Global diff --git a/src/RESPite.StackExchange.Redis/RespParsers.cs b/src/RESPite.StackExchange.Redis/RespParsers.cs index cff1bb209..9ad995ec1 100644 --- a/src/RESPite.StackExchange.Redis/RespParsers.cs +++ b/src/RESPite.StackExchange.Redis/RespParsers.cs @@ -1,4 +1,5 @@ -using RESPite.Messages; +using RESPite.Internal; +using RESPite.Messages; using StackExchange.Redis; namespace RESPite.StackExchange.Redis; @@ -10,6 +11,10 @@ public static class RespParsers public static IRespParser RedisKey => DefaultParser.Instance; public static IRespParser> BytesLease => DefaultParser.Instance; public static IRespParser HashEntryArray => DefaultParser.Instance; + public static IRespParser TimeSpanFromSeconds => TimeParser.FromSeconds; + public static IRespParser DateTimeFromSeconds => TimeParser.FromSeconds; + public static IRespParser TimeSpanFromMilliseconds => TimeParser.FromMilliseconds; + public static IRespParser DateTimeFromMilliseconds => TimeParser.FromMilliseconds; public static RedisValue ReadRedisValue(ref RespReader reader) { @@ -89,3 +94,27 @@ HashEntry[] IRespParser.Parse(ref RespReader reader) } } } + +internal sealed class TimeParser : IRespParser, IRespParser, IRespInlineParser +{ + private readonly bool _millis; + public static readonly TimeParser FromMilliseconds = new(true); + public static readonly TimeParser FromSeconds = new(false); + private TimeParser(bool millis) => _millis = millis; + + TimeSpan? IRespParser.Parse(ref RespReader reader) + { + if (reader.IsNull) return null; + var value = reader.ReadInt64(); + if (value < 0) return null; // -1 means no expiry and -2 means key does not exist + return _millis ? TimeSpan.FromMilliseconds(value) : TimeSpan.FromSeconds(value); + } + + DateTime? IRespParser.Parse(ref RespReader reader) + { + if (reader.IsNull) return null; + var value = reader.ReadInt64(); + if (value < 0) return null; // -1 means no expiry and -2 means key does not exist + return _millis ? RedisBase.UnixEpoch.AddMilliseconds(value) : RedisBase.UnixEpoch.AddSeconds(value); + } +} diff --git a/src/RESPite/RespKeyAttribute.cs b/src/RESPite/RespKeyAttribute.cs new file mode 100644 index 000000000..5c8b87a71 --- /dev/null +++ b/src/RESPite/RespKeyAttribute.cs @@ -0,0 +1,10 @@ +using System.ComponentModel; +using System.Diagnostics; + +namespace RESPite; + +[AttributeUsage(AttributeTargets.Parameter)] +[Conditional("DEBUG"), ImmutableObject(true)] +public sealed class RespKeyAttribute() : Attribute +{ +} diff --git a/src/StackExchange.Redis/RedisBase.cs b/src/StackExchange.Redis/RedisBase.cs index 8286a8837..1863cf2b1 100644 --- a/src/StackExchange.Redis/RedisBase.cs +++ b/src/StackExchange.Redis/RedisBase.cs @@ -104,6 +104,12 @@ internal static bool IsNil(in RedisValue pattern) internal static class WhenExtensions { + internal static void AlwaysOnly(this When when) + { + if (when != When.Always) Throw(when); + static void Throw(When when) => throw new ArgumentException(when + " is not valid in this context; the permitted values are: Always"); + } + internal static void AlwaysOrExists(this When when) { switch (when) diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 227a7d127..9b6852042 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -3776,7 +3776,7 @@ public Task StringSetRangeAsync(RedisKey key, long offset, RedisValu return ExecuteAsync(msg, ResultProcessor.RedisValue); } - private static long GetUnixTimeMilliseconds(DateTime when) => when.Kind switch + internal static long GetUnixTimeMilliseconds(DateTime when) => when.Kind switch { DateTimeKind.Local or DateTimeKind.Utc => (when.ToUniversalTime() - RedisBase.UnixEpoch).Ticks / TimeSpan.TicksPerMillisecond, _ => throw new ArgumentException("Expiry time must be either Utc or Local", nameof(when)), From a954e1f4d0472893370a5ee3dc72f3c09d82124f Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 1 Oct 2025 16:31:00 +0100 Subject: [PATCH 089/108] copy --- .../RedisCommands.KeyCommands.cs | 172 ++++++++++++------ .../RespContextDatabase.Key.cs | 12 +- 2 files changed, 118 insertions(+), 66 deletions(-) diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.KeyCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.KeyCommands.cs index 3616db10d..84e1dc33b 100644 --- a/src/RESPite.StackExchange.Redis/RedisCommands.KeyCommands.cs +++ b/src/RESPite.StackExchange.Redis/RedisCommands.KeyCommands.cs @@ -1,5 +1,4 @@ using System.Runtime.CompilerServices; -using System.Runtime.InteropServices.ComTypes; using RESPite.Messages; using StackExchange.Redis; @@ -19,6 +18,14 @@ public readonly struct KeyCommands(in RespContext context) internal static partial class KeyCommandsExtensions { + [RespCommand(Formatter = "CopyFormatter.Instance")] + public static partial RespOperation Copy( + this in KeyCommands context, + RedisKey source, + RedisKey destination, + int destinationDatabase = -1, + bool replace = false); + [RespCommand] public static partial RespOperation Del(this in KeyCommands context, RedisKey key); @@ -28,36 +35,9 @@ internal static partial class KeyCommandsExtensions [RespCommand] public static partial RespOperation Dump(this in KeyCommands context, RedisKey key); - [RespCommand("object")] - public static partial RespOperation ObjectEncoding(this in KeyCommands context, [RespPrefix("ENCODING")] RedisKey key); - - [RespCommand("object", Parser = "RespParsers.TimeSpanFromSeconds")] - public static partial RespOperation ObjectIdleTime(this in KeyCommands context, [RespPrefix("IDLETIME")] RedisKey key); - - [RespCommand("object")] - public static partial RespOperation ObjectRefCount(this in KeyCommands context, [RespPrefix("REFCOUNT")] RedisKey key); - - [RespCommand("object")] - public static partial RespOperation ObjectFreq(this in KeyCommands context, [RespPrefix("FREQ")] RedisKey key); - - [RespCommand(Parser = "RespParsers.TimeSpanFromSeconds")] - public static partial RespOperation Ttl(this in KeyCommands context, RedisKey key); - - [RespCommand(Parser = "RespParsers.TimeSpanFromMilliseconds")] - public static partial RespOperation Pttl(this in KeyCommands context, RedisKey key); - - [RespCommand(Parser = "RespParsers.DateTimeFromSeconds")] - public static partial RespOperation ExpireTime(this in KeyCommands context, RedisKey key); - - [RespCommand(Parser = "RespParsers.DateTimeFromMilliseconds")] - public static partial RespOperation PExpireTime(this in KeyCommands context, RedisKey key); - [RespCommand] public static partial RespOperation Exists(this in KeyCommands context, RedisKey key); - [RespCommand] - public static partial RespOperation Move(this in KeyCommands context, RedisKey key, int db); - [RespCommand] public static partial RespOperation Exists(this in KeyCommands context, [RespKey] RedisKey[] keys); @@ -77,6 +57,9 @@ public static RespOperation Expire(this in KeyCommands context, RedisKey k return PExpire(context, key, millis, when); } + [RespCommand(Formatter = "ExpireFormatter.Instance")] + public static partial RespOperation Expire(this in KeyCommands context, RedisKey key, long seconds, ExpireWhen when = ExpireWhen.Always); + public static RespOperation ExpireAt(this in KeyCommands context, RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always) { if (expiry is null || expiry == DateTime.MaxValue) @@ -93,15 +76,78 @@ public static RespOperation ExpireAt(this in KeyCommands context, RedisKey return PExpireAt(context, key, millis, when); } + [RespCommand(Formatter = "ExpireFormatter.Instance")] + public static partial RespOperation ExpireAt(this in KeyCommands context, RedisKey key, long seconds, ExpireWhen when = ExpireWhen.Always); + + [RespCommand(Parser = "RespParsers.DateTimeFromSeconds")] + public static partial RespOperation ExpireTime(this in KeyCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation Move(this in KeyCommands context, RedisKey key, int db); + + [RespCommand("object")] + public static partial RespOperation ObjectEncoding(this in KeyCommands context, [RespPrefix("ENCODING")] RedisKey key); + + [RespCommand("object")] + public static partial RespOperation ObjectFreq(this in KeyCommands context, [RespPrefix("FREQ")] RedisKey key); + + [RespCommand("object", Parser = "RespParsers.TimeSpanFromSeconds")] + public static partial RespOperation ObjectIdleTime(this in KeyCommands context, [RespPrefix("IDLETIME")] RedisKey key); + + [RespCommand("object")] + public static partial RespOperation ObjectRefCount(this in KeyCommands context, [RespPrefix("REFCOUNT")] RedisKey key); + + [RespCommand(Formatter = "ExpireFormatter.Instance")] + public static partial RespOperation PExpire(this in KeyCommands context, RedisKey key, long milliseconds, ExpireWhen when = ExpireWhen.Always); + + [RespCommand(Formatter = "ExpireFormatter.Instance")] + public static partial RespOperation PExpireAt(this in KeyCommands context, RedisKey key, long milliseconds, ExpireWhen when = ExpireWhen.Always); + + [RespCommand(Parser = "RespParsers.DateTimeFromMilliseconds")] + public static partial RespOperation PExpireTime(this in KeyCommands context, RedisKey key); + [RespCommand] public static partial RespOperation Persist(this in KeyCommands context, RedisKey key); + [RespCommand(Parser = "RespParsers.TimeSpanFromMilliseconds")] + public static partial RespOperation Pttl(this in KeyCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation RandomKey(this in KeyCommands context); + + [RespCommand] + public static partial RespOperation Rename(this in KeyCommands context, RedisKey key, RedisKey newKey); + + [RespCommand] + public static RespOperation Rename(this in KeyCommands context, RedisKey key, RedisKey newKey, When when) + { + switch (when) + { + case When.Always: + return Rename(context, key, newKey); + case When.NotExists: + return RenameNx(context, key, newKey); + default: + when.AlwaysOrNotExists(); // throws + return default; + } + } + + [RespCommand] + public static partial RespOperation RenameNx(this in KeyCommands context, RedisKey key, RedisKey newKey); + + [RespCommand(Formatter = "RestoreFormatter.Instance")] + public static partial RespOperation Restore(this in KeyCommands context, RedisKey key, TimeSpan? ttl, byte[] serializedValue); + [RespCommand] public static partial RespOperation Touch(this in KeyCommands context, RedisKey key); [RespCommand] public static partial RespOperation Touch(this in KeyCommands context, [RespKey] RedisKey[] keys); + [RespCommand(Parser = "RespParsers.TimeSpanFromSeconds")] + public static partial RespOperation Ttl(this in KeyCommands context, RedisKey key); + [RespCommand(Parser = "RedisTypeParser.Instance")] public static partial RespOperation Type(this in KeyCommands context, RedisKey key); @@ -118,51 +164,33 @@ public RedisType Parse(ref RespReader reader) } } - [RespCommand] - public static partial RespOperation Rename(this in KeyCommands context, RedisKey key, RedisKey newKey); - - [RespCommand(Formatter = "RestoreFormatter.Instance")] - public static partial RespOperation Restore(this in KeyCommands context, RedisKey key, TimeSpan? ttl, byte[] serializedValue); - - private sealed class RestoreFormatter : IRespFormatter<(RedisKey Key, TimeSpan? Ttl, byte[] SerializedValue)> + private sealed class CopyFormatter : IRespFormatter<(RedisKey Source, RedisKey Destination, int DestinationDatabase, + bool Replace)> { - public static readonly RestoreFormatter Instance = new(); - private RestoreFormatter() { } + public static readonly CopyFormatter Instance = new(); + private CopyFormatter() { } public void Format( scoped ReadOnlySpan command, ref RespWriter writer, - in (RedisKey Key, TimeSpan? Ttl, byte[] SerializedValue) request) + in (RedisKey Source, RedisKey Destination, int DestinationDatabase, bool Replace) request) { - writer.WriteCommand(command, 3); - writer.Write(request.Key); - if (request.Ttl.HasValue) + writer.WriteCommand(command, (request.DestinationDatabase >= 0 ? 4 : 2) + (request.Replace ? 1 : 0)); + writer.Write(request.Source); + writer.Write(request.Destination); + if (request.DestinationDatabase >= 0) { - writer.WriteBulkString((long)request.Ttl.Value.TotalMilliseconds); + writer.WriteRaw("$2\r\nDB\r\n"u8); + writer.WriteBulkString(request.DestinationDatabase); } - else + + if (request.Replace) { - writer.WriteRaw("$1\r\n0\r\n"u8); + writer.WriteRaw("$7\r\nREPLACE\r\n"u8); } - writer.WriteBulkString(request.SerializedValue); } } - [RespCommand] - public static partial RespOperation RandomKey(this in KeyCommands context); - - [RespCommand(Formatter = "ExpireFormatter.Instance")] - public static partial RespOperation Expire(this in KeyCommands context, RedisKey key, long seconds, ExpireWhen when = ExpireWhen.Always); - - [RespCommand(Formatter = "ExpireFormatter.Instance")] - public static partial RespOperation PExpire(this in KeyCommands context, RedisKey key, long milliseconds, ExpireWhen when = ExpireWhen.Always); - - [RespCommand(Formatter = "ExpireFormatter.Instance")] - public static partial RespOperation ExpireAt(this in KeyCommands context, RedisKey key, long seconds, ExpireWhen when = ExpireWhen.Always); - - [RespCommand(Formatter = "ExpireFormatter.Instance")] - public static partial RespOperation PExpireAt(this in KeyCommands context, RedisKey key, long milliseconds, ExpireWhen when = ExpireWhen.Always); - private sealed class ExpireFormatter : IRespFormatter<(RedisKey Key, long Value, ExpireWhen When)> { public static readonly ExpireFormatter Instance = new(); @@ -199,4 +227,28 @@ public void Format( } } } + + private sealed class RestoreFormatter : IRespFormatter<(RedisKey Key, TimeSpan? Ttl, byte[] SerializedValue)> + { + public static readonly RestoreFormatter Instance = new(); + private RestoreFormatter() { } + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, TimeSpan? Ttl, byte[] SerializedValue) request) + { + writer.WriteCommand(command, 3); + writer.Write(request.Key); + if (request.Ttl.HasValue) + { + writer.WriteBulkString((long)request.Ttl.Value.TotalMilliseconds); + } + else + { + writer.WriteRaw("$1\r\n0\r\n"u8); + } + writer.WriteBulkString(request.SerializedValue); + } + } } diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs index aa4cd397f..f7bdd84e8 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs @@ -5,17 +5,17 @@ namespace RESPite.StackExchange.Redis; internal partial class RespContextDatabase { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private KeyCommands Keys(CommandFlags flags) => Context(flags).Keys(); + // Async Key methods public Task KeyCopyAsync( RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private KeyCommands Keys(CommandFlags flags) => Context(flags).Keys(); + CommandFlags flags = CommandFlags.None) + => Keys(flags).Copy(sourceKey, destinationKey, destinationDatabase, replace).AsTask(); public Task KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Keys(flags).Del(keys).AsTask(); @@ -78,7 +78,7 @@ public Task KeyRenameAsync( RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None) - => Keys(flags).Rename(key, newKey).AsTask(); + => Keys(flags).Rename(key, newKey, when).AsTask(); public Task KeyRestoreAsync( RedisKey key, From bbcff915b354564538564a00387f7374eccb7c87 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 1 Oct 2025 16:40:51 +0100 Subject: [PATCH 090/108] tidy key commands --- .../RespContextDatabase.Key.cs | 191 +++++++++--------- 1 file changed, 96 insertions(+), 95 deletions(-) diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs index f7bdd84e8..342cf8eb3 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs @@ -8,7 +8,14 @@ internal partial class RespContextDatabase [MethodImpl(MethodImplOptions.AggressiveInlining)] private KeyCommands Keys(CommandFlags flags) => Context(flags).Keys(); - // Async Key methods + public bool KeyCopy( + RedisKey sourceKey, + RedisKey destinationKey, + int destinationDatabase = -1, + bool replace = false, + CommandFlags flags = CommandFlags.None) => + Keys(flags).Copy(sourceKey, destinationKey, destinationDatabase, replace).Wait(SyncTimeout); + public Task KeyCopyAsync( RedisKey sourceKey, RedisKey destinationKey, @@ -17,21 +24,62 @@ public Task KeyCopyAsync( CommandFlags flags = CommandFlags.None) => Keys(flags).Copy(sourceKey, destinationKey, destinationDatabase, replace).AsTask(); + public bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).Del(key).Wait(SyncTimeout); + + public long KeyDelete(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Keys(flags).Del(keys).Wait(SyncTimeout); + + public Task KeyDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).Del(key).AsTask(); + public Task KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Keys(flags).Del(keys).AsTask(); + public byte[]? KeyDump(RedisKey key, CommandFlags flags = CommandFlags.None) => + Keys(flags).Dump(key).Wait(SyncTimeout); + public Task KeyDumpAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).Dump(key).AsTask(); + public string? KeyEncoding(RedisKey key, CommandFlags flags = CommandFlags.None) => + Keys(flags).ObjectEncoding(key).Wait(SyncTimeout); + public Task KeyEncodingAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).ObjectEncoding(key).AsTask(); + public bool KeyExists(RedisKey key, CommandFlags flags = CommandFlags.None) => + Keys(flags).Exists(key).Wait(SyncTimeout); + + public long KeyExists(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Keys(flags).Exists(keys).Wait(SyncTimeout); + public Task KeyExistsAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).Exists(key).AsTask(); public Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Keys(flags).Exists(keys).AsTask(); + public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags) => + Keys(flags).Expire(key, expiry).Wait(SyncTimeout); + + public bool KeyExpire( + RedisKey key, + TimeSpan? expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => + Keys(flags).Expire(key, expiry, when).Wait(SyncTimeout); + + public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags) => + Keys(flags).ExpireAt(key, expiry).Wait(SyncTimeout); + + public bool KeyExpire( + RedisKey key, + DateTime? expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) => + Keys(flags).ExpireAt(key, expiry, when).Wait(SyncTimeout); + public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags) => Keys(flags).Expire(key, expiry).AsTask(); @@ -52,27 +100,55 @@ public Task KeyExpireAsync( CommandFlags flags = CommandFlags.None) => Keys(flags).ExpireAt(key, expiry, when).AsTask(); + public DateTime? KeyExpireTime(RedisKey key, CommandFlags flags = CommandFlags.None) => + Keys(flags).PExpireTime(key).Wait(SyncTimeout); + public Task KeyExpireTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).PExpireTime(key).AsTask(); + public long? KeyFrequency(RedisKey key, CommandFlags flags = CommandFlags.None) => + Keys(flags).ObjectFreq(key).Wait(SyncTimeout); + public Task KeyFrequencyAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).ObjectFreq(key).AsTask(); + public TimeSpan? KeyIdleTime(RedisKey key, CommandFlags flags = CommandFlags.None) => + Keys(flags).ObjectIdleTime(key).Wait(SyncTimeout); + public Task KeyIdleTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).ObjectIdleTime(key).AsTask(); + public bool KeyMove(RedisKey key, int database, CommandFlags flags = CommandFlags.None) => + Keys(flags).Move(key, database).Wait(SyncTimeout); + public Task KeyMoveAsync(RedisKey key, int database, CommandFlags flags = CommandFlags.None) => Keys(flags).Move(key, database).AsTask(); + public bool KeyPersist(RedisKey key, CommandFlags flags = CommandFlags.None) => + Keys(flags).Persist(key).Wait(SyncTimeout); + public Task KeyPersistAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).Persist(key).AsTask(); + public RedisKey KeyRandom(CommandFlags flags = CommandFlags.None) => + Keys(flags).RandomKey().Wait(SyncTimeout); + public Task KeyRandomAsync(CommandFlags flags = CommandFlags.None) => Keys(flags).RandomKey().AsTask(); + public long? KeyRefCount(RedisKey key, CommandFlags flags = CommandFlags.None) => + Keys(flags).ObjectRefCount(key).Wait(SyncTimeout); + public Task KeyRefCountAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).ObjectRefCount(key).AsTask(); + public bool KeyRename( + RedisKey key, + RedisKey newKey, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => + Keys(flags).Rename(key, newKey, when).Wait(SyncTimeout); + public Task KeyRenameAsync( RedisKey key, RedisKey newKey, @@ -80,6 +156,13 @@ public Task KeyRenameAsync( CommandFlags flags = CommandFlags.None) => Keys(flags).Rename(key, newKey, when).AsTask(); + public void KeyRestore( + RedisKey key, + byte[] value, + TimeSpan? expiry = null, + CommandFlags flags = CommandFlags.None) => + Keys(flags).Restore(key, expiry, value).Wait(SyncTimeout); + public Task KeyRestoreAsync( RedisKey key, byte[] value, @@ -87,109 +170,27 @@ public Task KeyRestoreAsync( CommandFlags flags = CommandFlags.None) => Keys(flags).Restore(key, expiry, value).AsTask(); + public TimeSpan? KeyTimeToLive(RedisKey key, CommandFlags flags = CommandFlags.None) => + Keys(flags).Pttl(key).Wait(SyncTimeout); + public Task KeyTimeToLiveAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).Pttl(key).AsTask(); + public bool KeyTouch(RedisKey key, CommandFlags flags = CommandFlags.None) => + Keys(flags).Touch(key).Wait(SyncTimeout); + + public long KeyTouch(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Keys(flags).Touch(keys).Wait(SyncTimeout); + public Task KeyTouchAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).Touch(key).AsTask(); public Task KeyTouchAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Keys(flags).Touch(keys).AsTask(); + public RedisType KeyType(RedisKey key, CommandFlags flags = CommandFlags.None) => + Keys(flags).Type(key).Wait(SyncTimeout); + public Task KeyTypeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).Type(key).AsTask(); - - // Synchronous Key methods - public bool KeyCopy( - RedisKey sourceKey, - RedisKey destinationKey, - int destinationDatabase = -1, - bool replace = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - [RespCommand("del")] - public partial bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None); - - public long KeyDelete(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public byte[]? KeyDump(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public string? KeyEncoding(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool KeyExists(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long KeyExists(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags) => - throw new NotImplementedException(); - - public bool KeyExpire( - RedisKey key, - TimeSpan? expiry, - ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags) => - throw new NotImplementedException(); - - public bool KeyExpire( - RedisKey key, - DateTime? expiry, - ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public DateTime? KeyExpireTime(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long? KeyFrequency(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public TimeSpan? KeyIdleTime(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool KeyMove(RedisKey key, int database, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool KeyPersist(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisKey KeyRandom(CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long? KeyRefCount(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool KeyRename( - RedisKey key, - RedisKey newKey, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public void KeyRestore( - RedisKey key, - byte[] value, - TimeSpan? expiry = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public TimeSpan? KeyTimeToLive(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public bool KeyTouch(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long KeyTouch(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisType KeyType(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); } From 09390bcca0112bdfa460891139b996b0e2ecc81c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 1 Oct 2025 16:43:54 +0100 Subject: [PATCH 091/108] normalize lambdas --- .../RespContextDatabase.Key.cs | 96 +++++++++---------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs index 342cf8eb3..2292c0868 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs @@ -13,8 +13,8 @@ public bool KeyCopy( RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, - CommandFlags flags = CommandFlags.None) => - Keys(flags).Copy(sourceKey, destinationKey, destinationDatabase, replace).Wait(SyncTimeout); + CommandFlags flags = CommandFlags.None) + => Keys(flags).Copy(sourceKey, destinationKey, destinationDatabase, replace).Wait(SyncTimeout); public Task KeyCopyAsync( RedisKey sourceKey, @@ -27,8 +27,8 @@ public Task KeyCopyAsync( public bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).Del(key).Wait(SyncTimeout); - public long KeyDelete(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - Keys(flags).Del(keys).Wait(SyncTimeout); + public long KeyDelete(RedisKey[] keys, CommandFlags flags = CommandFlags.None) + => Keys(flags).Del(keys).Wait(SyncTimeout); public Task KeyDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).Del(key).AsTask(); @@ -36,23 +36,23 @@ public Task KeyDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags public Task KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Keys(flags).Del(keys).AsTask(); - public byte[]? KeyDump(RedisKey key, CommandFlags flags = CommandFlags.None) => - Keys(flags).Dump(key).Wait(SyncTimeout); + public byte[]? KeyDump(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).Dump(key).Wait(SyncTimeout); public Task KeyDumpAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).Dump(key).AsTask(); - public string? KeyEncoding(RedisKey key, CommandFlags flags = CommandFlags.None) => - Keys(flags).ObjectEncoding(key).Wait(SyncTimeout); + public string? KeyEncoding(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).ObjectEncoding(key).Wait(SyncTimeout); public Task KeyEncodingAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).ObjectEncoding(key).AsTask(); - public bool KeyExists(RedisKey key, CommandFlags flags = CommandFlags.None) => - Keys(flags).Exists(key).Wait(SyncTimeout); + public bool KeyExists(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).Exists(key).Wait(SyncTimeout); - public long KeyExists(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - Keys(flags).Exists(keys).Wait(SyncTimeout); + public long KeyExists(RedisKey[] keys, CommandFlags flags = CommandFlags.None) + => Keys(flags).Exists(keys).Wait(SyncTimeout); public Task KeyExistsAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).Exists(key).AsTask(); @@ -60,25 +60,25 @@ public Task KeyExistsAsync(RedisKey key, CommandFlags flags = CommandFlags public Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Keys(flags).Exists(keys).AsTask(); - public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags) => - Keys(flags).Expire(key, expiry).Wait(SyncTimeout); + public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags) + => Keys(flags).Expire(key, expiry).Wait(SyncTimeout); public bool KeyExpire( RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - Keys(flags).Expire(key, expiry, when).Wait(SyncTimeout); + CommandFlags flags = CommandFlags.None) + => Keys(flags).Expire(key, expiry, when).Wait(SyncTimeout); - public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags) => - Keys(flags).ExpireAt(key, expiry).Wait(SyncTimeout); + public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags) + => Keys(flags).ExpireAt(key, expiry).Wait(SyncTimeout); public bool KeyExpire( RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - Keys(flags).ExpireAt(key, expiry, when).Wait(SyncTimeout); + CommandFlags flags = CommandFlags.None) + => Keys(flags).ExpireAt(key, expiry, when).Wait(SyncTimeout); public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags) => Keys(flags).Expire(key, expiry).AsTask(); @@ -87,8 +87,8 @@ public Task KeyExpireAsync( RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - Keys(flags).Expire(key, expiry, when).AsTask(); + CommandFlags flags = CommandFlags.None) + => Keys(flags).Expire(key, expiry, when).AsTask(); public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags) => Keys(flags).ExpireAt(key, expiry).AsTask(); @@ -100,44 +100,44 @@ public Task KeyExpireAsync( CommandFlags flags = CommandFlags.None) => Keys(flags).ExpireAt(key, expiry, when).AsTask(); - public DateTime? KeyExpireTime(RedisKey key, CommandFlags flags = CommandFlags.None) => - Keys(flags).PExpireTime(key).Wait(SyncTimeout); + public DateTime? KeyExpireTime(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).PExpireTime(key).Wait(SyncTimeout); public Task KeyExpireTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).PExpireTime(key).AsTask(); - public long? KeyFrequency(RedisKey key, CommandFlags flags = CommandFlags.None) => - Keys(flags).ObjectFreq(key).Wait(SyncTimeout); + public long? KeyFrequency(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).ObjectFreq(key).Wait(SyncTimeout); public Task KeyFrequencyAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).ObjectFreq(key).AsTask(); - public TimeSpan? KeyIdleTime(RedisKey key, CommandFlags flags = CommandFlags.None) => - Keys(flags).ObjectIdleTime(key).Wait(SyncTimeout); + public TimeSpan? KeyIdleTime(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).ObjectIdleTime(key).Wait(SyncTimeout); public Task KeyIdleTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).ObjectIdleTime(key).AsTask(); - public bool KeyMove(RedisKey key, int database, CommandFlags flags = CommandFlags.None) => - Keys(flags).Move(key, database).Wait(SyncTimeout); + public bool KeyMove(RedisKey key, int database, CommandFlags flags = CommandFlags.None) + => Keys(flags).Move(key, database).Wait(SyncTimeout); public Task KeyMoveAsync(RedisKey key, int database, CommandFlags flags = CommandFlags.None) => Keys(flags).Move(key, database).AsTask(); - public bool KeyPersist(RedisKey key, CommandFlags flags = CommandFlags.None) => - Keys(flags).Persist(key).Wait(SyncTimeout); + public bool KeyPersist(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).Persist(key).Wait(SyncTimeout); public Task KeyPersistAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).Persist(key).AsTask(); - public RedisKey KeyRandom(CommandFlags flags = CommandFlags.None) => - Keys(flags).RandomKey().Wait(SyncTimeout); + public RedisKey KeyRandom(CommandFlags flags = CommandFlags.None) + => Keys(flags).RandomKey().Wait(SyncTimeout); public Task KeyRandomAsync(CommandFlags flags = CommandFlags.None) => Keys(flags).RandomKey().AsTask(); - public long? KeyRefCount(RedisKey key, CommandFlags flags = CommandFlags.None) => - Keys(flags).ObjectRefCount(key).Wait(SyncTimeout); + public long? KeyRefCount(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).ObjectRefCount(key).Wait(SyncTimeout); public Task KeyRefCountAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).ObjectRefCount(key).AsTask(); @@ -146,8 +146,8 @@ public bool KeyRename( RedisKey key, RedisKey newKey, When when = When.Always, - CommandFlags flags = CommandFlags.None) => - Keys(flags).Rename(key, newKey, when).Wait(SyncTimeout); + CommandFlags flags = CommandFlags.None) + => Keys(flags).Rename(key, newKey, when).Wait(SyncTimeout); public Task KeyRenameAsync( RedisKey key, @@ -160,8 +160,8 @@ public void KeyRestore( RedisKey key, byte[] value, TimeSpan? expiry = null, - CommandFlags flags = CommandFlags.None) => - Keys(flags).Restore(key, expiry, value).Wait(SyncTimeout); + CommandFlags flags = CommandFlags.None) + => Keys(flags).Restore(key, expiry, value).Wait(SyncTimeout); public Task KeyRestoreAsync( RedisKey key, @@ -170,17 +170,17 @@ public Task KeyRestoreAsync( CommandFlags flags = CommandFlags.None) => Keys(flags).Restore(key, expiry, value).AsTask(); - public TimeSpan? KeyTimeToLive(RedisKey key, CommandFlags flags = CommandFlags.None) => - Keys(flags).Pttl(key).Wait(SyncTimeout); + public TimeSpan? KeyTimeToLive(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).Pttl(key).Wait(SyncTimeout); public Task KeyTimeToLiveAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).Pttl(key).AsTask(); - public bool KeyTouch(RedisKey key, CommandFlags flags = CommandFlags.None) => - Keys(flags).Touch(key).Wait(SyncTimeout); + public bool KeyTouch(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).Touch(key).Wait(SyncTimeout); - public long KeyTouch(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - Keys(flags).Touch(keys).Wait(SyncTimeout); + public long KeyTouch(RedisKey[] keys, CommandFlags flags = CommandFlags.None) + => Keys(flags).Touch(keys).Wait(SyncTimeout); public Task KeyTouchAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).Touch(key).AsTask(); @@ -188,8 +188,8 @@ public Task KeyTouchAsync(RedisKey key, CommandFlags flags = CommandFlags. public Task KeyTouchAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Keys(flags).Touch(keys).AsTask(); - public RedisType KeyType(RedisKey key, CommandFlags flags = CommandFlags.None) => - Keys(flags).Type(key).Wait(SyncTimeout); + public RedisType KeyType(RedisKey key, CommandFlags flags = CommandFlags.None) + => Keys(flags).Type(key).Wait(SyncTimeout); public Task KeyTypeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Keys(flags).Type(key).AsTask(); From 8eecbdc978643149c90a4129d91b08d8f727abb2 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 1 Oct 2025 16:47:40 +0100 Subject: [PATCH 092/108] pic-optimization; avoid struct-copyu from context()/keys() --- .../RespContextDatabase.Key.cs | 99 +++++++++---------- 1 file changed, 48 insertions(+), 51 deletions(-) diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs index 2292c0868..9ba447fbe 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs @@ -5,16 +5,13 @@ namespace RESPite.StackExchange.Redis; internal partial class RespContextDatabase { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private KeyCommands Keys(CommandFlags flags) => Context(flags).Keys(); - public bool KeyCopy( RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None) - => Keys(flags).Copy(sourceKey, destinationKey, destinationDatabase, replace).Wait(SyncTimeout); + => Context(flags).Keys().Copy(sourceKey, destinationKey, destinationDatabase, replace).Wait(SyncTimeout); public Task KeyCopyAsync( RedisKey sourceKey, @@ -22,175 +19,175 @@ public Task KeyCopyAsync( int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None) - => Keys(flags).Copy(sourceKey, destinationKey, destinationDatabase, replace).AsTask(); + => Context(flags).Keys().Copy(sourceKey, destinationKey, destinationDatabase, replace).AsTask(); public bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).Del(key).Wait(SyncTimeout); + => Context(flags).Keys().Del(key).Wait(SyncTimeout); public long KeyDelete(RedisKey[] keys, CommandFlags flags = CommandFlags.None) - => Keys(flags).Del(keys).Wait(SyncTimeout); + => Context(flags).Keys().Del(keys).Wait(SyncTimeout); public Task KeyDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).Del(key).AsTask(); + => Context(flags).Keys().Del(key).AsTask(); public Task KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) - => Keys(flags).Del(keys).AsTask(); + => Context(flags).Keys().Del(keys).AsTask(); public byte[]? KeyDump(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).Dump(key).Wait(SyncTimeout); + => Context(flags).Keys().Dump(key).Wait(SyncTimeout); public Task KeyDumpAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).Dump(key).AsTask(); + => Context(flags).Keys().Dump(key).AsTask(); public string? KeyEncoding(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).ObjectEncoding(key).Wait(SyncTimeout); + => Context(flags).Keys().ObjectEncoding(key).Wait(SyncTimeout); public Task KeyEncodingAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).ObjectEncoding(key).AsTask(); + => Context(flags).Keys().ObjectEncoding(key).AsTask(); public bool KeyExists(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).Exists(key).Wait(SyncTimeout); + => Context(flags).Keys().Exists(key).Wait(SyncTimeout); public long KeyExists(RedisKey[] keys, CommandFlags flags = CommandFlags.None) - => Keys(flags).Exists(keys).Wait(SyncTimeout); + => Context(flags).Keys().Exists(keys).Wait(SyncTimeout); public Task KeyExistsAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).Exists(key).AsTask(); + => Context(flags).Keys().Exists(key).AsTask(); public Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) - => Keys(flags).Exists(keys).AsTask(); + => Context(flags).Keys().Exists(keys).AsTask(); public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags) - => Keys(flags).Expire(key, expiry).Wait(SyncTimeout); + => Context(flags).Keys().Expire(key, expiry).Wait(SyncTimeout); public bool KeyExpire( RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) - => Keys(flags).Expire(key, expiry, when).Wait(SyncTimeout); + => Context(flags).Keys().Expire(key, expiry, when).Wait(SyncTimeout); public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags) - => Keys(flags).ExpireAt(key, expiry).Wait(SyncTimeout); + => Context(flags).Keys().ExpireAt(key, expiry).Wait(SyncTimeout); public bool KeyExpire( RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) - => Keys(flags).ExpireAt(key, expiry, when).Wait(SyncTimeout); + => Context(flags).Keys().ExpireAt(key, expiry, when).Wait(SyncTimeout); public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags) - => Keys(flags).Expire(key, expiry).AsTask(); + => Context(flags).Keys().Expire(key, expiry).AsTask(); public Task KeyExpireAsync( RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) - => Keys(flags).Expire(key, expiry, when).AsTask(); + => Context(flags).Keys().Expire(key, expiry, when).AsTask(); public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags) - => Keys(flags).ExpireAt(key, expiry).AsTask(); + => Context(flags).Keys().ExpireAt(key, expiry).AsTask(); public Task KeyExpireAsync( RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) - => Keys(flags).ExpireAt(key, expiry, when).AsTask(); + => Context(flags).Keys().ExpireAt(key, expiry, when).AsTask(); public DateTime? KeyExpireTime(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).PExpireTime(key).Wait(SyncTimeout); + => Context(flags).Keys().PExpireTime(key).Wait(SyncTimeout); public Task KeyExpireTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).PExpireTime(key).AsTask(); + => Context(flags).Keys().PExpireTime(key).AsTask(); public long? KeyFrequency(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).ObjectFreq(key).Wait(SyncTimeout); + => Context(flags).Keys().ObjectFreq(key).Wait(SyncTimeout); public Task KeyFrequencyAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).ObjectFreq(key).AsTask(); + => Context(flags).Keys().ObjectFreq(key).AsTask(); public TimeSpan? KeyIdleTime(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).ObjectIdleTime(key).Wait(SyncTimeout); + => Context(flags).Keys().ObjectIdleTime(key).Wait(SyncTimeout); public Task KeyIdleTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).ObjectIdleTime(key).AsTask(); + => Context(flags).Keys().ObjectIdleTime(key).AsTask(); public bool KeyMove(RedisKey key, int database, CommandFlags flags = CommandFlags.None) - => Keys(flags).Move(key, database).Wait(SyncTimeout); + => Context(flags).Keys().Move(key, database).Wait(SyncTimeout); public Task KeyMoveAsync(RedisKey key, int database, CommandFlags flags = CommandFlags.None) - => Keys(flags).Move(key, database).AsTask(); + => Context(flags).Keys().Move(key, database).AsTask(); public bool KeyPersist(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).Persist(key).Wait(SyncTimeout); + => Context(flags).Keys().Persist(key).Wait(SyncTimeout); public Task KeyPersistAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).Persist(key).AsTask(); + => Context(flags).Keys().Persist(key).AsTask(); public RedisKey KeyRandom(CommandFlags flags = CommandFlags.None) - => Keys(flags).RandomKey().Wait(SyncTimeout); + => Context(flags).Keys().RandomKey().Wait(SyncTimeout); public Task KeyRandomAsync(CommandFlags flags = CommandFlags.None) - => Keys(flags).RandomKey().AsTask(); + => Context(flags).Keys().RandomKey().AsTask(); public long? KeyRefCount(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).ObjectRefCount(key).Wait(SyncTimeout); + => Context(flags).Keys().ObjectRefCount(key).Wait(SyncTimeout); public Task KeyRefCountAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).ObjectRefCount(key).AsTask(); + => Context(flags).Keys().ObjectRefCount(key).AsTask(); public bool KeyRename( RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None) - => Keys(flags).Rename(key, newKey, when).Wait(SyncTimeout); + => Context(flags).Keys().Rename(key, newKey, when).Wait(SyncTimeout); public Task KeyRenameAsync( RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None) - => Keys(flags).Rename(key, newKey, when).AsTask(); + => Context(flags).Keys().Rename(key, newKey, when).AsTask(); public void KeyRestore( RedisKey key, byte[] value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) - => Keys(flags).Restore(key, expiry, value).Wait(SyncTimeout); + => Context(flags).Keys().Restore(key, expiry, value).Wait(SyncTimeout); public Task KeyRestoreAsync( RedisKey key, byte[] value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) - => Keys(flags).Restore(key, expiry, value).AsTask(); + => Context(flags).Keys().Restore(key, expiry, value).AsTask(); public TimeSpan? KeyTimeToLive(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).Pttl(key).Wait(SyncTimeout); + => Context(flags).Keys().Pttl(key).Wait(SyncTimeout); public Task KeyTimeToLiveAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).Pttl(key).AsTask(); + => Context(flags).Keys().Pttl(key).AsTask(); public bool KeyTouch(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).Touch(key).Wait(SyncTimeout); + => Context(flags).Keys().Touch(key).Wait(SyncTimeout); public long KeyTouch(RedisKey[] keys, CommandFlags flags = CommandFlags.None) - => Keys(flags).Touch(keys).Wait(SyncTimeout); + => Context(flags).Keys().Touch(keys).Wait(SyncTimeout); public Task KeyTouchAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).Touch(key).AsTask(); + => Context(flags).Keys().Touch(key).AsTask(); public Task KeyTouchAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) - => Keys(flags).Touch(keys).AsTask(); + => Context(flags).Keys().Touch(keys).AsTask(); public RedisType KeyType(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).Type(key).Wait(SyncTimeout); + => Context(flags).Keys().Type(key).Wait(SyncTimeout); public Task KeyTypeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - => Keys(flags).Type(key).AsTask(); + => Context(flags).Keys().Type(key).AsTask(); } From 5705cc6051e93d77dce2afffd32efc618009a5d5 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 1 Oct 2025 16:49:49 +0100 Subject: [PATCH 093/108] cleanup --- src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs index 9ba447fbe..e8cf31e2f 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Key.cs @@ -1,5 +1,4 @@ -using System.Runtime.CompilerServices; -using StackExchange.Redis; +using StackExchange.Redis; namespace RESPite.StackExchange.Redis; From b6afb3180fb65f2911cd76f33944245e9d8b08cd Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 1 Oct 2025 16:51:04 +0100 Subject: [PATCH 094/108] nesting --- .../RESPite.StackExchange.Redis.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj b/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj index bb4dea30b..7f6d1952a 100644 --- a/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj +++ b/src/RESPite.StackExchange.Redis/RESPite.StackExchange.Redis.csproj @@ -27,5 +27,8 @@ RespContextDatabase.cs + + RedisCommands.cs + From a5b5663a435a7ef860f9c2cabbd2e5354a55863a Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 2 Oct 2025 16:57:19 +0100 Subject: [PATCH 095/108] fix list impls --- .../RespCommandGenerator.cs | 229 +++++++++++------- .../RedisCommands.ListCommands.cs | 188 ++++++++++++++ .../RespFormatters.cs | 17 ++ .../RespParsers.cs | 22 +- src/RESPite/RespIgnoreAttribute.cs | 15 ++ src/RESPite/RespPrefixAttribute.cs | 9 +- src/RESPite/RespSuffixAttribute.cs | 6 +- 7 files changed, 401 insertions(+), 85 deletions(-) create mode 100644 src/RESPite.StackExchange.Redis/RedisCommands.ListCommands.cs create mode 100644 src/RESPite/RespIgnoreAttribute.cs diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index 978fddcc7..ce4c598a8 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Diagnostics; +using System.Globalization; using System.Reflection; using System.Text; using Microsoft.CodeAnalysis; @@ -56,7 +57,18 @@ private readonly record struct ParameterTuple( ParameterFlags Flags, EasyArray Literals, string? ElementType, - int ArgIndex); + string? IgnoreExpression, + int ArgIndex) + { + // variable if collection, nullable, or an explicit ignore expression + public bool IsVariable => OptionalReasons != 0; + + public ParameterFlags OptionalReasons => + Flags & (ParameterFlags.Collection | ParameterFlags.Nullable | ParameterFlags.IgnoreExpression); + + public bool IsCollection => (Flags & ParameterFlags.Collection) != 0; + public bool IsNullable => (Flags & ParameterFlags.Nullable) != 0; + } private readonly record struct MethodTuple( string Namespace, @@ -87,6 +99,7 @@ private enum RESPite RespPrefixAttribute, RespSuffixAttribute, RespOperation, + RespIgnoreAttribute, } private static bool IsRESPite(ITypeSymbol? symbol, RESPite type) @@ -99,6 +112,7 @@ private static bool IsRESPite(ITypeSymbol? symbol, RESPite type) RESPite.RespPrefixAttribute => nameof(RESPite.RespPrefixAttribute), RESPite.RespSuffixAttribute => nameof(RESPite.RespSuffixAttribute), RESPite.RespOperation => nameof(RESPite.RespOperation), + RESPite.RespIgnoreAttribute => nameof(RESPite.RespIgnoreAttribute), _ => type.ToString(), }; @@ -384,6 +398,7 @@ static bool IsIndirectRespContext(ITypeSymbol type, out string memberName) int nextArgIndex = 0; foreach (var param in method.Parameters) { + string? ignoreExpression = null; var flags = ParameterFlags.Parameter; if (IsKey(param)) flags |= ParameterFlags.Key; var elementType = param.Type; @@ -441,9 +456,9 @@ void AddLiteral(string token, LiteralFlags literalFlags) AddNotes(ref debugNote, $"checking {param.Name} for literals"); foreach (var attrib in param.GetAttributes()) { - if (IsRESPite(attrib.AttributeClass, RESPite.RespPrefixAttribute)) + if (attrib.ConstructorArguments.Length == 1) { - if (attrib.ConstructorArguments.Length == 1) + if (IsRESPite(attrib.AttributeClass, RESPite.RespPrefixAttribute)) { if (attrib.ConstructorArguments[0].Value?.ToString() is { Length: > 0 } val) { @@ -451,11 +466,8 @@ void AddLiteral(string token, LiteralFlags literalFlags) AddLiteral(val, LiteralFlags.None); } } - } - if (IsRESPite(attrib.AttributeClass, RESPite.RespSuffixAttribute)) - { - if (attrib.ConstructorArguments.Length == 1) + if (IsRESPite(attrib.AttributeClass, RESPite.RespSuffixAttribute)) { if (attrib.ConstructorArguments[0].Value?.ToString() is { Length: > 0 } val) { @@ -463,12 +475,31 @@ void AddLiteral(string token, LiteralFlags literalFlags) AddLiteral(val, LiteralFlags.Suffix); } } + + if (IsRESPite(attrib.AttributeClass, RESPite.RespIgnoreAttribute)) + { + var val = attrib.ConstructorArguments[0].Value; + if (val is string s) + { + ignoreExpression = CodeLiteral(s); + } + else if (val is bool b) + { + ignoreExpression = b ? "true" : "false"; + } + else if (val is long l) + { + ignoreExpression = l.ToString(CultureInfo.InvariantCulture); + } + if (ignoreExpression is not null) flags |= ParameterFlags.IgnoreExpression; + } } } var literalArray = literals is null ? EasyArray.Empty : new(literals.ToArray()); var argIndex = (flags & ParameterFlags.Data) != 0 ? nextArgIndex++ : -1; - parameters.Add(new(GetFullName(param.Type), param.Name, modifiers, flags, literalArray, elementTypeName, argIndex)); + + parameters.Add(new(GetFullName(param.Type), param.Name, modifiers, flags, literalArray, elementTypeName, ignoreExpression, argIndex)); } var syntax = (MethodDeclarationSyntax)ctx.Node; @@ -952,87 +983,108 @@ void WriteParameterName(in ParameterTuple p, StringBuilder? target = null) int index; if (isVariable) { - sb = NewLine().Append("writer.WriteCommand(command,"); - bool firstVariableItem = true; - if (constantCount != 0) - { - sb.Append(" ").Append(constantCount).Append(" // constant args"); - firstVariableItem = false; - } - indent++; - index = 0; foreach (var parameter in parameters.Span) { - var match = parameter.Flags & (ParameterFlags.Collection | ParameterFlags.Nullable); - if (match != 0) + if (parameter.IsVariable) { - sb = NewLine(); - if (firstVariableItem) - { - firstVariableItem = false; - } - else - { - sb.Append("+ "); - } - - var literalCount = parameter.Literals.Length; - switch (match) + sb = NewLine().Append("bool __inc").Append(parameter.ArgIndex).Append(" = "); + WriteParameterName(parameter); + switch (parameter.OptionalReasons) { case ParameterFlags.Nullable: - sb.Append("("); - WriteParameterName(parameter); - sb.Append(" is null ? 0 : ").Append(1 + literalCount).Append(")"); + sb.Append(" is not null"); + break; + case ParameterFlags.Nullable | ParameterFlags.IgnoreExpression: + sb.Append(" is { } __val").Append(parameter.ArgIndex) + .Append(" && __val").Append(parameter.ArgIndex) + .Append(" != ").Append(parameter.IgnoreExpression); + break; + case ParameterFlags.IgnoreExpression: + sb.Append(" != ").Append(parameter.IgnoreExpression); break; case ParameterFlags.Collection: // non-nullable collection; literals already handled switch (parameter.Flags & (ParameterFlags.CollectionWithCount | ParameterFlags.ImmutableArray)) { case ParameterFlags.CollectionWithCount: - WriteParameterName(parameter); - sb.Append(".Count"); + sb.Append(".Count != 0"); break; case ParameterFlags.ImmutableArray: // needs special care because of default (breaks .Length) - sb.Append("("); - WriteParameterName(parameter); - sb.Append(".IsDefaultOrEmpty ? 0 : "); - WriteParameterName(parameter); - sb.Append(".Length)"); + sb.Append(".IsDefaultOrEmpty == false"); break; default: - WriteParameterName(parameter); - sb.Append(".Length"); + sb.Append(".Length != 0"); break; } break; case ParameterFlags.Collection | ParameterFlags.Nullable: - sb.Append("("); - WriteParameterName(parameter); - sb.Append(" is null ? 0 : "); - if (literalCount != 0) sb.Append("("); - switch (parameter.Flags & ParameterFlags.CollectionWithCount) + sb.Append(" is { "); + switch (parameter.Flags & (ParameterFlags.CollectionWithCount | ParameterFlags.ImmutableArray)) { case ParameterFlags.CollectionWithCount: - WriteParameterName(parameter); - sb.Append(".Count"); + sb.Append("Count: > 0"); break; case ParameterFlags.ImmutableArray: // needs special care because of default (breaks .Length) - sb.Append("("); - WriteParameterName(parameter); - sb.Append(".GetValueOrDefault().IsDefaultOrEmpty ? 0 : "); - WriteParameterName(parameter); - sb.Append(".GetValueOrDefault().Length)"); + sb.Append("IsDefaultOrEmpty: false"); break; default: - WriteParameterName(parameter); - sb.Append(".Length"); + sb.Append("Length: > 0"); break; } - if (literalCount != 0) sb.Append(" + ").Append(literalCount).Append(")"); - sb.Append(")"); + sb.Append("}"); break; + default: + sb.Append($" false /* unhandled combination! */"); + break; + } + sb.Append("; // ").Append(parameter.OptionalReasons); + } + } + + sb = NewLine().Append("writer.WriteCommand(command,"); + bool firstVariableItem = true; + if (constantCount != 0) + { + sb.Append(" ").Append(constantCount).Append(" // constant args"); + firstVariableItem = false; + } + indent++; + index = 0; + foreach (var parameter in parameters.Span) + { + if (parameter.IsVariable) + { + sb = NewLine(); + if (firstVariableItem) + { + firstVariableItem = false; + } + else + { + sb.Append("+ "); + } + sb.Append("(__inc").Append(parameter.ArgIndex).Append(" ? "); + var literalCount = parameter.Literals.Length; + if (!parameter.IsCollection) + { + sb.Append(1 + literalCount); + } + else + { + sb.Append("("); + WriteParameterName(parameter); + if (parameter.IsNullable) sb.Append("!"); + sb.Append((parameter.Flags & ParameterFlags.CollectionWithCount) == 0 ? ".Length" : ".Count"); + sb.Append(" + ").Append(1 + literalCount).Append(")"); + } + + sb.Append(" : 0)"); + if (!parameter.IsCollection) + { + // help identify what this is (not needed for collections, since foo.Count etc) + sb.Append(" // "); + WriteParameterName(parameter); } - sb.Append(" // ").Append(match); } index++; } @@ -1063,20 +1115,36 @@ void WriteParameterName(in ParameterTuple p, StringBuilder? target = null) NewLine().Append("writer.WriteCommand(command, ").Append(constantCount).Append(");"); } - void WritePrefix(ParameterTuple p) => WriteLiteral(p, false); - void WriteSuffix(ParameterTuple p) => WriteLiteral(p, true); + void WritePrefix(in ParameterTuple p) => WriteLiteral(p, false); + void WriteSuffix(in ParameterTuple p) => WriteLiteral(p, true); - void WriteLiteral(ParameterTuple p, bool suffix) + void WriteLiteral(in ParameterTuple p, bool suffix) { LiteralFlags match = suffix ? LiteralFlags.Suffix : LiteralFlags.None; foreach (var literal in p.Literals.Span) { if ((literal.Flags & LiteralFlags.Suffix) == match) { - var len = Encoding.UTF8.GetByteCount(literal.Token); - var resp = $"${len}\r\n{literal.Token}\r\n"; - NewLine().Append("writer.WriteRaw(").Append(CodeLiteral(resp)).Append("u8); // ") - .Append(literal.Token); + if (string.IsNullOrEmpty(literal.Token)) + { + if (p.IsCollection) + { + WriteParameterName(p); + if (p.IsNullable) sb.Append("!"); + sb.Append((p.Flags & ParameterFlags.CollectionWithCount) == 0 ? ".Length" : ".Count"); + } + else + { + NewLine().Append("#error empty literal for ").Append(p.Name).AppendLine(); + } + } + else + { + var len = Encoding.UTF8.GetByteCount(literal.Token); + var resp = $"${len}\r\n{literal.Token}\r\n"; + NewLine().Append("writer.WriteRaw(").Append(CodeLiteral(resp)).Append("u8); // ") + .Append(literal.Token); + } } } } @@ -1086,23 +1154,20 @@ void WriteLiteral(ParameterTuple p, bool suffix) { if ((parameter.Flags & ParameterFlags.DataParameter) == ParameterFlags.DataParameter) { - bool isNullable = (parameter.Flags & ParameterFlags.Nullable) != 0; - bool isCollection = (parameter.Flags & ParameterFlags.Collection) != 0; - if (isNullable) + if (parameter.IsVariable) { - sb = NewLine().Append("if ("); - WriteParameterName(parameter); - sb.Append(" is not null)"); + sb = NewLine().Append("if (__inc").Append(parameter.ArgIndex).Append(")"); NewLine().Append("{"); indent++; } WritePrefix(parameter); var elementType = parameter.ElementType ?? parameter.Type; - if (isCollection) + if (parameter.IsCollection) { sb = NewLine().Append("foreach (").Append(elementType).Append(" val in "); WriteParameterName(parameter); + if (parameter.IsNullable) sb.Append("!"); sb.Append(")"); NewLine().Append("{"); indent++; @@ -1120,7 +1185,7 @@ void WriteLiteral(ParameterTuple p, bool suffix) } sb.Append("("); - if (isCollection) + if (parameter.IsCollection) { sb.Append("val"); } @@ -1130,14 +1195,14 @@ void WriteLiteral(ParameterTuple p, bool suffix) } sb.Append(");"); - if (isCollection) + if (parameter.IsCollection) { indent--; NewLine().Append("}"); } WriteSuffix(parameter); - if (isNullable) + if (parameter.IsVariable) { indent--; NewLine().Append("}"); @@ -1266,19 +1331,20 @@ private static int DataParameterCount( { if ((parameter.Flags & ParameterFlags.DataParameter) == ParameterFlags.DataParameter) { + bool thisParamIsVariable = false; count++; - if ((parameter.Flags & (ParameterFlags.Collection | ParameterFlags.Nullable)) != 0) + if (parameter.IsVariable) { - isVariable = true; // variable if either collection or nullable + isVariable = thisParamIsVariable = true; } else { constantCount++; } - if ((parameter.Flags & ParameterFlags.Nullable) == 0 & !parameter.Literals.IsEmpty) + if (!(thisParamIsVariable | parameter.Literals.IsEmpty)) { - constantCount += parameter.Literals.Length; // we include literals if not nullable + constantCount += parameter.Literals.Length; // we include literals if not variable } } } @@ -1367,6 +1433,7 @@ private enum ParameterFlags Collection = 1 << 6, CollectionWithCount = 1 << 7, // has .Count, otherwise assumed to have .Length ImmutableArray = 1 << 8, + IgnoreExpression = 1 << 9, } // compares whether a formatter can be shared, which depends on the key index and types (not names) diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.ListCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.ListCommands.cs new file mode 100644 index 000000000..40004c22c --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RedisCommands.ListCommands.cs @@ -0,0 +1,188 @@ +using System.Runtime.CompilerServices; +using RESPite.Messages; +using StackExchange.Redis; + +// ReSharper disable InconsistentNaming +namespace RESPite.StackExchange.Redis; + +internal static partial class RedisCommands +{ + // this is just a "type pun" - it should be an invisible/magic pointer cast to the JIT + public static ref readonly ListCommands Lists(this in RespContext context) + => ref Unsafe.As(ref Unsafe.AsRef(in context)); +} + +public readonly struct ListCommands(in RespContext context) +{ + public readonly RespContext Context = context; // important: this is the only field +} + +internal static partial class ListCommandsExtensions +{ + /* + [RespCommand] + public static partial RespOperation BLMove( + this in ListCommands context, + RedisKey source, + RedisKey destination, + ListSide sourceSide, + ListSide destinationSide, + double timeoutSeconds); + + [RespCommand] + public static partial RespOperation BLMPop( + this in ListCommands context, + [RespKey] RedisKey[] keys, + ListSide side, + long count, + double timeoutSeconds); + + [RespCommand] + public static partial RespOperation BLPop( + this in ListCommands context, + [RespKey] RedisKey[] keys, + double timeoutSeconds); + + [RespCommand] + public static partial RespOperation BRPop( + this in ListCommands context, + [RespKey] RedisKey[] keys, + double timeoutSeconds); + + [RespCommand] + public static partial RespOperation BRPopLPush( + this in ListCommands context, + RedisKey source, + RedisKey destination, + double timeoutSeconds); + */ + + [RespCommand] + public static partial RespOperation LIndex(this in ListCommands context, RedisKey key, long index); + + [RespCommand(Formatter = "LInsertFormatter.Instance")] + public static partial RespOperation LInsert( + this in ListCommands context, + RedisKey key, + bool insertBefore, + RedisValue pivot, + RedisValue element); + + private sealed class + LInsertFormatter : IRespFormatter<(RedisKey Key, bool InsertBefore, RedisValue Pivot, RedisValue Element)> + { + public static readonly LInsertFormatter Instance = new(); + private LInsertFormatter() { } + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, bool InsertBefore, RedisValue Pivot, RedisValue Element) request) + { + writer.WriteCommand(command, 4); + writer.Write(request.Key); + writer.WriteRaw(request.InsertBefore ? "$6\r\nBEFORE\r\n"u8 : "$5\r\nAFTER\r\n"u8); + writer.Write(request.Pivot); + writer.Write(request.Element); + } + } + + [RespCommand] + public static partial RespOperation LLen(this in ListCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation LMove( + this in ListCommands context, + RedisKey source, + RedisKey destination, + ListSide sourceSide, + ListSide destinationSide); + + [RespCommand] + public static partial RespOperation LPop(this in ListCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation LPop(this in ListCommands context, RedisKey key, long count); + + [RespCommand(Parser = "RespParsers.Int64Index")] + public static partial RespOperation LPos( + this in ListCommands context, + RedisKey key, + RedisValue element, + [RespPrefix("RANK"), RespIgnore(1)] long rank = 1, + [RespPrefix("MAXLEN"), RespIgnore(0)] long maxLen = 0); + + [RespCommand] + public static partial RespOperation LPos( + this in ListCommands context, + RedisKey key, + RedisValue element, + [RespPrefix("RANK"), RespIgnore(1)] long rank, + [RespPrefix("MAXLEN"), RespIgnore(0)] long maxLen, + [RespPrefix("COUNT")] long count); + + [RespCommand] + public static partial RespOperation LPush(this in ListCommands context, RedisKey key, RedisValue element); + + [RespCommand] + public static partial RespOperation LPush(this in ListCommands context, RedisKey key, RedisValue[] elements); + + [RespCommand] + public static partial RespOperation LPushX(this in ListCommands context, RedisKey key, RedisValue element); + + [RespCommand] + public static partial RespOperation LPushX(this in ListCommands context, RedisKey key, RedisValue[] elements); + + [RespCommand] + public static partial RespOperation LRange( + this in ListCommands context, + RedisKey key, + long start, + long stop); + + [RespCommand] + public static partial RespOperation LRem( + this in ListCommands context, + RedisKey key, + long count, + RedisValue element); + + [RespCommand] + public static partial RespOperation LSet( + this in ListCommands context, + RedisKey key, + long index, + RedisValue element); + + [RespCommand] + public static partial RespOperation LTrim(this in ListCommands context, RedisKey key, long start, long stop); + + [RespCommand] + public static partial RespOperation RPop(this in ListCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation RPop(this in ListCommands context, RedisKey key, long count); + + [RespCommand] + public static partial RespOperation RPopLPush(this in ListCommands context, RedisKey source, + RedisKey destination); + + [RespCommand] + public static partial RespOperation RPush(this in ListCommands context, RedisKey key, RedisValue element); + + [RespCommand] + public static partial RespOperation RPush(this in ListCommands context, RedisKey key, RedisValue[] elements); + + [RespCommand] + public static partial RespOperation RPushX(this in ListCommands context, RedisKey key, RedisValue element); + + [RespCommand] + public static partial RespOperation RPushX(this in ListCommands context, RedisKey key, RedisValue[] elements); + + [RespCommand(Parser = "RespParsers.ListPopResult")] + public static partial RespOperation LMPop( + this in ListCommands context, + [RespPrefix, RespKey] RedisKey[] keys, + ListSide side, + [RespIgnore(1), RespPrefix("COUNT")] long count = 1); +} diff --git a/src/RESPite.StackExchange.Redis/RespFormatters.cs b/src/RESPite.StackExchange.Redis/RespFormatters.cs index b6537f15e..35884dc5b 100644 --- a/src/RESPite.StackExchange.Redis/RespFormatters.cs +++ b/src/RESPite.StackExchange.Redis/RespFormatters.cs @@ -57,6 +57,23 @@ public static void Write(this ref RespWriter writer, in RedisKey key) } } + internal static void WriteBulkString(this ref RespWriter writer, ListSide side) + { + switch (side) + { + case ListSide.Left: + writer.WriteRaw("$4\r\nLEFT\r\n"u8); + break; + case ListSide.Right: + writer.WriteRaw("$5\r\nRIGHT\r\n"u8); + break; + default: + Throw(); + break; + } + static void Throw() => throw new ArgumentOutOfRangeException(nameof(side)); + } + // ReSharper disable once MemberCanBePrivate.Global public static void Write(this ref RespWriter writer, in RedisValue value) { diff --git a/src/RESPite.StackExchange.Redis/RespParsers.cs b/src/RESPite.StackExchange.Redis/RespParsers.cs index 9ad995ec1..4e8dc6d8e 100644 --- a/src/RESPite.StackExchange.Redis/RespParsers.cs +++ b/src/RESPite.StackExchange.Redis/RespParsers.cs @@ -15,6 +15,8 @@ public static class RespParsers public static IRespParser DateTimeFromSeconds => TimeParser.FromSeconds; public static IRespParser TimeSpanFromMilliseconds => TimeParser.FromMilliseconds; public static IRespParser DateTimeFromMilliseconds => TimeParser.FromMilliseconds; + internal static IRespParser Int64Index => Int64DefaultNegativeOneParser.Instance; + internal static IRespParser ListPopResult => DefaultParser.Instance; public static RedisValue ReadRedisValue(ref RespReader reader) { @@ -40,7 +42,7 @@ public static RedisKey ReadRedisKey(ref RespReader reader) private sealed class DefaultParser : IRespParser, IRespParser, IRespParser>, IRespParser, IRespParser, - IRespParser + IRespParser, IRespParser { private DefaultParser() { } public static readonly DefaultParser Instance = new(); @@ -92,9 +94,27 @@ HashEntry[] IRespParser.Parse(ref RespReader reader) return result; */ } + + ListPopResult IRespParser.Parse(ref RespReader reader) + { + if (reader.IsNull) return global::StackExchange.Redis.ListPopResult.Null; + reader.DemandAggregate(); + reader.MoveNext(); + var key = ReadRedisKey(ref reader); + reader.MoveNext(); + var arr = reader.ReadArray(SharedReadRedisValue, scalar: true)!; + return new(key, arr); + } } } +internal sealed class Int64DefaultNegativeOneParser : IRespParser, IRespInlineParser +{ + private Int64DefaultNegativeOneParser() { } + public static readonly Int64DefaultNegativeOneParser Instance = new(); + public long Parse(ref RespReader reader) => reader.IsNull ? -1 : reader.ReadInt64(); +} + internal sealed class TimeParser : IRespParser, IRespParser, IRespInlineParser { private readonly bool _millis; diff --git a/src/RESPite/RespIgnoreAttribute.cs b/src/RESPite/RespIgnoreAttribute.cs new file mode 100644 index 000000000..bd44a9716 --- /dev/null +++ b/src/RESPite/RespIgnoreAttribute.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; +using System.Diagnostics; + +namespace RESPite; + +[AttributeUsage(AttributeTargets.Parameter)] +[Conditional("DEBUG"), ImmutableObject(true)] +public sealed class RespIgnoreAttribute : Attribute +{ + private readonly object _value; + public object Value => _value; + public RespIgnoreAttribute(string value) => _value = value; + public RespIgnoreAttribute(long value) => _value = value; + public RespIgnoreAttribute(bool value) => _value = value; +} diff --git a/src/RESPite/RespPrefixAttribute.cs b/src/RESPite/RespPrefixAttribute.cs index 7a6f51c9a..e9d4fd438 100644 --- a/src/RESPite/RespPrefixAttribute.cs +++ b/src/RESPite/RespPrefixAttribute.cs @@ -1,7 +1,12 @@ -namespace RESPite; +using System.ComponentModel; +using System.Diagnostics; +namespace RESPite; + +// note: omitting the token means that a collection-count prefix will be written [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] -public sealed class RespPrefixAttribute(string token) : Attribute +[Conditional("DEBUG"), ImmutableObject(true)] +public sealed class RespPrefixAttribute(string token = "") : Attribute { public string Token => token; } diff --git a/src/RESPite/RespSuffixAttribute.cs b/src/RESPite/RespSuffixAttribute.cs index e8ab23661..5bd8e3515 100644 --- a/src/RESPite/RespSuffixAttribute.cs +++ b/src/RESPite/RespSuffixAttribute.cs @@ -1,6 +1,10 @@ -namespace RESPite; +using System.ComponentModel; +using System.Diagnostics; + +namespace RESPite; [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] +[Conditional("DEBUG"), ImmutableObject(true)] public sealed class RespSuffixAttribute(string token) : Attribute { public string Token => token; From 6afdfa1fd3d44be3c5ec8d40bde40d7599775a1d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 3 Oct 2025 10:27:38 +0100 Subject: [PATCH 096/108] finish lists --- .../RedisCommands.ListCommands.cs | 36 +- .../RespContextDatabase.List.cs | 353 +++++++++--------- 2 files changed, 204 insertions(+), 185 deletions(-) diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.ListCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.ListCommands.cs index 40004c22c..658b4cfe7 100644 --- a/src/RESPite.StackExchange.Redis/RedisCommands.ListCommands.cs +++ b/src/RESPite.StackExchange.Redis/RedisCommands.ListCommands.cs @@ -124,6 +124,38 @@ public static partial RespOperation LPos( [RespCommand] public static partial RespOperation LPush(this in ListCommands context, RedisKey key, RedisValue element); + internal static RespOperation Push(this in ListCommands context, RedisKey key, RedisValue element, ListSide side, When when) + { + switch (when) + { + case When.Always: + return side == ListSide.Left ? LPush(context, key, element) : RPush(context, key, element); + case When.Exists: + return side == ListSide.Left ? LPushX(context, key, element) : RPushX(context, key, element); + default: + when.AlwaysOrExists(); // throws + return default; + } + } + + internal static RespOperation Push(this in ListCommands context, RedisKey key, RedisValue[] elements, ListSide side, When when) + { + switch (when) + { + case When.Always when elements.Length == 1: + return side == ListSide.Left ? LPush(context, key, elements[0]) : RPush(context, key, elements[0]); + case When.Always when elements.Length > 1: + return side == ListSide.Left ? LPush(context, key, elements) : RPush(context, key, elements); + case When.Exists when elements.Length == 1: + return side == ListSide.Left ? LPushX(context, key, elements[0]) : RPushX(context, key, elements[0]); + case When.Exists when elements.Length > 1: + return side == ListSide.Left ? LPushX(context, key, elements) : RPushX(context, key, elements); + default: + when.AlwaysOrExists(); // check that "when" is valid + return LLen(context, key); // handle zero case (no insert, just get length) + } + } + [RespCommand] public static partial RespOperation LPush(this in ListCommands context, RedisKey key, RedisValue[] elements); @@ -164,7 +196,9 @@ public static partial RespOperation LSet( public static partial RespOperation RPop(this in ListCommands context, RedisKey key, long count); [RespCommand] - public static partial RespOperation RPopLPush(this in ListCommands context, RedisKey source, + public static partial RespOperation RPopLPush( + this in ListCommands context, + RedisKey source, RedisKey destination); [RespCommand] diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.List.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.List.cs index b39d72749..995950ee4 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.List.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.List.cs @@ -4,164 +4,129 @@ namespace RESPite.StackExchange.Redis; internal partial class RespContextDatabase { - // Async List methods - public Task ListGetByIndexAsync(RedisKey key, long index, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue ListGetByIndex(RedisKey key, long index, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LIndex(key, index).Wait(SyncTimeout); - public Task ListInsertAfterAsync( + public Task ListGetByIndexAsync(RedisKey key, long index, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LIndex(key, index).AsTask(); + + public long ListInsertAfter( RedisKey key, RedisValue pivot, RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LInsert(key, false, pivot, value).Wait(SyncTimeout); - public Task ListInsertBeforeAsync( + public Task ListInsertAfterAsync( RedisKey key, RedisValue pivot, RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListLeftPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListPositionAsync( - RedisKey key, - RedisValue element, - long rank = 1, - long maxLength = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LInsert(key, false, pivot, value).AsTask(); - public Task ListPositionsAsync( + public long ListInsertBefore( RedisKey key, - RedisValue element, - long count, - long rank = 1, - long maxLength = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + RedisValue pivot, + RedisValue value, + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LInsert(key, true, pivot, value).Wait(SyncTimeout); - public Task ListLeftPushAsync( + public Task ListInsertBeforeAsync( RedisKey key, + RedisValue pivot, RedisValue value, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => when switch - { - When.Always => LPushAsync(key, value, flags), - When.Exists => LPushXAsync(key, value, flags), - _ => Task.FromResult(NotSupportedInt64(when)), - }; + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LInsert(key, true, pivot, value).AsTask(); - public Task ListLeftPushAsync( - RedisKey key, - RedisValue[] values, - When when, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LPop(key).Wait(SyncTimeout); - public Task ListLeftPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LPop(key, count).Wait(SyncTimeout); - public Task ListMoveAsync( - RedisKey sourceKey, - RedisKey destinationKey, - ListSide sourceSide, - ListSide destinationSide, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public ListPopResult ListLeftPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LMPop(keys, ListSide.Left, count).Wait(SyncTimeout); - public Task ListRemoveAsync( - RedisKey key, - RedisValue value, - long count = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task ListLeftPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LPop(key).AsTask(); - public Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LPop(key, count).AsTask(); - public Task ListRightPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task ListLeftPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LMPop(keys, ListSide.Left, count).AsTask(); - public Task ListRightPopLeftPushAsync( - RedisKey source, - RedisKey destination, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task ListRightPushAsync( + public long ListLeftPush( RedisKey key, RedisValue value, When when = When.Always, - CommandFlags flags = CommandFlags.None) => when switch - { - When.Always => RPushAsync(key, value, flags), - When.Exists => RPushXAsync(key, value, flags), - _ => Task.FromResult(NotSupportedInt64(when)), - }; + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().Push(key, value, ListSide.Left, when).Wait(SyncTimeout); - public Task ListRightPushAsync( + public long ListLeftPush( RedisKey key, RedisValue[] values, When when, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().Push(key, values, ListSide.Left, when).Wait(SyncTimeout); - public Task ListRightPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public long ListLeftPush(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LPush(key, values).Wait(SyncTimeout); - public Task ListSetByIndexAsync( + public Task ListLeftPushAsync( RedisKey key, - long index, RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + When when = When.Always, + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().Push(key, value, ListSide.Left, when).AsTask(); - public Task ListTrimAsync( + public Task ListLeftPushAsync( RedisKey key, - long start, - long stop, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + RedisValue[] values, + When when, + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().Push(key, values, ListSide.Left, when).AsTask(); - // Synchronous List methods - public RedisValue ListGetByIndex(RedisKey key, long index, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task ListLeftPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LPush(key, values).AsTask(); - public long ListInsertAfter( - RedisKey key, - RedisValue pivot, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public long ListLength(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LLen(key).Wait(SyncTimeout); - public long ListInsertBefore( - RedisKey key, - RedisValue pivot, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - [RespCommand("lpop")] - public partial RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.None); + public Task ListLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LLen(key).AsTask(); - public RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue ListMove( + RedisKey sourceKey, + RedisKey destinationKey, + ListSide sourceSide, + ListSide destinationSide, + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LMove(sourceKey, destinationKey, sourceSide, destinationSide).Wait(SyncTimeout); - public ListPopResult ListLeftPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task ListMoveAsync( + RedisKey sourceKey, + RedisKey destinationKey, + ListSide sourceSide, + ListSide destinationSide, + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LMove(sourceKey, destinationKey, sourceSide, destinationSide).AsTask(); public long ListPosition( RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LPos(key, element, rank, maxLength).Wait(SyncTimeout); + + public Task ListPositionAsync( + RedisKey key, + RedisValue element, + long rank = 1, + long maxLength = 0, + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LPos(key, element, rank, maxLength).AsTask(); public long[] ListPositions( RedisKey key, @@ -169,115 +134,135 @@ public long[] ListPositions( long count, long rank = 1, long maxLength = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LPos(key, element, rank, maxLength, count).Wait(SyncTimeout); - public long ListLeftPush( + public Task ListPositionsAsync( RedisKey key, - RedisValue value, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => when switch - { - When.Always => LPush(key, value, flags), - When.Exists => LPushX(key, value, flags), - _ => NotSupportedInt64(when), - }; - - private static long NotSupportedInt64(When when) => throw new NotSupportedException( - $"The condition '{when}' is not supported for this command"); - - [RespCommand] - private partial long LPush(RedisKey key, RedisValue value, CommandFlags flags); - [RespCommand] - private partial long LPushX(RedisKey key, RedisValue value, CommandFlags flags); + RedisValue element, + long count, + long rank = 1, + long maxLength = 0, + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LPos(key, element, rank, maxLength, count).AsTask(); - public long ListLeftPush( + public RedisValue[] ListRange( RedisKey key, - RedisValue[] values, - When when, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public long ListLeftPush(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - [RespCommand("LLEN")] - public partial long ListLength(RedisKey key, CommandFlags flags = CommandFlags.None); - - public RedisValue ListMove( - RedisKey sourceKey, - RedisKey destinationKey, - ListSide sourceSide, - ListSide destinationSide, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + long start, + long stop, + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LRange(key, start, stop).Wait(SyncTimeout); - [RespCommand("lrange")] - public partial RedisValue[] ListRange( + public Task ListRangeAsync( RedisKey key, long start, long stop, - CommandFlags flags = CommandFlags.None); + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LRange(key, start, stop).AsTask(); public long ListRemove( RedisKey key, RedisValue value, long count = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LRem(key, count, value).Wait(SyncTimeout); + + public Task ListRemoveAsync( + RedisKey key, + RedisValue value, + long count = 0, + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LRem(key, count, value).AsTask(); + + public RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().RPop(key).Wait(SyncTimeout); + + public RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().RPop(key, count).Wait(SyncTimeout); + + public ListPopResult ListRightPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LMPop(keys, ListSide.Right, count).Wait(SyncTimeout); - [RespCommand("rpop")] - public partial RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.None); + public Task ListRightPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().RPop(key).AsTask(); - public RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().RPop(key, count).AsTask(); - public ListPopResult ListRightPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task ListRightPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LMPop(keys, ListSide.Right, count).AsTask(); public RedisValue ListRightPopLeftPush( RedisKey source, RedisKey destination, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().RPopLPush(source, destination).Wait(SyncTimeout); + + public Task ListRightPopLeftPushAsync( + RedisKey source, + RedisKey destination, + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().RPopLPush(source, destination).AsTask(); public long ListRightPush( RedisKey key, RedisValue value, When when = When.Always, - CommandFlags flags = CommandFlags.None) => when switch - { - When.Always => RPush(key, value, flags), - When.Exists => RPushX(key, value, flags), - _ => NotSupportedInt64(when), - }; - - [RespCommand] - private partial long RPush(RedisKey key, RedisValue value, CommandFlags flags); - [RespCommand] - private partial long RPushX(RedisKey key, RedisValue value, CommandFlags flags); + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().Push(key, value, ListSide.Right, when).Wait(SyncTimeout); public long ListRightPush( RedisKey key, RedisValue[] values, When when, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().Push(key, values, ListSide.Right, when).Wait(SyncTimeout); + + public long ListRightPush(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().RPush(key, values).Wait(SyncTimeout); + + public Task ListRightPushAsync( + RedisKey key, + RedisValue value, + When when = When.Always, + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().Push(key, value, ListSide.Right, when).AsTask(); + + public Task ListRightPushAsync( + RedisKey key, + RedisValue[] values, + When when, + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().Push(key, values, ListSide.Right, when).AsTask(); - public long ListRightPush(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task ListRightPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().RPush(key, values).AsTask(); public void ListSetByIndex( RedisKey key, long index, RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LSet(key, index, value).Wait(SyncTimeout); + + public Task ListSetByIndexAsync( + RedisKey key, + long index, + RedisValue value, + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LSet(key, index, value).AsTask(); public void ListTrim( RedisKey key, long start, long stop, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LTrim(key, start, stop).Wait(SyncTimeout); + + public Task ListTrimAsync( + RedisKey key, + long start, + long stop, + CommandFlags flags = CommandFlags.None) + => Context(flags).Lists().LTrim(key, start, stop).AsTask(); } From 87aff3fe7c2b341bb52c94330035c9e56697a427 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 3 Oct 2025 11:18:25 +0100 Subject: [PATCH 097/108] set operations --- .../RedisCommands.SetCommands.cs | 154 +++++++++++++ .../RespContextDatabase.Set.cs | 215 +++++++++--------- 2 files changed, 264 insertions(+), 105 deletions(-) create mode 100644 src/RESPite.StackExchange.Redis/RedisCommands.SetCommands.cs diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.SetCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.SetCommands.cs new file mode 100644 index 000000000..b44dbef8d --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RedisCommands.SetCommands.cs @@ -0,0 +1,154 @@ +using System.Runtime.CompilerServices; +using StackExchange.Redis; + +// ReSharper disable InconsistentNaming +// ReSharper disable MemberCanBePrivate.Global +namespace RESPite.StackExchange.Redis; + +internal static partial class RedisCommands +{ + // this is just a "type pun" - it should be an invisible/magic pointer cast to the JIT + public static ref readonly SetCommands Sets(this in RespContext context) + => ref Unsafe.As(ref Unsafe.AsRef(in context)); +} + +public readonly struct SetCommands(in RespContext context) +{ + public readonly RespContext Context = context; // important: this is the only field +} + +internal static partial class SetCommandsExtensions +{ + [RespCommand] + public static partial RespOperation SAdd(this in SetCommands context, RedisKey key, RedisValue member); + + [RespCommand] + public static partial RespOperation SAdd(this in SetCommands context, RedisKey key, RedisValue[] members); + + [RespCommand] + public static partial RespOperation SCard(this in SetCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation SDiff(this in SetCommands context, RedisKey first, RedisKey second); + + [RespCommand] + public static partial RespOperation SDiff(this in SetCommands context, RedisKey[] keys); + + [RespCommand] + public static partial RespOperation SDiffStore(this in SetCommands context, RedisKey destination, RedisKey first, RedisKey second); + + [RespCommand] + public static partial RespOperation SDiffStore(this in SetCommands context, RedisKey destination, RedisKey[] keys); + + [RespCommand] + public static partial RespOperation SInter(this in SetCommands context, RedisKey first, RedisKey second); + + [RespCommand] + public static partial RespOperation SInter(this in SetCommands context, RedisKey[] keys); + + [RespCommand] + public static partial RespOperation SInterCard(this in SetCommands context, RedisKey first, RedisKey second, long limit = 0); + + [RespCommand] + public static partial RespOperation SInterCard(this in SetCommands context, RedisKey[] keys, long limit = 0); + + [RespCommand] + public static partial RespOperation SInterStore(this in SetCommands context, RedisKey destination, RedisKey first, RedisKey second); + + [RespCommand] + public static partial RespOperation SInterStore(this in SetCommands context, RedisKey destination, RedisKey[] keys); + + [RespCommand] + public static partial RespOperation SIsMember(this in SetCommands context, RedisKey key, RedisValue member); + + [RespCommand] + public static partial RespOperation SMIsMember(this in SetCommands context, RedisKey key, RedisValue[] members); + + [RespCommand] + public static partial RespOperation SMembers(this in SetCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation SMove(this in SetCommands context, RedisKey source, RedisKey destination, RedisValue member); + + [RespCommand] + public static partial RespOperation SPop(this in SetCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation SPop(this in SetCommands context, RedisKey key, long count); + + [RespCommand] + public static partial RespOperation SRandMember(this in SetCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation SRandMember(this in SetCommands context, RedisKey key, long count); + + [RespCommand] + public static partial RespOperation SRem(this in SetCommands context, RedisKey key, RedisValue member); + + [RespCommand] + public static partial RespOperation SRem(this in SetCommands context, RedisKey key, RedisValue[] members); + + [RespCommand] + public static partial RespOperation SUnion(this in SetCommands context, RedisKey first, RedisKey second); + + [RespCommand] + public static partial RespOperation SUnion(this in SetCommands context, RedisKey[] keys); + + [RespCommand] + public static partial RespOperation SUnionStore(this in SetCommands context, RedisKey destination, RedisKey first, RedisKey second); + + [RespCommand] + public static partial RespOperation SUnionStore(this in SetCommands context, RedisKey destination, RedisKey[] keys); + + internal static RespOperation CombineStore( + this in SetCommands context, + SetOperation operation, + RedisKey destination, + RedisKey first, + RedisKey second) => + operation switch + { + SetOperation.Difference => context.SDiffStore(destination, first, second), + SetOperation.Intersect => context.SInterStore(destination, first, second), + SetOperation.Union => context.SUnionStore(destination, first, second), + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; + + internal static RespOperation CombineStore( + this in SetCommands context, + SetOperation operation, + RedisKey destination, + RedisKey[] keys) => + operation switch + { + SetOperation.Difference => context.SDiffStore(destination, keys), + SetOperation.Intersect => context.SInterStore(destination, keys), + SetOperation.Union => context.SUnionStore(destination, keys), + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; + + internal static RespOperation Combine( + this in SetCommands context, + SetOperation operation, + RedisKey first, + RedisKey second) => + operation switch + { + SetOperation.Difference => context.SDiff(first, second), + SetOperation.Intersect => context.SInter(first, second), + SetOperation.Union => context.SUnion(first, second), + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; + + internal static RespOperation Combine( + this in SetCommands context, + SetOperation operation, + RedisKey[] keys) => + operation switch + { + SetOperation.Difference => context.SDiff(keys), + SetOperation.Intersect => context.SInter(keys), + SetOperation.Union => context.SUnion(keys), + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; +} diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Set.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Set.cs index 6410ee4f4..4ccde4b67 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Set.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Set.cs @@ -4,158 +4,154 @@ namespace RESPite.StackExchange.Redis; internal partial class RespContextDatabase { - // Async Set methods - public Task SetAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + // Synchronous Set methods + public bool SetAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SAdd(key, value).Wait(SyncTimeout); - public Task SetCombineAsync( + public long SetAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SAdd(key, values).Wait(SyncTimeout); + + public Task SetAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SAdd(key, value).AsTask(); + + public Task SetAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SAdd(key, values).AsTask(); + + public RedisValue[] SetCombine( SetOperation operation, RedisKey first, RedisKey second, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().Combine(operation, first, second).Wait(SyncTimeout); - public Task SetCombineAsync( + public RedisValue[] SetCombine(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().Combine(operation, keys).Wait(SyncTimeout); + + public long SetCombineAndStore( SetOperation operation, + RedisKey destination, + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().CombineStore(operation, destination, first, second).Wait(SyncTimeout); + + public long SetCombineAndStore( + SetOperation operation, + RedisKey destination, RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().CombineStore(operation, destination, keys).Wait(SyncTimeout); public Task SetCombineAndStoreAsync( SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().CombineStore(operation, destination, first, second).AsTask(); public Task SetCombineAndStoreAsync( SetOperation operation, RedisKey destination, RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().CombineStore(operation, destination, keys).AsTask(); - public Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetContainsAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task SetCombineAsync( + SetOperation operation, + RedisKey first, + RedisKey second, + CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().Combine(operation, first, second).AsTask(); - public Task SetIntersectionLengthAsync( + public Task SetCombineAsync( + SetOperation operation, RedisKey[] keys, - long limit = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().Combine(operation, keys).AsTask(); - public Task SetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public bool SetContains(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SIsMember(key, value).Wait(SyncTimeout); - public Task SetMembersAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public bool[] SetContains(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SMIsMember(key, values).Wait(SyncTimeout); - public Task SetMoveAsync( - RedisKey source, - RedisKey destination, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SIsMember(key, value).AsTask(); - public Task SetRemoveAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task SetContainsAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SMIsMember(key, values).AsTask(); - public Task SetRemoveAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public long SetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SInterCard(keys, limit).Wait(SyncTimeout); - public IAsyncEnumerable SetScanAsync( - RedisKey key, - RedisValue pattern = default, - int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, - long cursor = RedisBase.CursorUtils.Origin, - int pageOffset = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task SetIntersectionLengthAsync( + RedisKey[] keys, + long limit = 0, + CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SInterCard(keys, limit).AsTask(); - // Synchronous Set methods - [RespCommand("sadd")] - public partial bool SetAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); + public long SetLength(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SCard(key).Wait(SyncTimeout); - public long SetAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task SetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SCard(key).AsTask(); - public RedisValue[] SetCombine( - SetOperation operation, - RedisKey first, - RedisKey second, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue[] SetMembers(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SMembers(key).Wait(SyncTimeout); - public RedisValue[] SetCombine(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task SetMembersAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SMembers(key).AsTask(); - public long SetCombineAndStore( - SetOperation operation, + public bool SetMove( + RedisKey source, RedisKey destination, - RedisKey first, - RedisKey second, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + RedisValue value, + CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SMove(source, destination, value).Wait(SyncTimeout); - public long SetCombineAndStore( - SetOperation operation, + public Task SetMoveAsync( + RedisKey source, RedisKey destination, - RedisKey[] keys, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + RedisValue value, + CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SMove(source, destination, value).AsTask(); - public bool SetContains(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue SetPop(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SPop(key).Wait(SyncTimeout); - public bool[] SetContains(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue[] SetPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SPop(key, count).Wait(SyncTimeout); - public long SetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task SetPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SPop(key).AsTask(); - public long SetLength(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task SetPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SPop(key, count).AsTask(); - public RedisValue[] SetMembers(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue SetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SRandMember(key).Wait(SyncTimeout); - public bool SetMove( - RedisKey source, - RedisKey destination, - RedisValue value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue[] SetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SRandMember(key, count).Wait(SyncTimeout); - [RespCommand("spop")] - public partial RedisValue SetPop(RedisKey key, CommandFlags flags = CommandFlags.None); + public Task SetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SRandMember(key).AsTask(); - public RedisValue[] SetPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task SetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SRandMember(key, count).AsTask(); - public RedisValue SetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public bool SetRemove(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SRem(key, value).Wait(SyncTimeout); - public RedisValue[] SetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public long SetRemove(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SRem(key, values).Wait(SyncTimeout); - public bool SetRemove(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task SetRemoveAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SRem(key, value).AsTask(); - public long SetRemove(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task SetRemoveAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) + => Context(flags).Sets().SRem(key, values).AsTask(); public IEnumerable SetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) => throw new NotImplementedException(); @@ -168,4 +164,13 @@ public IEnumerable SetScan( int pageOffset = 0, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); + + public IAsyncEnumerable SetScanAsync( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) => + throw new NotImplementedException(); } From 7858604d49768b89cb2e406ff1485f29f176194f Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 3 Oct 2025 11:37:35 +0100 Subject: [PATCH 098/108] hashes --- .../RedisCommands.HashCommands.cs | 115 ++++ .../RespContextDatabase.Hash.cs | 562 +++++++----------- .../RespContextDatabase.Set.cs | 1 - 3 files changed, 319 insertions(+), 359 deletions(-) create mode 100644 src/RESPite.StackExchange.Redis/RedisCommands.HashCommands.cs diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.HashCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.HashCommands.cs new file mode 100644 index 000000000..c2571ca26 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RedisCommands.HashCommands.cs @@ -0,0 +1,115 @@ +using System.Runtime.CompilerServices; +using RESPite.Messages; +using StackExchange.Redis; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable InconsistentNaming +namespace RESPite.StackExchange.Redis; + +internal static partial class RedisCommands +{ + // this is just a "type pun" - it should be an invisible/magic pointer cast to the JIT + public static ref readonly HashCommands Hashes(this in RespContext context) + => ref Unsafe.As(ref Unsafe.AsRef(in context)); +} + +public readonly struct HashCommands(in RespContext context) +{ + public readonly RespContext Context = context; // important: this is the only field +} + +internal static partial class HashCommandsExtensions +{ + [RespCommand] + public static partial RespOperation HDel(this in HashCommands context, RedisKey key, RedisValue hashField); + + [RespCommand] + public static partial RespOperation HDel(this in HashCommands context, RedisKey key, RedisValue[] hashFields); + + [RespCommand] + public static partial RespOperation HExists(this in HashCommands context, RedisKey key, RedisValue hashField); + + [RespCommand] + public static partial RespOperation HGet(this in HashCommands context, RedisKey key, RedisValue hashField); + + [RespCommand] + public static partial RespOperation HGetAll(this in HashCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation HIncrBy(this in HashCommands context, RedisKey key, RedisValue hashField, long value = 1); + + [RespCommand] + public static partial RespOperation HIncrByFloat(this in HashCommands context, RedisKey key, RedisValue hashField, double value); + + [RespCommand] + public static partial RespOperation HKeys(this in HashCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation HLen(this in HashCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation HMGet(this in HashCommands context, RedisKey key, RedisValue[] hashFields); + + [RespCommand] + public static partial RespOperation HRandField(this in HashCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation HRandField(this in HashCommands context, RedisKey key, long count); + + [RespCommand] + public static partial RespOperation HRandFieldWithValues(this in HashCommands context, RedisKey key, [RespSuffix("WITHVALUES")] long count); + + [RespCommand] + public static partial RespOperation HSet(this in HashCommands context, RedisKey key, RedisValue hashField, RedisValue value); + + internal static RespOperation HSet( + this in HashCommands context, + RedisKey key, + RedisValue hashField, + RedisValue value, + When when) + { + switch (when) + { + case When.Always: + return HSet(context, key, hashField, value); + case When.NotExists: + return HSetNX(context, key, hashField, value); + default: + when.AlwaysOrNotExists(); // throws + return default; + } + } + + [RespCommand(Formatter = "HSetFormatter.Instance")] + public static partial RespOperation HSet(this in HashCommands context, RedisKey key, HashEntry[] hashFields); + + private sealed class HSetFormatter : IRespFormatter<(RedisKey Key, HashEntry[] HashFields)> + { + private HSetFormatter() { } + public static readonly HSetFormatter Instance = new(); + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, HashEntry[] HashFields) request) + { + writer.WriteCommand(command, 1 + (request.HashFields.Length * 2)); + writer.Write(request.Key); + foreach (var entry in request.HashFields) + { + writer.Write(entry.Name); + writer.Write(entry.Value); + } + } + } + + [RespCommand] + public static partial RespOperation HSetNX(this in HashCommands context, RedisKey key, RedisValue hashField, RedisValue value); + + [RespCommand] + public static partial RespOperation HStrLen(this in HashCommands context, RedisKey key, RedisValue hashField); + + [RespCommand] + public static partial RespOperation HVals(this in HashCommands context, RedisKey key); +} diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs index 1465b4cd5..9fe33b586 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs @@ -5,419 +5,244 @@ namespace RESPite.StackExchange.Redis; internal partial class RespContextDatabase { - // Async Hash methods - public Task HashDecrementAsync( + public long HashDecrement( RedisKey key, RedisValue hashField, long value = 1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HIncrBy(key, hashField, -value).Wait(SyncTimeout); - public Task HashDecrementAsync( + public double HashDecrement( RedisKey key, RedisValue hashField, double value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HIncrByFloat(key, hashField, -value).Wait(SyncTimeout); - public Task HashFieldExpireAsync( + public Task HashDecrementAsync( RedisKey key, - RedisValue[] hashFields, - TimeSpan expiry, - ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + RedisValue hashField, + long value = 1, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HIncrBy(key, hashField, -value).AsTask(); - public Task HashFieldExpireAsync( + public Task HashDecrementAsync( RedisKey key, - RedisValue[] hashFields, - DateTime expiry, - ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + RedisValue hashField, + double value, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HIncrByFloat(key, hashField, -value).AsTask(); - public Task HashFieldGetExpireDateTimeAsync( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public bool HashDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HDel(key, hashField).Wait(SyncTimeout); - public Task HashFieldPersistAsync( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public long HashDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HDel(key, hashFields).Wait(SyncTimeout); - public Task HashFieldGetTimeToLiveAsync( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HDel(key, hashField).AsTask(); - public Task HashGetAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HDel(key, hashFields).AsTask(); - public Task?> HashGetLeaseAsync( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public bool HashExists(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HExists(key, hashField).Wait(SyncTimeout); - public Task HashGetAsync( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HExists(key, hashField).AsTask(); - public Task HashFieldGetAndDeleteAsync( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Task?> HashFieldGetLeaseAndDeleteAsync( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Task HashFieldGetAndDeleteAsync( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Task HashFieldGetAndSetExpiryAsync( - RedisKey key, - RedisValue hashField, - TimeSpan? expiry = null, - bool persist = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Task HashFieldGetAndSetExpiryAsync( - RedisKey key, - RedisValue hashField, - DateTime expiry, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue HashFieldGetAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Task?> HashFieldGetLeaseAndSetExpiryAsync( - RedisKey key, - RedisValue hashField, - TimeSpan? expiry = null, - bool persist = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue[] HashFieldGetAndDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Task?> HashFieldGetLeaseAndSetExpiryAsync( - RedisKey key, - RedisValue hashField, - DateTime expiry, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Task HashFieldGetAndSetExpiryAsync( - RedisKey key, - RedisValue[] hashFields, - TimeSpan? expiry = null, - bool persist = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Task HashFieldGetAndSetExpiryAsync( - RedisKey key, - RedisValue[] hashFields, - DateTime expiry, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Task HashFieldSetAndSetExpiryAsync( - RedisKey key, - RedisValue field, - RedisValue value, - TimeSpan? expiry = null, - bool keepTtl = false, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Task HashFieldSetAndSetExpiryAsync( - RedisKey key, - RedisValue field, - RedisValue value, - DateTime expiry, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Task HashFieldSetAndSetExpiryAsync( - RedisKey key, - HashEntry[] hashFields, - TimeSpan? expiry = null, - bool keepTtl = false, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Task HashFieldSetAndSetExpiryAsync( - RedisKey key, - HashEntry[] hashFields, - DateTime expiry, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public IAsyncEnumerable HashScanAsync( - RedisKey key, - RedisValue pattern = default, - int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, - long cursor = RedisBase.CursorUtils.Origin, - int pageOffset = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public IAsyncEnumerable HashScanNoValuesAsync( - RedisKey key, - RedisValue pattern = default, - int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, - long cursor = RedisBase.CursorUtils.Origin, - int pageOffset = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Task HashSetAsync(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Task HashSetAsync( - RedisKey key, - RedisValue hashField, - RedisValue value, - When when = When.Always, - CommandFlags flags = CommandFlags.None) - { - when.AlwaysOrNotExists(); - if (value.IsNull) return HashDeleteAsync(key, hashField, flags); - return when == When.Always - ? HashSetCoreAsync(key, hashField, value, flags) - : HashSetNXCoreAsync(key, hashField, value, flags); - } + public long[] HashFieldGetExpireDateTime(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Task HashStringLengthAsync( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashFieldGetExpireDateTimeAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Task HashValuesAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Lease? HashFieldGetLeaseAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - // Synchronous Hash methods - public long HashDecrement( - RedisKey key, - RedisValue hashField, - long value = 1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task?> HashFieldGetLeaseAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public double HashDecrement( - RedisKey key, - RedisValue hashField, - double value, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - [RespCommand("hdel")] - public partial bool HashDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); + public Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public long HashDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public bool HashExists(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public ExpireResult[] HashFieldExpire( - RedisKey key, - RedisValue[] hashFields, - TimeSpan expiry, - ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public long[] HashFieldGetTimeToLive(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public ExpireResult[] HashFieldExpire( - RedisKey key, - RedisValue[] hashFields, - DateTime expiry, - ExpireWhen when = ExpireWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashFieldGetTimeToLiveAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public long[] HashFieldGetExpireDateTime( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public PersistResult[] HashFieldPersist(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public PersistResult[] HashFieldPersist( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashFieldPersistAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public long[] HashFieldGetTimeToLive( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue hashField, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public RedisValue HashGet(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue hashField, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Lease? HashGetLease(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public RedisValue[] HashGet(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public RedisValue HashFieldGetAndDelete( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue hashField, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public Lease? HashFieldGetLeaseAndDelete( - RedisKey key, - RedisValue hashField, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue hashField, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public RedisValue[] HashFieldGetAndDelete( - RedisKey key, - RedisValue[] hashFields, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public RedisValue HashFieldGetAndSetExpiry( - RedisKey key, - RedisValue hashField, - TimeSpan? expiry = null, - bool persist = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public RedisValue HashFieldGetAndSetExpiry( - RedisKey key, - RedisValue hashField, - DateTime expiry, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue HashGet(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGet(key, hashField).Wait(SyncTimeout); - public Lease? HashFieldGetLeaseAndSetExpiry( - RedisKey key, - RedisValue hashField, - TimeSpan? expiry = null, - bool persist = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue[] HashGet(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HMGet(key, hashFields).Wait(SyncTimeout); - public Lease? HashFieldGetLeaseAndSetExpiry( - RedisKey key, - RedisValue hashField, - DateTime expiry, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public HashEntry[] HashGetAll(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetAll(key).Wait(SyncTimeout); - public RedisValue[] HashFieldGetAndSetExpiry( - RedisKey key, - RedisValue[] hashFields, - TimeSpan? expiry = null, - bool persist = false, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashGetAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGet(key, hashField).AsTask(); - public RedisValue[] HashFieldGetAndSetExpiry( + public Task HashGetAsync( RedisKey key, RedisValue[] hashFields, - DateTime expiry, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HMGet(key, hashFields).AsTask(); - public RedisValue HashFieldSetAndSetExpiry( - RedisKey key, - RedisValue field, - RedisValue value, - TimeSpan? expiry = null, - bool keepTtl = false, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashGetAllAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetAll(key).AsTask(); - public RedisValue HashFieldSetAndSetExpiry( - RedisKey key, - RedisValue field, - RedisValue value, - DateTime expiry, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Lease? HashGetLease(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public RedisValue HashFieldSetAndSetExpiry( - RedisKey key, - HashEntry[] hashFields, - TimeSpan? expiry = null, - bool keepTtl = false, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task?> HashGetLeaseAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public RedisValue HashFieldSetAndSetExpiry( + public long HashIncrement( RedisKey key, - HashEntry[] hashFields, - DateTime expiry, - When when = When.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + RedisValue hashField, + long value = 1, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HIncrBy(key, hashField, value).Wait(SyncTimeout); - [RespCommand("hgetall")] - public partial HashEntry[] HashGetAll(RedisKey key, CommandFlags flags = CommandFlags.None); + public double HashIncrement( + RedisKey key, + RedisValue hashField, + double value, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HIncrByFloat(key, hashField, value).Wait(SyncTimeout); - [RespCommand("hincrby")] - public partial long HashIncrement( + public Task HashIncrementAsync( RedisKey key, RedisValue hashField, long value = 1, - CommandFlags flags = CommandFlags.None); + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HIncrBy(key, hashField, value).AsTask(); - [RespCommand("hincrbyfloat")] - public partial double HashIncrement( + public Task HashIncrementAsync( RedisKey key, RedisValue hashField, double value, - CommandFlags flags = CommandFlags.None); + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HIncrByFloat(key, hashField, value).AsTask(); + + public RedisValue[] HashKeys(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HKeys(key).Wait(SyncTimeout); + + public Task HashKeysAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HKeys(key).AsTask(); - [RespCommand("hkeys")] - public partial RedisValue[] HashKeys(RedisKey key, CommandFlags flags = CommandFlags.None); + public long HashLength(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HLen(key).Wait(SyncTimeout); - [RespCommand("hlen")] - public partial long HashLength(RedisKey key, CommandFlags flags = CommandFlags.None); + public Task HashLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HLen(key).AsTask(); - [RespCommand("hrandfield")] - public partial RedisValue HashRandomField(RedisKey key, CommandFlags flags = CommandFlags.None); + public RedisValue HashRandomField(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HRandField(key).Wait(SyncTimeout); - [RespCommand("hrandfield")] - public partial RedisValue[] HashRandomFields(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + public RedisValue[] HashRandomFields(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HRandField(key, count).Wait(SyncTimeout); - [RespCommand("hrandfield")] - public partial HashEntry[] HashRandomFieldsWithValues(RedisKey key, [RespSuffix("WITHVALUES")] long count, CommandFlags flags = CommandFlags.None); + public HashEntry[] HashRandomFieldsWithValues(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HRandFieldWithValues(key, count).Wait(SyncTimeout); - public IEnumerable HashScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) => - throw new NotImplementedException(); + public Task HashRandomFieldAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HRandField(key).AsTask(); + + public Task HashRandomFieldsAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HRandField(key, count).AsTask(); + + public Task HashRandomFieldsWithValuesAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HRandFieldWithValues(key, count).AsTask(); public IEnumerable HashScan( RedisKey key, @@ -425,8 +250,20 @@ public IEnumerable HashScan( int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); + + public IEnumerable HashScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) + => throw new NotImplementedException(); + + public IAsyncEnumerable HashScanAsync( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); public IEnumerable HashScanNoValues( RedisKey key, @@ -434,11 +271,17 @@ public IEnumerable HashScanNoValues( int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public void HashSet(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public IAsyncEnumerable HashScanNoValuesAsync( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); public bool HashSet( RedisKey key, @@ -446,31 +289,34 @@ public bool HashSet( RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - when.AlwaysOrNotExists(); - if (value.IsNull) return HashDelete(key, hashField, flags); - return when == When.Always - ? HashSetCore(key, hashField, value, flags) - : HashSetNXCore(key, hashField, value, flags); - } - - [RespCommand("hset")] - private partial bool HashSetCore( + => Context(flags).Hashes().HSet(key, hashField, value, when).Wait(SyncTimeout); + + public Task HashSetAsync( RedisKey key, RedisValue hashField, RedisValue value, - CommandFlags flags = CommandFlags.None); + When when = When.Always, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HSet(key, hashField, value, when).AsTask(); + + public void HashSet(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HSet(key, hashFields).Wait(SyncTimeout); - [RespCommand("hsetnx")] - private partial bool HashSetNXCore( + public Task HashSetAsync(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HSet(key, hashFields).AsTask(); + + public long HashStringLength(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HStrLen(key, hashField).Wait(SyncTimeout); + + public Task HashStringLengthAsync( RedisKey key, RedisValue hashField, - RedisValue value, - CommandFlags flags = CommandFlags.None); + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HStrLen(key, hashField).AsTask(); - public long HashStringLength(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue[] HashValues(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HVals(key).Wait(SyncTimeout); - public RedisValue[] HashValues(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HashValuesAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HVals(key).AsTask(); } diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Set.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Set.cs index 4ccde4b67..4563ccd05 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Set.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Set.cs @@ -4,7 +4,6 @@ namespace RESPite.StackExchange.Redis; internal partial class RespContextDatabase { - // Synchronous Set methods public bool SetAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => Context(flags).Sets().SAdd(key, value).Wait(SyncTimeout); From 4b6a58eba37084872348cf14335b95f6c60fd47b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 3 Oct 2025 12:28:01 +0100 Subject: [PATCH 099/108] hashes, but fix the codegen to work better for expire etc scenarios (optional NX|XX|...) --- StackExchange.Redis.sln.DotSettings | 1 + .../RespCommandGenerator.cs | 56 +++++++-- .../RedisCommands.HashCommands.cs | 19 +++ .../RedisCommands.KeyCommands.cs | 110 +++++++++--------- .../RedisCommands.ListCommands.cs | 1 + .../RespContextDatabase.Hash.cs | 4 +- .../RespFormatters.cs | 25 +++- src/RESPite/RespIgnoreAttribute.cs | 4 +- 8 files changed, 150 insertions(+), 70 deletions(-) diff --git a/StackExchange.Redis.sln.DotSettings b/StackExchange.Redis.sln.DotSettings index ae5924d19..2992748a5 100644 --- a/StackExchange.Redis.sln.DotSettings +++ b/StackExchange.Redis.sln.DotSettings @@ -3,6 +3,7 @@ PONG RES SE + True True True True diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index ce4c598a8..ee66af9a1 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -460,7 +460,7 @@ void AddLiteral(string token, LiteralFlags literalFlags) { if (IsRESPite(attrib.AttributeClass, RESPite.RespPrefixAttribute)) { - if (attrib.ConstructorArguments[0].Value?.ToString() is { Length: > 0 } val) + if (attrib.ConstructorArguments[0].Value?.ToString() is { } val) { AddNotes(ref debugNote, $"prefix {val}"); AddLiteral(val, LiteralFlags.None); @@ -479,19 +479,38 @@ void AddLiteral(string token, LiteralFlags literalFlags) if (IsRESPite(attrib.AttributeClass, RESPite.RespIgnoreAttribute)) { var val = attrib.ConstructorArguments[0].Value; - if (val is string s) + var expr = val switch { - ignoreExpression = CodeLiteral(s); - } - else if (val is bool b) + string s => CodeLiteral(s), + bool b => b ? "true" : "false", + long l when attrib.ConstructorArguments[0].Type is INamedTypeSymbol { EnumUnderlyingType: not null } enumType + => GetEnumExpression(enumType, l), + long l => l.ToString(CultureInfo.InvariantCulture), + int i when attrib.ConstructorArguments[0].Type is INamedTypeSymbol { EnumUnderlyingType: not null } enumType + => GetEnumExpression(enumType, i), + int i => i.ToString(CultureInfo.InvariantCulture), + _ => null, + }; + + if (expr is not null) { - ignoreExpression = b ? "true" : "false"; + flags |= ParameterFlags.IgnoreExpression; + ignoreExpression = expr; } - else if (val is long l) + + static string GetEnumExpression(INamedTypeSymbol enumType, object value) { - ignoreExpression = l.ToString(CultureInfo.InvariantCulture); + foreach (var member in enumType.GetMembers()) + { + if (member is IFieldSymbol { IsStatic: true, IsConst: true } field + && Equals(field.ConstantValue, value)) + { + return $"{GetFullName(enumType)}.{field.Name}"; + } + } + + return $"({GetFullName(enumType)}){value}"; } - if (ignoreExpression is not null) flags |= ParameterFlags.IgnoreExpression; } } } @@ -1071,11 +1090,11 @@ void WriteParameterName(in ParameterTuple p, StringBuilder? target = null) } else { - sb.Append("("); + if (literalCount != 0) sb.Append("("); WriteParameterName(parameter); if (parameter.IsNullable) sb.Append("!"); sb.Append((parameter.Flags & ParameterFlags.CollectionWithCount) == 0 ? ".Length" : ".Count"); - sb.Append(" + ").Append(1 + literalCount).Append(")"); + if (literalCount != 0) sb.Append(" + ").Append(literalCount).Append(")"); } sb.Append(" : 0)"); @@ -1084,6 +1103,17 @@ void WriteParameterName(in ParameterTuple p, StringBuilder? target = null) // help identify what this is (not needed for collections, since foo.Count etc) sb.Append(" // "); WriteParameterName(parameter); + if (argCount != 1) sb.Append(" (").Append(parameter.Name).Append(")"); // give an example + } + + if (literalCount != 0) + { + if (parameter.IsCollection) sb.Append(" //"); + sb.Append(" with"); + foreach (var literal in parameter.Literals.Span) + { + sb.Append(" ").Append(string.IsNullOrEmpty(literal.Token) ? "(count)" : literal.Token); + } } } index++; @@ -1129,9 +1159,11 @@ void WriteLiteral(in ParameterTuple p, bool suffix) { if (p.IsCollection) { + sb = NewLine().Append("writer.WriteBulkString("); WriteParameterName(p); if (p.IsNullable) sb.Append("!"); - sb.Append((p.Flags & ParameterFlags.CollectionWithCount) == 0 ? ".Length" : ".Count"); + sb.Append((p.Flags & ParameterFlags.CollectionWithCount) == 0 ? ".Length" : ".Count") + .Append(");"); } else { diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.HashCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.HashCommands.cs index c2571ca26..18d93d2d0 100644 --- a/src/RESPite.StackExchange.Redis/RedisCommands.HashCommands.cs +++ b/src/RESPite.StackExchange.Redis/RedisCommands.HashCommands.cs @@ -29,9 +29,28 @@ internal static partial class HashCommandsExtensions [RespCommand] public static partial RespOperation HExists(this in HashCommands context, RedisKey key, RedisValue hashField); + [RespCommand] + private static partial RespOperation HExpire( + this in HashCommands context, + RedisKey key, + long seconds, + [RespIgnore(ExpireWhen.Always)] ExpireWhen when, + [RespPrefix("FIELDS"), RespPrefix] RedisValue[] hashFields); + + [RespCommand] + private static partial RespOperation HPExpire( + this in HashCommands context, + RedisKey key, + long milliseconds, + [RespIgnore(ExpireWhen.Always)] ExpireWhen when, + [RespPrefix("FIELDS"), RespPrefix] RedisValue[] hashFields); + [RespCommand] public static partial RespOperation HGet(this in HashCommands context, RedisKey key, RedisValue hashField); + [RespCommand("hget")] + public static partial RespOperation?> HGetLease(this in HashCommands context, RedisKey key, RedisValue hashField); + [RespCommand] public static partial RespOperation HGetAll(this in HashCommands context, RedisKey key); diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.KeyCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.KeyCommands.cs index 84e1dc33b..91b5eb255 100644 --- a/src/RESPite.StackExchange.Redis/RedisCommands.KeyCommands.cs +++ b/src/RESPite.StackExchange.Redis/RedisCommands.KeyCommands.cs @@ -2,6 +2,8 @@ using RESPite.Messages; using StackExchange.Redis; +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable InconsistentNaming namespace RESPite.StackExchange.Redis; internal static partial class RedisCommands @@ -41,7 +43,11 @@ public static partial RespOperation Copy( [RespCommand] public static partial RespOperation Exists(this in KeyCommands context, [RespKey] RedisKey[] keys); - public static RespOperation Expire(this in KeyCommands context, RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always) + public static RespOperation Expire( + this in KeyCommands context, + RedisKey key, + TimeSpan? expiry, + ExpireWhen when = ExpireWhen.Always) { if (expiry is null || expiry == TimeSpan.MaxValue) { @@ -49,18 +55,28 @@ public static RespOperation Expire(this in KeyCommands context, RedisKey k return Persist(context, key); static void Throw(ExpireWhen when) => throw new ArgumentException($"PERSIST cannot be used with {when}."); } + var millis = (long)expiry.GetValueOrDefault().TotalMilliseconds; if (millis % 1000 == 0) // use seconds { return Expire(context, key, millis / 1000, when); } + return PExpire(context, key, millis, when); } - [RespCommand(Formatter = "ExpireFormatter.Instance")] - public static partial RespOperation Expire(this in KeyCommands context, RedisKey key, long seconds, ExpireWhen when = ExpireWhen.Always); + [RespCommand] + public static partial RespOperation Expire( + this in KeyCommands context, + RedisKey key, + long seconds, + [RespIgnore(ExpireWhen.Always)] ExpireWhen when = ExpireWhen.Always); - public static RespOperation ExpireAt(this in KeyCommands context, RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always) + public static RespOperation ExpireAt( + this in KeyCommands context, + RedisKey key, + DateTime? expiry, + ExpireWhen when = ExpireWhen.Always) { if (expiry is null || expiry == DateTime.MaxValue) { @@ -68,16 +84,22 @@ public static RespOperation ExpireAt(this in KeyCommands context, RedisKey return Persist(context, key); static void Throw(ExpireWhen when) => throw new ArgumentException($"PERSIST cannot be used with {when}."); } + var millis = RedisDatabase.GetUnixTimeMilliseconds(expiry.GetValueOrDefault()); if (millis % 1000 == 0) // use seconds { return ExpireAt(context, key, millis / 1000, when); } + return PExpireAt(context, key, millis, when); } - [RespCommand(Formatter = "ExpireFormatter.Instance")] - public static partial RespOperation ExpireAt(this in KeyCommands context, RedisKey key, long seconds, ExpireWhen when = ExpireWhen.Always); + [RespCommand] + public static partial RespOperation ExpireAt( + this in KeyCommands context, + RedisKey key, + long seconds, + [RespIgnore(ExpireWhen.Always)] ExpireWhen when = ExpireWhen.Always); [RespCommand(Parser = "RespParsers.DateTimeFromSeconds")] public static partial RespOperation ExpireTime(this in KeyCommands context, RedisKey key); @@ -86,22 +108,38 @@ public static RespOperation ExpireAt(this in KeyCommands context, RedisKey public static partial RespOperation Move(this in KeyCommands context, RedisKey key, int db); [RespCommand("object")] - public static partial RespOperation ObjectEncoding(this in KeyCommands context, [RespPrefix("ENCODING")] RedisKey key); + public static partial RespOperation ObjectEncoding( + this in KeyCommands context, + [RespPrefix("ENCODING")] RedisKey key); [RespCommand("object")] - public static partial RespOperation ObjectFreq(this in KeyCommands context, [RespPrefix("FREQ")] RedisKey key); + public static partial RespOperation ObjectFreq( + this in KeyCommands context, + [RespPrefix("FREQ")] RedisKey key); [RespCommand("object", Parser = "RespParsers.TimeSpanFromSeconds")] - public static partial RespOperation ObjectIdleTime(this in KeyCommands context, [RespPrefix("IDLETIME")] RedisKey key); + public static partial RespOperation ObjectIdleTime( + this in KeyCommands context, + [RespPrefix("IDLETIME")] RedisKey key); [RespCommand("object")] - public static partial RespOperation ObjectRefCount(this in KeyCommands context, [RespPrefix("REFCOUNT")] RedisKey key); + public static partial RespOperation ObjectRefCount( + this in KeyCommands context, + [RespPrefix("REFCOUNT")] RedisKey key); - [RespCommand(Formatter = "ExpireFormatter.Instance")] - public static partial RespOperation PExpire(this in KeyCommands context, RedisKey key, long milliseconds, ExpireWhen when = ExpireWhen.Always); + [RespCommand] + public static partial RespOperation PExpire( + this in KeyCommands context, + RedisKey key, + long milliseconds, + [RespIgnore(ExpireWhen.Always)] ExpireWhen when = ExpireWhen.Always); - [RespCommand(Formatter = "ExpireFormatter.Instance")] - public static partial RespOperation PExpireAt(this in KeyCommands context, RedisKey key, long milliseconds, ExpireWhen when = ExpireWhen.Always); + [RespCommand] + public static partial RespOperation PExpireAt( + this in KeyCommands context, + RedisKey key, + long milliseconds, + [RespIgnore(ExpireWhen.Always)] ExpireWhen when = ExpireWhen.Always); [RespCommand(Parser = "RespParsers.DateTimeFromMilliseconds")] public static partial RespOperation PExpireTime(this in KeyCommands context, RedisKey key); @@ -137,7 +175,11 @@ public static RespOperation Rename(this in KeyCommands context, RedisKey k public static partial RespOperation RenameNx(this in KeyCommands context, RedisKey key, RedisKey newKey); [RespCommand(Formatter = "RestoreFormatter.Instance")] - public static partial RespOperation Restore(this in KeyCommands context, RedisKey key, TimeSpan? ttl, byte[] serializedValue); + public static partial RespOperation Restore( + this in KeyCommands context, + RedisKey key, + TimeSpan? ttl, + byte[] serializedValue); [RespCommand] public static partial RespOperation Touch(this in KeyCommands context, RedisKey key); @@ -191,43 +233,6 @@ public void Format( } } - private sealed class ExpireFormatter : IRespFormatter<(RedisKey Key, long Value, ExpireWhen When)> - { - public static readonly ExpireFormatter Instance = new(); - private ExpireFormatter() { } - - public void Format( - scoped ReadOnlySpan command, - ref RespWriter writer, - in (RedisKey Key, long Value, ExpireWhen When) request) - { - writer.WriteCommand(command, request.When == ExpireWhen.Always ? 2 : 3); - writer.Write(request.Key); - writer.Write(request.Value); - switch (request.When) - { - case ExpireWhen.Always: - break; - case ExpireWhen.HasExpiry: - writer.WriteRaw("$2\r\nXX\r\n"u8); - break; - case ExpireWhen.HasNoExpiry: - writer.WriteRaw("$2\r\nNX\r\n"u8); - break; - case ExpireWhen.GreaterThanCurrentExpiry: - writer.WriteRaw("$2\r\nGT\r\n"u8); - break; - case ExpireWhen.LessThanCurrentExpiry: - writer.WriteRaw("$2\r\nLT\r\n"u8); - break; - default: - Throw(); - static void Throw() => throw new ArgumentOutOfRangeException(nameof(request.When)); - break; - } - } - } - private sealed class RestoreFormatter : IRespFormatter<(RedisKey Key, TimeSpan? Ttl, byte[] SerializedValue)> { public static readonly RestoreFormatter Instance = new(); @@ -248,6 +253,7 @@ public void Format( { writer.WriteRaw("$1\r\n0\r\n"u8); } + writer.WriteBulkString(request.SerializedValue); } } diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.ListCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.ListCommands.cs index 658b4cfe7..1112e26c2 100644 --- a/src/RESPite.StackExchange.Redis/RedisCommands.ListCommands.cs +++ b/src/RESPite.StackExchange.Redis/RedisCommands.ListCommands.cs @@ -2,6 +2,7 @@ using RESPite.Messages; using StackExchange.Redis; +// ReSharper disable MemberCanBePrivate.Global // ReSharper disable InconsistentNaming namespace RESPite.StackExchange.Redis; diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs index 9fe33b586..f59b2fb8c 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs @@ -181,10 +181,10 @@ public Task HashGetAllAsync(RedisKey key, CommandFlags flags = Comm => Context(flags).Hashes().HGetAll(key).AsTask(); public Lease? HashGetLease(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).Hashes().HGetLease(key, hashField).Wait(SyncTimeout); public Task?> HashGetLeaseAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).Hashes().HGetLease(key, hashField).AsTask(); public long HashIncrement( RedisKey key, diff --git a/src/RESPite.StackExchange.Redis/RespFormatters.cs b/src/RESPite.StackExchange.Redis/RespFormatters.cs index 35884dc5b..332fc54ca 100644 --- a/src/RESPite.StackExchange.Redis/RespFormatters.cs +++ b/src/RESPite.StackExchange.Redis/RespFormatters.cs @@ -57,6 +57,29 @@ public static void Write(this ref RespWriter writer, in RedisKey key) } } + internal static void WriteBulkString(this ref RespWriter writer, ExpireWhen when) + { + switch (when) + { + case ExpireWhen.HasExpiry: + writer.WriteRaw("$2\r\nXX\r\n"u8); + break; + case ExpireWhen.HasNoExpiry: + writer.WriteRaw("$2\r\nNX\r\n"u8); + break; + case ExpireWhen.GreaterThanCurrentExpiry: + writer.WriteRaw("$2\r\nGT\r\n"u8); + break; + case ExpireWhen.LessThanCurrentExpiry: + writer.WriteRaw("$2\r\nLT\r\n"u8); + break; + default: + Throw(); + static void Throw() => throw new ArgumentOutOfRangeException(nameof(when)); + break; + } + } + internal static void WriteBulkString(this ref RespWriter writer, ListSide side) { switch (side) @@ -69,9 +92,9 @@ internal static void WriteBulkString(this ref RespWriter writer, ListSide side) break; default: Throw(); + static void Throw() => throw new ArgumentOutOfRangeException(nameof(side)); break; } - static void Throw() => throw new ArgumentOutOfRangeException(nameof(side)); } // ReSharper disable once MemberCanBePrivate.Global diff --git a/src/RESPite/RespIgnoreAttribute.cs b/src/RESPite/RespIgnoreAttribute.cs index bd44a9716..614218b1c 100644 --- a/src/RESPite/RespIgnoreAttribute.cs +++ b/src/RESPite/RespIgnoreAttribute.cs @@ -9,7 +9,5 @@ public sealed class RespIgnoreAttribute : Attribute { private readonly object _value; public object Value => _value; - public RespIgnoreAttribute(string value) => _value = value; - public RespIgnoreAttribute(long value) => _value = value; - public RespIgnoreAttribute(bool value) => _value = value; + public RespIgnoreAttribute(object value) => _value = value; } From 692ce5311e97e90c1f100a462f1a512601138c17 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 3 Oct 2025 15:03:43 +0100 Subject: [PATCH 100/108] more hashes --- StackExchange.Redis.sln.DotSettings | 2 + .../RedisCommands.HashCommands.cs | 464 +++++++++++++++++- .../RespContextDatabase.Hash.cs | 364 +++++++++++--- .../RespFormatters.cs | 26 + .../RespParsers.cs | 37 +- 5 files changed, 782 insertions(+), 111 deletions(-) diff --git a/StackExchange.Redis.sln.DotSettings b/StackExchange.Redis.sln.DotSettings index 2992748a5..0c18b97d4 100644 --- a/StackExchange.Redis.sln.DotSettings +++ b/StackExchange.Redis.sln.DotSettings @@ -3,8 +3,10 @@ PONG RES SE + True True True + True True True True diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.HashCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.HashCommands.cs index 18d93d2d0..7b8ce3c39 100644 --- a/src/RESPite.StackExchange.Redis/RedisCommands.HashCommands.cs +++ b/src/RESPite.StackExchange.Redis/RedisCommands.HashCommands.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using RESPite.Internal; using RESPite.Messages; using StackExchange.Redis; @@ -21,44 +22,358 @@ public readonly struct HashCommands(in RespContext context) internal static partial class HashCommandsExtensions { [RespCommand] - public static partial RespOperation HDel(this in HashCommands context, RedisKey key, RedisValue hashField); + public static partial RespOperation HDel(this in HashCommands context, RedisKey key, RedisValue field); [RespCommand] - public static partial RespOperation HDel(this in HashCommands context, RedisKey key, RedisValue[] hashFields); + public static partial RespOperation HDel(this in HashCommands context, RedisKey key, RedisValue[] fields); [RespCommand] - public static partial RespOperation HExists(this in HashCommands context, RedisKey key, RedisValue hashField); + public static partial RespOperation HExists(this in HashCommands context, RedisKey key, RedisValue field); - [RespCommand] - private static partial RespOperation HExpire( + [RespCommand(Parser = "ExpireResultParser.Default")] + private static partial RespOperation HExpire( this in HashCommands context, RedisKey key, long seconds, [RespIgnore(ExpireWhen.Always)] ExpireWhen when, - [RespPrefix("FIELDS"), RespPrefix] RedisValue[] hashFields); + [RespPrefix("FIELDS"), RespPrefix] RedisValue[] fields); - [RespCommand] - private static partial RespOperation HPExpire( + [RespCommand(Parser = "ExpireResultParser.Default")] + private static partial RespOperation HExpireAt( + this in HashCommands context, + RedisKey key, + long seconds, + [RespIgnore(ExpireWhen.Always)] ExpireWhen when, + [RespPrefix("FIELDS"), RespPrefix] RedisValue[] fields); + + [RespCommand(Parser = "ExpireResultParser.Default")] + private static partial RespOperation HPExpire( this in HashCommands context, RedisKey key, long milliseconds, [RespIgnore(ExpireWhen.Always)] ExpireWhen when, - [RespPrefix("FIELDS"), RespPrefix] RedisValue[] hashFields); + [RespPrefix("FIELDS"), RespPrefix] RedisValue[] fields); + + [RespCommand(Parser = "ExpireResultParser.Default")] + private static partial RespOperation HPExpireAt( + this in HashCommands context, + RedisKey key, + long milliseconds, + [RespIgnore(ExpireWhen.Always)] ExpireWhen when, + [RespPrefix("FIELDS"), RespPrefix] RedisValue[] fields); + + private sealed class ExpireResultParser : IRespParser, IRespParser + { + private ExpireResultParser() { } + public static readonly ExpireResultParser Default = new(); + + ExpireResult IRespParser.Parse(ref RespReader reader) + { + if (reader.IsAggregate & !reader.IsNull) + { + // if aggregate: take the first element + reader.MoveNext(); + } + + // otherwise, take first from array + return (ExpireResult)reader.ReadInt64(); + } + + ExpireResult[] IRespParser.Parse(ref RespReader reader) + => reader.ReadArray(static (ref RespReader reader) => (ExpireResult)reader.ReadInt64(), scalar: true)!; + } + + internal static RespOperation HExpire( + this in HashCommands context, + RedisKey key, + TimeSpan expiry, + ExpireWhen when, + RedisValue[] fields) + { + var millis = (long)expiry.TotalMilliseconds; + if (millis % 1000 == 0) // use seconds + { + return HExpire(context, key, millis / 1000, when, fields); + } + + return HPExpire(context, key, millis, when, fields); + } + + internal static RespOperation HExpireAt( + this in HashCommands context, + RedisKey key, + DateTime expiry, + ExpireWhen when, + RedisValue[] fields) + { + var millis = RedisDatabase.GetUnixTimeMilliseconds(expiry); + if (millis % 1000 == 0) // use seconds + { + return HExpireAt(context, key, millis / 1000, when, fields); + } + + return HPExpireAt(context, key, millis, when, fields); + } + + [RespCommand(Parser = "RespParsers.DateTimeFromSeconds")] + public static partial RespOperation HExpireTime( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix("1")] RedisValue field); + + [RespCommand(Parser = "RespParsers.DateTimeArrayFromSeconds")] + public static partial RespOperation HExpireTime( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix] RedisValue[] fields); + + [RespCommand(nameof(HPExpireTime))] + public static partial RespOperation HPExpireTimeRaw( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix("1")] RedisValue field); + + [RespCommand(nameof(HPExpireTime))] + public static partial RespOperation HPExpireTimeRaw( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix] RedisValue[] fields); + + [RespCommand(Parser = "RespParsers.DateTimeFromMilliseconds")] + public static partial RespOperation HPExpireTime( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix("1")] RedisValue field); + + [RespCommand(Parser = "RespParsers.DateTimeArrayFromMilliseconds")] + public static partial RespOperation HPExpireTime( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix] RedisValue[] fields); + + [RespCommand] + public static partial RespOperation HGet( + this in HashCommands context, + RedisKey key, + RedisValue field); + + [RespCommand] + public static partial RespOperation HGetDel( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix] RedisValue[] fields); [RespCommand] - public static partial RespOperation HGet(this in HashCommands context, RedisKey key, RedisValue hashField); + public static partial RespOperation HGetDel( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix("1")] + RedisValue fields); + + [RespCommand(nameof(HGetDel))] + public static partial RespOperation?> HGetDelLease( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix("1")] + RedisValue fields); + + public static RespOperation HGetEx( + this in HashCommands context, + RedisKey key, + RedisValue field, + bool persist = false) + => HGetEx(context, key, persist ? HGetExMode.PERSIST : HGetExMode.None, -1, field); + + public static RespOperation?> HGetExLease( + this in HashCommands context, + RedisKey key, + RedisValue field, + bool persist = false) + => HGetExLease(context, key, persist ? HGetExMode.PERSIST : HGetExMode.None, -1, field); + + internal static RespOperation?> HGetExLease( + this in HashCommands context, + RedisKey key, + RedisValue field, + TimeSpan? expiry, + bool persist) + => expiry.HasValue + ? HGetExLease(context, key, expiry.GetValueOrDefault(), field) + : HGetExLease(context, key, field, persist); + + internal static RespOperation HGetEx( + this in HashCommands context, + RedisKey key, + RedisValue field, + TimeSpan? expiry, + bool persist) + => expiry.HasValue + ? HGetEx(context, key, expiry.GetValueOrDefault(), field) + : HGetEx(context, key, field, persist); + + internal static RespOperation HGetEx( + this in HashCommands context, + RedisKey key, + RedisValue[] fields, + TimeSpan? expiry, + bool persist) + => expiry.HasValue + ? HGetEx(context, key, expiry.GetValueOrDefault(), fields) + : HGetEx(context, key, fields, persist); + + public static RespOperation HGetEx( + this in HashCommands context, + RedisKey key, + RedisValue[] fields, + bool persist = false) + => HGetEx(context, key, persist ? HGetExMode.PERSIST : HGetExMode.None, -1, fields); + + public static RespOperation HGetEx( + this in HashCommands context, + RedisKey key, + DateTime expiry, + RedisValue field) + { + var millis = RedisDatabase.GetUnixTimeMilliseconds(expiry); + if (millis % 1000 == 0) // use seconds + { + return HGetEx(context, key, HGetExMode.EXAT, millis / 1000, field); + } + + return HGetEx(context, key, HGetExMode.PXAT, millis, field); + } + + public static RespOperation?> HGetExLease( + this in HashCommands context, + RedisKey key, + DateTime expiry, + RedisValue field) + { + var millis = RedisDatabase.GetUnixTimeMilliseconds(expiry); + if (millis % 1000 == 0) // use seconds + { + return HGetExLease(context, key, HGetExMode.EXAT, millis / 1000, field); + } + + return HGetExLease(context, key, HGetExMode.PXAT, millis, field); + } + + public static RespOperation HGetEx( + this in HashCommands context, + RedisKey key, + DateTime expiry, + RedisValue[] fields) + { + var millis = RedisDatabase.GetUnixTimeMilliseconds(expiry); + if (millis % 1000 == 0) // use seconds + { + return HGetEx(context, key, HGetExMode.EXAT, millis / 1000, fields); + } + + return HGetEx(context, key, HGetExMode.PXAT, millis, fields); + } + + public static RespOperation HGetEx( + this in HashCommands context, + RedisKey key, + TimeSpan expiry, + RedisValue field) + { + var millis = (long)expiry.TotalMilliseconds; + if (millis % 1000 == 0) // use seconds + { + return HGetEx(context, key, HGetExMode.EX, millis / 1000, field); + } + + return HGetEx(context, key, HGetExMode.PX, millis, field); + } + + public static RespOperation?> HGetExLease( + this in HashCommands context, + RedisKey key, + TimeSpan expiry, + RedisValue field) + { + var millis = (long)expiry.TotalMilliseconds; + if (millis % 1000 == 0) // use seconds + { + return HGetExLease(context, key, HGetExMode.EX, millis / 1000, field); + } + + return HGetExLease(context, key, HGetExMode.PX, millis, field); + } + + public static RespOperation HGetEx( + this in HashCommands context, + RedisKey key, + TimeSpan expiry, + RedisValue[] fields) + { + var millis = (long)expiry.TotalMilliseconds; + if (millis % 1000 == 0) // use seconds + { + return HGetEx(context, key, HGetExMode.EXAT, millis / 1000, fields); + } + + return HGetEx(context, key, HGetExMode.PXAT, millis, fields); + } + + internal enum HGetExMode + { + None, + EX, + PX, + EXAT, + PXAT, + PERSIST, + } + + [RespCommand] + private static partial RespOperation HGetEx( + this in HashCommands context, + RedisKey key, + [RespIgnore(HGetExMode.None)] HGetExMode mode, + [RespIgnore(-1)] long value, + [RespPrefix("FIELDS"), RespPrefix] RedisValue[] fields); + + [RespCommand] + private static partial RespOperation HGetEx( + this in HashCommands context, + RedisKey key, + [RespIgnore(HGetExMode.None)] HGetExMode mode, + [RespIgnore(-1)] long value, + [RespPrefix("FIELDS"), RespPrefix("1")] RedisValue field); + + [RespCommand(nameof(HGetEx))] + private static partial RespOperation?> HGetExLease( + this in HashCommands context, + RedisKey key, + [RespIgnore(HGetExMode.None)] HGetExMode mode, + [RespIgnore(-1)] long value, + [RespPrefix("FIELDS"), RespPrefix("1")] RedisValue field); [RespCommand("hget")] - public static partial RespOperation?> HGetLease(this in HashCommands context, RedisKey key, RedisValue hashField); + public static partial RespOperation?> HGetLease( + this in HashCommands context, + RedisKey key, + RedisValue field); [RespCommand] public static partial RespOperation HGetAll(this in HashCommands context, RedisKey key); [RespCommand] - public static partial RespOperation HIncrBy(this in HashCommands context, RedisKey key, RedisValue hashField, long value = 1); + public static partial RespOperation HIncrBy( + this in HashCommands context, + RedisKey key, + RedisValue field, + long value = 1); [RespCommand] - public static partial RespOperation HIncrByFloat(this in HashCommands context, RedisKey key, RedisValue hashField, double value); + public static partial RespOperation HIncrByFloat( + this in HashCommands context, + RedisKey key, + RedisValue field, + double value); [RespCommand] public static partial RespOperation HKeys(this in HashCommands context, RedisKey key); @@ -67,33 +382,44 @@ private static partial RespOperation HPExpire( public static partial RespOperation HLen(this in HashCommands context, RedisKey key); [RespCommand] - public static partial RespOperation HMGet(this in HashCommands context, RedisKey key, RedisValue[] hashFields); + public static partial RespOperation HMGet( + this in HashCommands context, + RedisKey key, + RedisValue[] fields); [RespCommand] public static partial RespOperation HRandField(this in HashCommands context, RedisKey key); [RespCommand] - public static partial RespOperation HRandField(this in HashCommands context, RedisKey key, long count); + public static partial RespOperation + HRandField(this in HashCommands context, RedisKey key, long count); [RespCommand] - public static partial RespOperation HRandFieldWithValues(this in HashCommands context, RedisKey key, [RespSuffix("WITHVALUES")] long count); + public static partial RespOperation HRandFieldWithValues( + this in HashCommands context, + RedisKey key, + [RespSuffix("WITHVALUES")] long count); [RespCommand] - public static partial RespOperation HSet(this in HashCommands context, RedisKey key, RedisValue hashField, RedisValue value); + public static partial RespOperation HSet( + this in HashCommands context, + RedisKey key, + RedisValue field, + RedisValue value); internal static RespOperation HSet( this in HashCommands context, RedisKey key, - RedisValue hashField, + RedisValue field, RedisValue value, When when) { switch (when) { case When.Always: - return HSet(context, key, hashField, value); + return HSet(context, key, field, value); case When.NotExists: - return HSetNX(context, key, hashField, value); + return HSetNX(context, key, field, value); default: when.AlwaysOrNotExists(); // throws return default; @@ -101,9 +427,9 @@ internal static RespOperation HSet( } [RespCommand(Formatter = "HSetFormatter.Instance")] - public static partial RespOperation HSet(this in HashCommands context, RedisKey key, HashEntry[] hashFields); + public static partial RespOperation HSet(this in HashCommands context, RedisKey key, HashEntry[] fields); - private sealed class HSetFormatter : IRespFormatter<(RedisKey Key, HashEntry[] HashFields)> + private sealed class HSetFormatter : IRespFormatter<(RedisKey Key, HashEntry[] Fields)> { private HSetFormatter() { } public static readonly HSetFormatter Instance = new(); @@ -111,11 +437,11 @@ private HSetFormatter() { } public void Format( scoped ReadOnlySpan command, ref RespWriter writer, - in (RedisKey Key, HashEntry[] HashFields) request) + in (RedisKey Key, HashEntry[] Fields) request) { - writer.WriteCommand(command, 1 + (request.HashFields.Length * 2)); + writer.WriteCommand(command, 1 + (request.Fields.Length * 2)); writer.Write(request.Key); - foreach (var entry in request.HashFields) + foreach (var entry in request.Fields) { writer.Write(entry.Name); writer.Write(entry.Value); @@ -124,10 +450,94 @@ public void Format( } [RespCommand] - public static partial RespOperation HSetNX(this in HashCommands context, RedisKey key, RedisValue hashField, RedisValue value); + public static partial RespOperation HSetNX( + this in HashCommands context, + RedisKey key, + RedisValue field, + RedisValue value); [RespCommand] - public static partial RespOperation HStrLen(this in HashCommands context, RedisKey key, RedisValue hashField); + public static partial RespOperation HStrLen(this in HashCommands context, RedisKey key, RedisValue field); + + [RespCommand(Parser = "PersistResultParser.Default")] + public static partial RespOperation HPersist( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix("1")] RedisValue field); + + [RespCommand(Parser = "PersistResultParser.Default")] + public static partial RespOperation HPersist( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix] RedisValue[] fields); + + private sealed class PersistResultParser : IRespParser, IRespParser, IRespInlineParser + { + private PersistResultParser() { } + public static readonly PersistResultParser Default = new(); + PersistResult IRespParser.Parse(ref RespReader reader) + { + if (reader.IsAggregate) + { + reader.MoveNext(); // read first element from array + } + return (PersistResult)reader.ReadInt64(); + } + + PersistResult[] IRespParser.Parse(ref RespReader reader) => reader.ReadArray( + static (ref RespReader reader) => (PersistResult)reader.ReadInt64(), + scalar: true)!; + } + + [RespCommand(nameof(HPTtl))] + public static partial RespOperation HPTtlRaw( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix("1")] + RedisValue field); + + [RespCommand(nameof(HPTtl))] + public static partial RespOperation HPTtlRaw( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix] RedisValue[] fields); + + [RespCommand(Parser = "RespParsers.TimeSpanFromMilliseconds")] + public static partial RespOperation HPTtl( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix("1")] RedisValue field); + + [RespCommand(Parser = "RespParsers.TimeSpanArrayFromMilliseconds")] + public static partial RespOperation HPTtl( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix] RedisValue[] fields); + + [RespCommand(nameof(HTtl))] + public static partial RespOperation HTtlRaw( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix("1")] + RedisValue field); + + [RespCommand(nameof(HTtl))] + public static partial RespOperation HTtlRaw( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix] RedisValue[] fields); + + [RespCommand(Parser = "RespParsers.TimeSpanFromSeconds")] + public static partial RespOperation HTtl( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix("1")] RedisValue field); + + [RespCommand(Parser = "RespParsers.TimeSpanArrayFromSeconds")] + public static partial RespOperation HTtl( + this in HashCommands context, + RedisKey key, + [RespPrefix("FIELDS"), RespPrefix] RedisValue[] fields); [RespCommand] public static partial RespOperation HVals(this in HashCommands context, RedisKey key); diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs index f59b2fb8c..7563215d5 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs @@ -33,130 +33,303 @@ public Task HashDecrementAsync( CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HIncrByFloat(key, hashField, -value).AsTask(); - public bool HashDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) - => Context(flags).Hashes().HDel(key, hashField).Wait(SyncTimeout); + public bool HashDelete( + RedisKey key, + RedisValue hashFields, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HDel(key, hashFields).Wait(SyncTimeout); - public long HashDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) + public long HashDelete( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HDel(key, hashFields).Wait(SyncTimeout); - public Task HashDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + public Task HashDeleteAsync( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HDel(key, hashField).AsTask(); - public Task HashDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) + public Task HashDeleteAsync( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HDel(key, hashFields).AsTask(); public bool HashExists(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HExists(key, hashField).Wait(SyncTimeout); - public Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + public Task HashExistsAsync( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HExists(key, hashField).AsTask(); - public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public ExpireResult[] HashFieldExpire( + RedisKey key, + RedisValue[] hashFields, + TimeSpan expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HExpire(key, expiry, when, hashFields).Wait(SyncTimeout); - public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public ExpireResult[] HashFieldExpire( + RedisKey key, + RedisValue[] hashFields, + DateTime expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HExpireAt(key, expiry, when, hashFields).Wait(SyncTimeout); - public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public Task HashFieldExpireAsync( + RedisKey key, + RedisValue[] hashFields, + TimeSpan expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HExpire(key, expiry, when, hashFields).AsTask(); - public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public Task HashFieldExpireAsync( + RedisKey key, + RedisValue[] hashFields, + DateTime expiry, + ExpireWhen when = ExpireWhen.Always, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HExpireAt(key, expiry, when, hashFields).AsTask(); - public RedisValue HashFieldGetAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public RedisValue HashFieldGetAndDelete( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetDel(key, hashField).Wait(SyncTimeout); - public RedisValue[] HashFieldGetAndDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public RedisValue[] HashFieldGetAndDelete( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetDel(key, hashFields).Wait(SyncTimeout); - public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public Task HashFieldGetAndDeleteAsync( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetDel(key, hashField).AsTask(); - public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public Task HashFieldGetAndDeleteAsync( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetDel(key, hashFields).AsTask(); - public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public RedisValue HashFieldGetAndSetExpiry( + RedisKey key, + RedisValue hashField, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetEx(key, hashField, expiry, persist).Wait(SyncTimeout); - public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public RedisValue HashFieldGetAndSetExpiry( + RedisKey key, + RedisValue hashField, + DateTime expiry, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetEx(key, expiry, hashField).Wait(SyncTimeout); - public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public RedisValue[] HashFieldGetAndSetExpiry( + RedisKey key, + RedisValue[] hashFields, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetEx(key, hashFields, expiry, persist).Wait(SyncTimeout); - public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public RedisValue[] HashFieldGetAndSetExpiry( + RedisKey key, + RedisValue[] hashFields, + DateTime expiry, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetEx(key, expiry, hashFields).Wait(SyncTimeout); - public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public Task HashFieldGetAndSetExpiryAsync( + RedisKey key, + RedisValue hashField, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetEx(key, hashField, expiry, persist).AsTask(); - public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public Task HashFieldGetAndSetExpiryAsync( + RedisKey key, + RedisValue hashField, + DateTime expiry, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetEx(key, expiry, hashField).AsTask(); - public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public Task HashFieldGetAndSetExpiryAsync( + RedisKey key, + RedisValue[] hashFields, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetEx(key, hashFields, expiry, persist).AsTask(); - public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public Task HashFieldGetAndSetExpiryAsync( + RedisKey key, + RedisValue[] hashFields, + DateTime expiry, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetEx(key, expiry, hashFields).AsTask(); - public long[] HashFieldGetExpireDateTime(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public long[] HashFieldGetExpireDateTime( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HPExpireTimeRaw(key, hashFields).Wait(SyncTimeout); - public Task HashFieldGetExpireDateTimeAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public Task HashFieldGetExpireDateTimeAsync( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HPExpireTimeRaw(key, hashFields).AsTask(); - public Lease? HashFieldGetLeaseAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public Lease? HashFieldGetLeaseAndDelete( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetDelLease(key, hashField).Wait(SyncTimeout); - public Task?> HashFieldGetLeaseAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public Task?> HashFieldGetLeaseAndDeleteAsync( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetDelLease(key, hashField).AsTask(); - public Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public Lease? HashFieldGetLeaseAndSetExpiry( + RedisKey key, + RedisValue hashField, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetExLease(key, hashField, expiry, persist).Wait(SyncTimeout); - public Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public Lease? HashFieldGetLeaseAndSetExpiry( + RedisKey key, + RedisValue hashField, + DateTime expiry, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetExLease(key, expiry, hashField).Wait(SyncTimeout); - public Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public Task?> HashFieldGetLeaseAndSetExpiryAsync( + RedisKey key, + RedisValue hashField, + TimeSpan? expiry = null, + bool persist = false, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetExLease(key, hashField, expiry, persist).AsTask(); - public Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public Task?> HashFieldGetLeaseAndSetExpiryAsync( + RedisKey key, + RedisValue hashField, + DateTime expiry, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HGetExLease(key, expiry, hashField).AsTask(); - public long[] HashFieldGetTimeToLive(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public long[] HashFieldGetTimeToLive( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HTtlRaw(key, hashFields).Wait(SyncTimeout); - public Task HashFieldGetTimeToLiveAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public Task HashFieldGetTimeToLiveAsync( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HTtlRaw(key, hashFields).AsTask(); - public PersistResult[] HashFieldPersist(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public PersistResult[] HashFieldPersist( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HPersist(key, hashFields).Wait(SyncTimeout); - public Task HashFieldPersistAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + public Task HashFieldPersistAsync( + RedisKey key, + RedisValue[] hashFields, + CommandFlags flags = CommandFlags.None) + => Context(flags).Hashes().HPersist(key, hashFields).AsTask(); - public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue hashField, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) + public RedisValue HashFieldSetAndSetExpiry( + RedisKey key, + RedisValue hashField, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue hashField, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) + public RedisValue HashFieldSetAndSetExpiry( + RedisKey key, + RedisValue hashField, + RedisValue value, + DateTime expiry, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) + public RedisValue HashFieldSetAndSetExpiry( + RedisKey key, + HashEntry[] hashFields, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) + public RedisValue HashFieldSetAndSetExpiry( + RedisKey key, + HashEntry[] hashFields, + DateTime expiry, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue hashField, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) + public Task HashFieldSetAndSetExpiryAsync( + RedisKey key, + RedisValue hashField, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue hashField, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) + public Task HashFieldSetAndSetExpiryAsync( + RedisKey key, + RedisValue hashField, + RedisValue value, + DateTime expiry, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) + public Task HashFieldSetAndSetExpiryAsync( + RedisKey key, + HashEntry[] hashFields, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) + public Task HashFieldSetAndSetExpiryAsync( + RedisKey key, + HashEntry[] hashFields, + DateTime expiry, + When when = When.Always, + CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); public RedisValue HashGet(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) @@ -168,7 +341,10 @@ public RedisValue[] HashGet(RedisKey key, RedisValue[] hashFields, CommandFlags public HashEntry[] HashGetAll(RedisKey key, CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HGetAll(key).Wait(SyncTimeout); - public Task HashGetAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + public Task HashGetAsync( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HGet(key, hashField).AsTask(); public Task HashGetAsync( @@ -183,7 +359,10 @@ public Task HashGetAllAsync(RedisKey key, CommandFlags flags = Comm public Lease? HashGetLease(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HGetLease(key, hashField).Wait(SyncTimeout); - public Task?> HashGetLeaseAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + public Task?> HashGetLeaseAsync( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HGetLease(key, hashField).AsTask(); public long HashIncrement( @@ -229,19 +408,31 @@ public Task HashLengthAsync(RedisKey key, CommandFlags flags = CommandFlag public RedisValue HashRandomField(RedisKey key, CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HRandField(key).Wait(SyncTimeout); - public RedisValue[] HashRandomFields(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + public RedisValue[] HashRandomFields( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HRandField(key, count).Wait(SyncTimeout); - public HashEntry[] HashRandomFieldsWithValues(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + public HashEntry[] HashRandomFieldsWithValues( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HRandFieldWithValues(key, count).Wait(SyncTimeout); public Task HashRandomFieldAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HRandField(key).AsTask(); - public Task HashRandomFieldsAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + public Task HashRandomFieldsAsync( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HRandField(key, count).AsTask(); - public Task HashRandomFieldsWithValuesAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + public Task HashRandomFieldsWithValuesAsync( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HRandFieldWithValues(key, count).AsTask(); public IEnumerable HashScan( @@ -253,7 +444,11 @@ public IEnumerable HashScan( CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); - public IEnumerable HashScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) + public IEnumerable HashScan( + RedisKey key, + RedisValue pattern, + int pageSize, + CommandFlags flags) => throw new NotImplementedException(); public IAsyncEnumerable HashScanAsync( @@ -299,13 +494,22 @@ public Task HashSetAsync( CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HSet(key, hashField, value, when).AsTask(); - public void HashSet(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None) + public void HashSet( + RedisKey key, + HashEntry[] hashFields, + CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HSet(key, hashFields).Wait(SyncTimeout); - public Task HashSetAsync(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None) + public Task HashSetAsync( + RedisKey key, + HashEntry[] hashFields, + CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HSet(key, hashFields).AsTask(); - public long HashStringLength(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + public long HashStringLength( + RedisKey key, + RedisValue hashField, + CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HStrLen(key, hashField).Wait(SyncTimeout); public Task HashStringLengthAsync( diff --git a/src/RESPite.StackExchange.Redis/RespFormatters.cs b/src/RESPite.StackExchange.Redis/RespFormatters.cs index 332fc54ca..419957fe7 100644 --- a/src/RESPite.StackExchange.Redis/RespFormatters.cs +++ b/src/RESPite.StackExchange.Redis/RespFormatters.cs @@ -57,6 +57,32 @@ public static void Write(this ref RespWriter writer, in RedisKey key) } } + internal static void WriteBulkString(this ref RespWriter writer, HashCommandsExtensions.HGetExMode when) + { + switch (when) + { + case HashCommandsExtensions.HGetExMode.EX: + writer.WriteRaw("$2\r\nEX\r\n"u8); + break; + case HashCommandsExtensions.HGetExMode.PX: + writer.WriteRaw("$2\r\nPX\r\n"u8); + break; + case HashCommandsExtensions.HGetExMode.EXAT: + writer.WriteRaw("$4\r\nEXAT\r\n"u8); + break; + case HashCommandsExtensions.HGetExMode.PXAT: + writer.WriteRaw("$4\r\nPXAT\r\n"u8); + break; + case HashCommandsExtensions.HGetExMode.PERSIST: + writer.WriteRaw("$7\r\nPERSIST\r\n"u8); + break; + default: + Throw(); + static void Throw() => throw new ArgumentOutOfRangeException(nameof(when)); + break; + } + } + internal static void WriteBulkString(this ref RespWriter writer, ExpireWhen when) { switch (when) diff --git a/src/RESPite.StackExchange.Redis/RespParsers.cs b/src/RESPite.StackExchange.Redis/RespParsers.cs index 4e8dc6d8e..b0d7ea882 100644 --- a/src/RESPite.StackExchange.Redis/RespParsers.cs +++ b/src/RESPite.StackExchange.Redis/RespParsers.cs @@ -12,9 +12,13 @@ public static class RespParsers public static IRespParser> BytesLease => DefaultParser.Instance; public static IRespParser HashEntryArray => DefaultParser.Instance; public static IRespParser TimeSpanFromSeconds => TimeParser.FromSeconds; + public static IRespParser TimeSpanArrayFromSeconds => TimeParser.FromSeconds; public static IRespParser DateTimeFromSeconds => TimeParser.FromSeconds; + public static IRespParser DateTimeArrayFromSeconds => TimeParser.FromSeconds; public static IRespParser TimeSpanFromMilliseconds => TimeParser.FromMilliseconds; + public static IRespParser TimeSpanArrayFromMilliseconds => TimeParser.FromMilliseconds; public static IRespParser DateTimeFromMilliseconds => TimeParser.FromMilliseconds; + public static IRespParser DateTimeArrayFromMilliseconds => TimeParser.FromMilliseconds; internal static IRespParser Int64Index => Int64DefaultNegativeOneParser.Instance; internal static IRespParser ListPopResult => DefaultParser.Instance; @@ -115,26 +119,51 @@ private Int64DefaultNegativeOneParser() { } public long Parse(ref RespReader reader) => reader.IsNull ? -1 : reader.ReadInt64(); } -internal sealed class TimeParser : IRespParser, IRespParser, IRespInlineParser +internal sealed class TimeParser : IRespParser, IRespParser, IRespInlineParser, + IRespParser, IRespParser { private readonly bool _millis; public static readonly TimeParser FromMilliseconds = new(true); public static readonly TimeParser FromSeconds = new(false); - private TimeParser(bool millis) => _millis = millis; - TimeSpan? IRespParser.Parse(ref RespReader reader) + private readonly RespReader.Projection _readTimeSpan; + private readonly RespReader.Projection _readDateTime; + private TimeParser(bool millis) + { + _millis = millis; + _readTimeSpan = ReadTimeSpan; + _readDateTime = ReadDateTime; + } + + TimeSpan? IRespParser.Parse(ref RespReader reader) => ReadTimeSpan(ref reader); + private TimeSpan? ReadTimeSpan(ref RespReader reader) { if (reader.IsNull) return null; + if (reader.IsAggregate) + { + reader.MoveNext(); // take first element from aggregate + if (reader.IsNull) return null; + } var value = reader.ReadInt64(); if (value < 0) return null; // -1 means no expiry and -2 means key does not exist return _millis ? TimeSpan.FromMilliseconds(value) : TimeSpan.FromSeconds(value); } - DateTime? IRespParser.Parse(ref RespReader reader) + DateTime? IRespParser.Parse(ref RespReader reader) => ReadDateTime(ref reader); + private DateTime? ReadDateTime(ref RespReader reader) { if (reader.IsNull) return null; + if (reader.IsAggregate) + { + reader.MoveNext(); // take first element from aggregate + if (reader.IsNull) return null; + } var value = reader.ReadInt64(); if (value < 0) return null; // -1 means no expiry and -2 means key does not exist return _millis ? RedisBase.UnixEpoch.AddMilliseconds(value) : RedisBase.UnixEpoch.AddSeconds(value); } + + TimeSpan?[] IRespParser.Parse(ref RespReader reader) => reader.ReadArray(_readTimeSpan, scalar: true)!; + + DateTime?[] IRespParser.Parse(ref RespReader reader) => reader.ReadArray(_readDateTime, scalar: true)!; } From b71166604635fe2ff9f777dfb5fc2b187c0b3ca1 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 3 Oct 2025 16:08:53 +0100 Subject: [PATCH 101/108] hashes, everything except scans --- .../RedisCommands.HashCommands.cs | 371 +++++++++++++++++- .../RespContextDatabase.Hash.cs | 16 +- .../RespFormatters.cs | 15 +- 3 files changed, 368 insertions(+), 34 deletions(-) diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.HashCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.HashCommands.cs index 7b8ce3c39..48e52aa67 100644 --- a/src/RESPite.StackExchange.Redis/RedisCommands.HashCommands.cs +++ b/src/RESPite.StackExchange.Redis/RedisCommands.HashCommands.cs @@ -182,14 +182,14 @@ public static RespOperation HGetEx( RedisKey key, RedisValue field, bool persist = false) - => HGetEx(context, key, persist ? HGetExMode.PERSIST : HGetExMode.None, -1, field); + => HGetEx(context, key, persist ? HashExpiryMode.PERSIST : HashExpiryMode.None, -1, field); public static RespOperation?> HGetExLease( this in HashCommands context, RedisKey key, RedisValue field, bool persist = false) - => HGetExLease(context, key, persist ? HGetExMode.PERSIST : HGetExMode.None, -1, field); + => HGetExLease(context, key, persist ? HashExpiryMode.PERSIST : HashExpiryMode.None, -1, field); internal static RespOperation?> HGetExLease( this in HashCommands context, @@ -226,7 +226,7 @@ public static RespOperation HGetEx( RedisKey key, RedisValue[] fields, bool persist = false) - => HGetEx(context, key, persist ? HGetExMode.PERSIST : HGetExMode.None, -1, fields); + => HGetEx(context, key, persist ? HashExpiryMode.PERSIST : HashExpiryMode.None, -1, fields); public static RespOperation HGetEx( this in HashCommands context, @@ -237,10 +237,10 @@ public static RespOperation HGetEx( var millis = RedisDatabase.GetUnixTimeMilliseconds(expiry); if (millis % 1000 == 0) // use seconds { - return HGetEx(context, key, HGetExMode.EXAT, millis / 1000, field); + return HGetEx(context, key, HashExpiryMode.EXAT, millis / 1000, field); } - return HGetEx(context, key, HGetExMode.PXAT, millis, field); + return HGetEx(context, key, HashExpiryMode.PXAT, millis, field); } public static RespOperation?> HGetExLease( @@ -252,10 +252,10 @@ public static RespOperation HGetEx( var millis = RedisDatabase.GetUnixTimeMilliseconds(expiry); if (millis % 1000 == 0) // use seconds { - return HGetExLease(context, key, HGetExMode.EXAT, millis / 1000, field); + return HGetExLease(context, key, HashExpiryMode.EXAT, millis / 1000, field); } - return HGetExLease(context, key, HGetExMode.PXAT, millis, field); + return HGetExLease(context, key, HashExpiryMode.PXAT, millis, field); } public static RespOperation HGetEx( @@ -267,10 +267,10 @@ public static RespOperation HGetEx( var millis = RedisDatabase.GetUnixTimeMilliseconds(expiry); if (millis % 1000 == 0) // use seconds { - return HGetEx(context, key, HGetExMode.EXAT, millis / 1000, fields); + return HGetEx(context, key, HashExpiryMode.EXAT, millis / 1000, fields); } - return HGetEx(context, key, HGetExMode.PXAT, millis, fields); + return HGetEx(context, key, HashExpiryMode.PXAT, millis, fields); } public static RespOperation HGetEx( @@ -282,10 +282,10 @@ public static RespOperation HGetEx( var millis = (long)expiry.TotalMilliseconds; if (millis % 1000 == 0) // use seconds { - return HGetEx(context, key, HGetExMode.EX, millis / 1000, field); + return HGetEx(context, key, HashExpiryMode.EX, millis / 1000, field); } - return HGetEx(context, key, HGetExMode.PX, millis, field); + return HGetEx(context, key, HashExpiryMode.PX, millis, field); } public static RespOperation?> HGetExLease( @@ -297,10 +297,10 @@ public static RespOperation HGetEx( var millis = (long)expiry.TotalMilliseconds; if (millis % 1000 == 0) // use seconds { - return HGetExLease(context, key, HGetExMode.EX, millis / 1000, field); + return HGetExLease(context, key, HashExpiryMode.EX, millis / 1000, field); } - return HGetExLease(context, key, HGetExMode.PX, millis, field); + return HGetExLease(context, key, HashExpiryMode.PX, millis, field); } public static RespOperation HGetEx( @@ -312,13 +312,13 @@ public static RespOperation HGetEx( var millis = (long)expiry.TotalMilliseconds; if (millis % 1000 == 0) // use seconds { - return HGetEx(context, key, HGetExMode.EXAT, millis / 1000, fields); + return HGetEx(context, key, HashExpiryMode.EXAT, millis / 1000, fields); } - return HGetEx(context, key, HGetExMode.PXAT, millis, fields); + return HGetEx(context, key, HashExpiryMode.PXAT, millis, fields); } - internal enum HGetExMode + internal enum HashExpiryMode { None, EX, @@ -326,13 +326,14 @@ internal enum HGetExMode EXAT, PXAT, PERSIST, + KEEPTTL, } [RespCommand] private static partial RespOperation HGetEx( this in HashCommands context, RedisKey key, - [RespIgnore(HGetExMode.None)] HGetExMode mode, + [RespIgnore(HashExpiryMode.None)] HashExpiryMode mode, [RespIgnore(-1)] long value, [RespPrefix("FIELDS"), RespPrefix] RedisValue[] fields); @@ -340,7 +341,7 @@ private static partial RespOperation HGetEx( private static partial RespOperation HGetEx( this in HashCommands context, RedisKey key, - [RespIgnore(HGetExMode.None)] HGetExMode mode, + [RespIgnore(HashExpiryMode.None)] HashExpiryMode mode, [RespIgnore(-1)] long value, [RespPrefix("FIELDS"), RespPrefix("1")] RedisValue field); @@ -348,11 +349,11 @@ private static partial RespOperation HGetEx( private static partial RespOperation?> HGetExLease( this in HashCommands context, RedisKey key, - [RespIgnore(HGetExMode.None)] HGetExMode mode, + [RespIgnore(HashExpiryMode.None)] HashExpiryMode mode, [RespIgnore(-1)] long value, [RespPrefix("FIELDS"), RespPrefix("1")] RedisValue field); - [RespCommand("hget")] + [RespCommand(nameof(HGet))] public static partial RespOperation?> HGetLease( this in HashCommands context, RedisKey key, @@ -449,6 +450,336 @@ public void Format( } } + public static RespOperation HSetEx( + this in HashCommands context, + RedisKey key, + TimeSpan expiry, + RedisValue field, + RedisValue value, + When when = When.Always) + { + var millis = (long)expiry.TotalMilliseconds; + if (millis % 1000 == 0) // use seconds + { + return HSetEx(context, key, when, HashExpiryMode.EX, millis / 1000, field, value); + } + + return HSetEx(context, key, when, HashExpiryMode.PX, millis, field, value); + } + + // "Legacy" - OK, so: historically, HashFieldSetAndSetExpiry returned RedisValue; this is ... bizarre, + // since HSETEX returns a bool. So: in the name of not breaking the world, we'll keep returning RedisValue; + // but: in the nice clean shiny API: expose bool + internal static RespOperation HSetExLegacy( + this in HashCommands context, + RedisKey key, + TimeSpan expiry, + RedisValue field, + RedisValue value, + When when) + { + var millis = (long)expiry.TotalMilliseconds; + if (millis % 1000 == 0) // use seconds + { + return HSetExLegacy(context, key, when, HashExpiryMode.EX, millis / 1000, field, value); + } + + return HSetExLegacy(context, key, when, HashExpiryMode.PX, millis, field, value); + } + + internal static RespOperation HSetExLegacy( + this in HashCommands context, + RedisKey key, + TimeSpan? expiry, + RedisValue field, + RedisValue value, + When when, + bool keepTtl) + { + if (expiry.HasValue) return HSetExLegacy(context, key, expiry.GetValueOrDefault(), field, value, when); + return HSetExLegacy(context, key, field, value, when, keepTtl); + } + + public static RespOperation HSetEx( + this in HashCommands context, + RedisKey key, + TimeSpan expiry, + HashEntry[] fields, + When when = When.Always) + { + if (fields.Length == 1) return HSetEx(context, key, expiry, fields[0].Name, fields[0].Value, when); + var millis = (long)expiry.TotalMilliseconds; + if (millis % 1000 == 0) // use seconds + { + return HSetEx(context, key, when, HashExpiryMode.EX, millis / 1000, fields); + } + + return HSetEx(context, key, when, HashExpiryMode.PX, millis, fields); + } + + private static RespOperation HSetExLegacy( + this in HashCommands context, + RedisKey key, + TimeSpan expiry, + HashEntry[] fields, + When when) + { + if (fields.Length == 1) return HSetExLegacy(context, key, expiry, fields[0].Name, fields[0].Value, when); + var millis = (long)expiry.TotalMilliseconds; + if (millis % 1000 == 0) // use seconds + { + return HSetExLegacy(context, key, when, HashExpiryMode.EX, millis / 1000, fields); + } + + return HSetExLegacy(context, key, when, HashExpiryMode.PX, millis, fields); + } + + internal static RespOperation HSetExLegacy( + this in HashCommands context, + RedisKey key, + TimeSpan? expiry, + HashEntry[] fields, + When when, + bool keepTtl) + { + if (expiry.HasValue) return HSetExLegacy(context, key, expiry.GetValueOrDefault(), fields, when); + return HSetExLegacy(context, key, fields, when, keepTtl); + } + + public static RespOperation HSetEx( + this in HashCommands context, + RedisKey key, + DateTime expiry, + RedisValue field, + RedisValue value, + When when = When.Always) + { + var millis = RedisDatabase.GetUnixTimeMilliseconds(expiry); + if (millis % 1000 == 0) // use seconds + { + return HSetEx(context, key, when, HashExpiryMode.EXAT, millis / 1000, field, value); + } + + return HSetEx(context, key, when, HashExpiryMode.PXAT, millis, field, value); + } + + internal static RespOperation HSetExLegacy( + this in HashCommands context, + RedisKey key, + DateTime expiry, + RedisValue field, + RedisValue value, + When when) + { + var millis = RedisDatabase.GetUnixTimeMilliseconds(expiry); + if (millis % 1000 == 0) // use seconds + { + return HSetExLegacy(context, key, when, HashExpiryMode.EXAT, millis / 1000, field, value); + } + + return HSetExLegacy(context, key, when, HashExpiryMode.PXAT, millis, field, value); + } + + public static RespOperation HSetEx( + this in HashCommands context, + RedisKey key, + DateTime expiry, + HashEntry[] fields, + When when = When.Always) + { + if (fields.Length == 1) return HSetEx(context, key, expiry, fields[0].Name, fields[0].Value, when); + var millis = RedisDatabase.GetUnixTimeMilliseconds(expiry); + if (millis % 1000 == 0) // use seconds + { + return HSetEx(context, key, when, HashExpiryMode.EXAT, millis / 1000, fields); + } + + return HSetEx(context, key, when, HashExpiryMode.PXAT, millis, fields); + } + + internal static RespOperation HSetExLegacy( + this in HashCommands context, + RedisKey key, + DateTime expiry, + HashEntry[] fields, + When when) + { + if (fields.Length == 1) return HSetExLegacy(context, key, expiry, fields[0].Name, fields[0].Value, when); + var millis = RedisDatabase.GetUnixTimeMilliseconds(expiry); + if (millis % 1000 == 0) // use seconds + { + return HSetExLegacy(context, key, when, HashExpiryMode.EXAT, millis / 1000, fields); + } + + return HSetExLegacy(context, key, when, HashExpiryMode.PXAT, millis, fields); + } + + public static RespOperation HSetEx( + this in HashCommands context, + RedisKey key, + RedisValue field, + RedisValue value, + When when = When.Always, + bool keepTtl = false) + => HSetEx(context, key, when, keepTtl ? HashExpiryMode.KEEPTTL : HashExpiryMode.None, -1, field, value); + + private static RespOperation HSetExLegacy( + this in HashCommands context, + RedisKey key, + RedisValue field, + RedisValue value, + When when = When.Always, + bool keepTtl = false) + => HSetExLegacy(context, key, when, keepTtl ? HashExpiryMode.KEEPTTL : HashExpiryMode.None, -1, field, value); + + public static RespOperation HSetEx( + this in HashCommands context, + RedisKey key, + HashEntry[] fields, + When when = When.Always, + bool keepTtl = false) + { + if (fields.Length == 1) return HSetEx(context, key, fields[0].Name, fields[0].Value, when, keepTtl); + return HSetEx(context, key, when, keepTtl ? HashExpiryMode.KEEPTTL : HashExpiryMode.None, -1, fields); + } + + private static RespOperation HSetExLegacy( + this in HashCommands context, + RedisKey key, + HashEntry[] fields, + When when, + bool keepTtl) + { + if (fields.Length == 1) return HSetExLegacy(context, key, fields[0].Name, fields[0].Value, when, keepTtl); + return HSetExLegacy(context, key, when, keepTtl ? HashExpiryMode.KEEPTTL : HashExpiryMode.None, -1, fields); + } + + [RespCommand(Formatter = "HSetExFormatter.Instance")] + private static partial RespOperation HSetEx( + this in HashCommands context, + RedisKey key, + When when, + HashExpiryMode mode, + long expiry, + RedisValue field, + RedisValue value); + + [RespCommand(Formatter = "HSetExFormatter.Instance")] + private static partial RespOperation HSetEx( + this in HashCommands context, + RedisKey key, + When when, + HashExpiryMode mode, + long expiry, + HashEntry[] fields); + + [RespCommand(nameof(HSetEx), Formatter = "HSetExFormatter.Instance")] + private static partial RespOperation HSetExLegacy( + this in HashCommands context, + RedisKey key, + When when, + HashExpiryMode mode, + long expiry, + RedisValue field, + RedisValue value); + + [RespCommand(nameof(HSetEx), Formatter = "HSetExFormatter.Instance")] + private static partial RespOperation HSetExLegacy( + this in HashCommands context, + RedisKey key, + When when, + HashExpiryMode mode, + long expiry, + HashEntry[] fields); + + private sealed class + HSetExFormatter : IRespFormatter<(RedisKey Key, When When, HashExpiryMode Mode, long Expiry, HashEntry[] Fields)>, + IRespFormatter<(RedisKey Key, When When, HashExpiryMode Mode, long Expiry, RedisValue Field, RedisValue Value)> + { + private HSetExFormatter() { } + public static readonly HSetExFormatter Instance = new(); + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, When When, HashExpiryMode Mode, long Expiry, HashEntry[] Fields) request) + { + bool __inc0 = request.When != When.Always; // IgnoreExpression + bool __inc1 = request.Mode != HashExpiryMode.None; // IgnoreExpression + bool __inc2 = request.Expiry != -1; // IgnoreExpression +#pragma warning disable SA1118 + writer.WriteCommand(command, 3 // constant args: key, FIELDS, numfields + + (__inc0 ? 1 : 0) // request.When + + (__inc1 ? 1 : 0) // request.Mode + + (__inc2 ? 1 : 0) // request.Expiry + + (request.Fields.Length * 2)); // request.Fields +#pragma warning restore SA1118 + writer.Write(request.Key); + if (__inc0) + { + writer.WriteRaw(GetRaw(request.When)); + } + if (__inc1) + { + writer.WriteBulkString(request.Mode); + } + if (__inc2) + { + writer.WriteBulkString(request.Expiry); + } + writer.WriteRaw("$6\r\nFIELDS\r\n"u8); // FIELDS + writer.WriteBulkString(request.Fields.Length); + foreach (var entry in request.Fields) + { + writer.Write(entry.Name); + writer.Write(entry.Value); + } + } + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, When When, HashExpiryMode Mode, long Expiry, RedisValue Field, RedisValue Value) request) + { + bool __inc0 = request.When != When.Always; // IgnoreExpression + bool __inc1 = request.Mode != HashExpiryMode.None; // IgnoreExpression + bool __inc2 = request.Expiry != -1; // IgnoreExpression +#pragma warning disable SA1118 + writer.WriteCommand(command, 5 // constant args: key, FIELDS, numfields, field, value + + (__inc0 ? 1 : 0) // request.When + + (__inc1 ? 1 : 0) // request.Mode + + (__inc2 ? 1 : 0)); // request.Expiry +#pragma warning restore SA1118 + writer.Write(request.Key); + if (__inc0) + { + writer.WriteRaw(GetRaw(request.When)); + } + if (__inc1) + { + writer.WriteBulkString(request.Mode); + } + if (__inc2) + { + writer.WriteBulkString(request.Expiry); + } + writer.WriteRaw("$6\r\nFIELDS\r\n$1\r\n1\r\n"u8); // FIELDS 1 + writer.Write(request.Field); + writer.Write(request.Value); + } + + private static ReadOnlySpan GetRaw(When when) + { + return when switch + { + When.Exists => "FXX"u8, + When.NotExists => "FNX"u8, + _ => Throw(), + }; + static ReadOnlySpan Throw() => throw new ArgumentOutOfRangeException(nameof(when)); + } + } + [RespCommand] public static partial RespOperation HSetNX( this in HashCommands context, diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs index 7563215d5..d721e9a8a 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs @@ -268,7 +268,7 @@ public RedisValue HashFieldSetAndSetExpiry( bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).Hashes().HSetExLegacy(key, expiry, hashField, value, when, keepTtl).Wait(SyncTimeout); public RedisValue HashFieldSetAndSetExpiry( RedisKey key, @@ -277,7 +277,7 @@ public RedisValue HashFieldSetAndSetExpiry( DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).Hashes().HSetExLegacy(key, expiry, hashField, value, when).Wait(SyncTimeout); public RedisValue HashFieldSetAndSetExpiry( RedisKey key, @@ -286,7 +286,7 @@ public RedisValue HashFieldSetAndSetExpiry( bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).Hashes().HSetExLegacy(key, expiry, hashFields, when, keepTtl).Wait(SyncTimeout); public RedisValue HashFieldSetAndSetExpiry( RedisKey key, @@ -294,7 +294,7 @@ public RedisValue HashFieldSetAndSetExpiry( DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).Hashes().HSetExLegacy(key, expiry, hashFields, when).Wait(SyncTimeout); public Task HashFieldSetAndSetExpiryAsync( RedisKey key, @@ -304,7 +304,7 @@ public Task HashFieldSetAndSetExpiryAsync( bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).Hashes().HSetExLegacy(key, expiry, hashField, value, when, keepTtl).AsTask(); public Task HashFieldSetAndSetExpiryAsync( RedisKey key, @@ -313,7 +313,7 @@ public Task HashFieldSetAndSetExpiryAsync( DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).Hashes().HSetExLegacy(key, expiry, hashField, value, when).AsTask(); public Task HashFieldSetAndSetExpiryAsync( RedisKey key, @@ -322,7 +322,7 @@ public Task HashFieldSetAndSetExpiryAsync( bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).Hashes().HSetExLegacy(key, expiry, hashFields, when, keepTtl).AsTask(); public Task HashFieldSetAndSetExpiryAsync( RedisKey key, @@ -330,7 +330,7 @@ public Task HashFieldSetAndSetExpiryAsync( DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).Hashes().HSetExLegacy(key, expiry, hashFields, when).AsTask(); public RedisValue HashGet(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => Context(flags).Hashes().HGet(key, hashField).Wait(SyncTimeout); diff --git a/src/RESPite.StackExchange.Redis/RespFormatters.cs b/src/RESPite.StackExchange.Redis/RespFormatters.cs index 419957fe7..c2d09a640 100644 --- a/src/RESPite.StackExchange.Redis/RespFormatters.cs +++ b/src/RESPite.StackExchange.Redis/RespFormatters.cs @@ -57,25 +57,28 @@ public static void Write(this ref RespWriter writer, in RedisKey key) } } - internal static void WriteBulkString(this ref RespWriter writer, HashCommandsExtensions.HGetExMode when) + internal static void WriteBulkString(this ref RespWriter writer, HashCommandsExtensions.HashExpiryMode when) { switch (when) { - case HashCommandsExtensions.HGetExMode.EX: + case HashCommandsExtensions.HashExpiryMode.EX: writer.WriteRaw("$2\r\nEX\r\n"u8); break; - case HashCommandsExtensions.HGetExMode.PX: + case HashCommandsExtensions.HashExpiryMode.PX: writer.WriteRaw("$2\r\nPX\r\n"u8); break; - case HashCommandsExtensions.HGetExMode.EXAT: + case HashCommandsExtensions.HashExpiryMode.EXAT: writer.WriteRaw("$4\r\nEXAT\r\n"u8); break; - case HashCommandsExtensions.HGetExMode.PXAT: + case HashCommandsExtensions.HashExpiryMode.PXAT: writer.WriteRaw("$4\r\nPXAT\r\n"u8); break; - case HashCommandsExtensions.HGetExMode.PERSIST: + case HashCommandsExtensions.HashExpiryMode.PERSIST: writer.WriteRaw("$7\r\nPERSIST\r\n"u8); break; + case HashCommandsExtensions.HashExpiryMode.KEEPTTL: + writer.WriteRaw("$7\r\nKEEPTTL\r\n"u8); + break; default: Throw(); static void Throw() => throw new ArgumentOutOfRangeException(nameof(when)); From 630d6af592f8bccef93a52a754bb5ad172960a42 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 3 Oct 2025 16:11:58 +0100 Subject: [PATCH 102/108] hyperloglog --- .../RedisCommands.HyperLogLogCommands.cs | 39 ++++++++++++ .../RespContextDatabase.HyperLogLog.cs | 63 ++++++++++++------- 2 files changed, 81 insertions(+), 21 deletions(-) create mode 100644 src/RESPite.StackExchange.Redis/RedisCommands.HyperLogLogCommands.cs diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.HyperLogLogCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.HyperLogLogCommands.cs new file mode 100644 index 000000000..553d0b78d --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RedisCommands.HyperLogLogCommands.cs @@ -0,0 +1,39 @@ +using System.Runtime.CompilerServices; +using StackExchange.Redis; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable InconsistentNaming +namespace RESPite.StackExchange.Redis; + +internal static partial class RedisCommands +{ + // this is just a "type pun" - it should be an invisible/magic pointer cast to the JIT + public static ref readonly HyperLogLogCommands HyperLogLogs(this in RespContext context) + => ref Unsafe.As(ref Unsafe.AsRef(in context)); +} + +public readonly struct HyperLogLogCommands(in RespContext context) +{ + public readonly RespContext Context = context; // important: this is the only field +} + +internal static partial class HyperLogLogCommandsExtensions +{ + [RespCommand] + public static partial RespOperation PfAdd(this in HyperLogLogCommands context, RedisKey key, RedisValue value); + + [RespCommand] + public static partial RespOperation PfAdd(this in HyperLogLogCommands context, RedisKey key, RedisValue[] values); + + [RespCommand] + public static partial RespOperation PfCount(this in HyperLogLogCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation PfCount(this in HyperLogLogCommands context, RedisKey[] keys); + + [RespCommand] + public static partial RespOperation PfMerge(this in HyperLogLogCommands context, RedisKey destination, RedisKey first, RedisKey second); + + [RespCommand] + public static partial RespOperation PfMerge(this in HyperLogLogCommands context, RedisKey destination, RedisKey[] sourceKeys); +} diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.HyperLogLog.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.HyperLogLog.cs index 0ce47cb9d..ddc07dfba 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.HyperLogLog.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.HyperLogLog.cs @@ -1,58 +1,79 @@ -using StackExchange.Redis; +using RESPite.Messages; +using StackExchange.Redis; namespace RESPite.StackExchange.Redis; internal partial class RespContextDatabase { // Async HyperLogLog methods - public Task HyperLogLogAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HyperLogLogAddAsync( + RedisKey key, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + Context(flags).HyperLogLogs().PfAdd(key, value).AsTask(); - public Task HyperLogLogAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HyperLogLogAddAsync( + RedisKey key, + RedisValue[] values, + CommandFlags flags = CommandFlags.None) => + Context(flags).HyperLogLogs().PfAdd(key, values).AsTask(); - public Task HyperLogLogLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HyperLogLogLengthAsync( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + Context(flags).HyperLogLogs().PfCount(key).AsTask(); - public Task HyperLogLogLengthAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task HyperLogLogLengthAsync( + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + Context(flags).HyperLogLogs().PfCount(keys).AsTask(); public Task HyperLogLogMergeAsync( RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).HyperLogLogs().PfMerge(destination, first, second).AsTask(); public Task HyperLogLogMergeAsync( RedisKey destination, RedisKey[] sourceKeys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).HyperLogLogs().PfMerge(destination, sourceKeys).AsTask(); // Synchronous HyperLogLog methods - public bool HyperLogLogAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public bool HyperLogLogAdd( + RedisKey key, + RedisValue value, + CommandFlags flags = CommandFlags.None) => + Context(flags).HyperLogLogs().PfAdd(key, value).Wait(SyncTimeout); - public bool HyperLogLogAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public bool HyperLogLogAdd( + RedisKey key, + RedisValue[] values, + CommandFlags flags = CommandFlags.None) => + Context(flags).HyperLogLogs().PfAdd(key, values).Wait(SyncTimeout); - public long HyperLogLogLength(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public long HyperLogLogLength( + RedisKey key, + CommandFlags flags = CommandFlags.None) => + Context(flags).HyperLogLogs().PfCount(key).Wait(SyncTimeout); - public long HyperLogLogLength(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public long HyperLogLogLength( + RedisKey[] keys, + CommandFlags flags = CommandFlags.None) => + Context(flags).HyperLogLogs().PfCount(keys).Wait(SyncTimeout); public void HyperLogLogMerge( RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).HyperLogLogs().PfMerge(destination, first, second).Wait(SyncTimeout); public void HyperLogLogMerge( RedisKey destination, RedisKey[] sourceKeys, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).HyperLogLogs().PfMerge(destination, sourceKeys).Wait(SyncTimeout); } From 66e9e03f9d0346b71d18c6ef5bd795ca3c27188e Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 3 Oct 2025 17:08:05 +0100 Subject: [PATCH 103/108] making progress on sorted sets --- .../RespCommandGenerator.cs | 2 + .../RedisCommands.SortedSetCommands.cs | 402 ++++++++++++++++++ .../RespContextDatabase.SortedSet.cs | 160 +++---- .../RespParsers.cs | 26 +- .../Enums/SortedSetWhen.cs | 2 +- src/StackExchange.Redis/RedisDatabase.cs | 8 +- 6 files changed, 483 insertions(+), 117 deletions(-) create mode 100644 src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index ee66af9a1..0b7ee8875 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -1421,6 +1421,8 @@ private static int DataParameterCount( "global::StackExchange.Redis.RedisValue" => "global::RESPite.StackExchange.Redis.RespParsers.RedisValue", "global::StackExchange.Redis.RedisValue[]" => "global::RESPite.StackExchange.Redis.RespParsers.RedisValueArray", "global::StackExchange.Redis.HashEntry[]" => "global::RESPite.StackExchange.Redis.RespParsers.HashEntryArray", + "global::StackExchange.Redis.SortedSetEntry[]" => "global::RESPite.StackExchange.Redis.RespParsers.SortedSetEntryArray", + "global::StackExchange.Redis.SortedSetEntry?" => "global::RESPite.StackExchange.Redis.RespParsers.SortedSetEntry", "global::StackExchange.Redis.Lease" => "global::RESPite.StackExchange.Redis.RespParsers.BytesLease", _ => null, }; diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs new file mode 100644 index 000000000..56849be29 --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs @@ -0,0 +1,402 @@ +using System.Runtime.CompilerServices; +using RESPite.Messages; +using StackExchange.Redis; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable InconsistentNaming +namespace RESPite.StackExchange.Redis; + +internal static partial class RedisCommands +{ + // this is just a "type pun" - it should be an invisible/magic pointer cast to the JIT + public static ref readonly SortedSetCommands SortedSets(this in RespContext context) + => ref Unsafe.As(ref Unsafe.AsRef(in context)); +} + +public readonly struct SortedSetCommands(in RespContext context) +{ + public readonly RespContext Context = context; // important: this is the only field +} + +internal static partial class SortedSetCommandsExtensions +{ + [RespCommand] + public static partial RespOperation ZAdd( + this in SortedSetCommands context, + RedisKey key, + RedisValue member, + double score); + + [RespCommand(Formatter = "ZAddFormatter.Instance")] + public static partial RespOperation ZAdd( + this in SortedSetCommands context, + RedisKey key, + SortedSetWhen when, + RedisValue member, + double score); + + [RespCommand(Formatter = "ZAddFormatter.Instance")] + public static RespOperation ZAdd( + this in SortedSetCommands context, + RedisKey key, + SortedSetEntry[] values) => + context.ZAdd(key, SortedSetWhen.Always, values); + + [RespCommand(Formatter = "ZAddFormatter.Instance")] + public static partial RespOperation ZAdd( + this in SortedSetCommands context, + RedisKey key, + SortedSetWhen when, + SortedSetEntry[] values); + + private sealed class ZAddFormatter : + IRespFormatter<(RedisKey Key, SortedSetWhen When, RedisValue Member, double Score)>, + IRespFormatter<(RedisKey Key, SortedSetWhen When, SortedSetEntry[] Values)> + { + private ZAddFormatter() { } + public static readonly ZAddFormatter Instance = new(); + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, SortedSetWhen When, RedisValue Member, double Score) request) + { + var argCount = 3 + GetWhenFlagCount(request.When); + writer.WriteCommand(command, argCount); + writer.Write(request.Key); + WriteWhenFlags(ref writer, request.When); + writer.WriteBulkString(request.Score); + writer.Write(request.Member); + } + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, SortedSetWhen When, SortedSetEntry[] Values) request) + { + var argCount = 1 + GetWhenFlagCount(request.When) + (request.Values.Length * 2); + writer.WriteCommand(command, argCount); + writer.Write(request.Key); + WriteWhenFlags(ref writer, request.When); + foreach (var entry in request.Values) + { + writer.WriteBulkString(entry.Score); + writer.Write(entry.Element); + } + } + + private static int GetWhenFlagCount(SortedSetWhen when) + { + when &= SortedSetWhen.NotExists | SortedSetWhen.Exists | SortedSetWhen.GreaterThan | SortedSetWhen.LessThan; + return (int)when.CountBits(); + } + + private static void WriteWhenFlags(ref RespWriter writer, SortedSetWhen when) + { + if ((when & SortedSetWhen.NotExists) != 0) + writer.WriteBulkString("NX"u8); + if ((when & SortedSetWhen.Exists) != 0) + writer.WriteBulkString("XX"u8); + if ((when & SortedSetWhen.GreaterThan) != 0) + writer.WriteBulkString("GT"u8); + if ((when & SortedSetWhen.LessThan) != 0) + writer.WriteBulkString("LT"u8); + } + } + + [RespCommand] + public static partial RespOperation ZCard(this in SortedSetCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation ZDiff( + this in SortedSetCommands context, + RedisKey[] keys); + + [RespCommand] + public static partial RespOperation ZDiffWithScores( + this in SortedSetCommands context, + RedisKey[] keys); + + [RespCommand] + public static partial RespOperation ZDiffStore( + this in SortedSetCommands context, + RedisKey destination, + RedisKey[] keys); + + [RespCommand] + public static partial RespOperation ZIncrBy( + this in SortedSetCommands context, + RedisKey key, + RedisValue member, + double increment); + + [RespCommand] + public static partial RespOperation ZInter( + this in SortedSetCommands context, + RedisKey[] keys); + + [RespCommand] + public static partial RespOperation ZInterWithScores( + this in SortedSetCommands context, + RedisKey[] keys); + + [RespCommand] + public static partial RespOperation ZInterStore( + this in SortedSetCommands context, + RedisKey destination, + RedisKey[] keys); + + [RespCommand] + public static partial RespOperation ZLexCount( + this in SortedSetCommands context, + RedisKey key, + RedisValue min, + RedisValue max); + + [RespCommand] + public static partial RespOperation ZPopMax(this in SortedSetCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation ZPopMax( + this in SortedSetCommands context, + RedisKey key, + long count); + + [RespCommand] + public static partial RespOperation ZPopMin(this in SortedSetCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation ZPopMin( + this in SortedSetCommands context, + RedisKey key, + long count); + + [RespCommand] + public static partial RespOperation ZRandMember(this in SortedSetCommands context, RedisKey key); + + [RespCommand] + public static partial RespOperation ZRandMember( + this in SortedSetCommands context, + RedisKey key, + long count); + + [RespCommand] + public static partial RespOperation ZRandMemberWithScores( + this in SortedSetCommands context, + RedisKey key, + long count); + + [RespCommand] + public static partial RespOperation ZRange( + this in SortedSetCommands context, + RedisKey key, + long start, + long stop); + + [RespCommand] + public static partial RespOperation ZRangeWithScores( + this in SortedSetCommands context, + RedisKey key, + long start, + long stop); + + [RespCommand] + public static partial RespOperation ZRangeByLex( + this in SortedSetCommands context, + RedisKey key, + RedisValue min, + RedisValue max); + + [RespCommand] + public static partial RespOperation ZRangeByScore( + this in SortedSetCommands context, + RedisKey key, + double min, + double max); + + [RespCommand] + public static partial RespOperation ZRangeByScoreWithScores( + this in SortedSetCommands context, + RedisKey key, + double min, + double max); + + [RespCommand] + public static partial RespOperation ZRangeStore( + this in SortedSetCommands context, + RedisKey destination, + RedisKey source, + long start, + long stop); + + [RespCommand] + public static partial RespOperation ZRank( + this in SortedSetCommands context, + RedisKey key, + RedisValue member); + + [RespCommand] + public static partial RespOperation ZRem( + this in SortedSetCommands context, + RedisKey key, + RedisValue member); + + [RespCommand] + public static partial RespOperation ZRem( + this in SortedSetCommands context, + RedisKey key, + RedisValue[] members); + + [RespCommand] + public static partial RespOperation ZRemRangeByLex( + this in SortedSetCommands context, + RedisKey key, + RedisValue min, + RedisValue max); + + [RespCommand] + public static partial RespOperation ZRemRangeByRank( + this in SortedSetCommands context, + RedisKey key, + long start, + long stop); + + [RespCommand] + public static partial RespOperation ZRemRangeByScore( + this in SortedSetCommands context, + RedisKey key, + double min, + double max); + + [RespCommand] + public static partial RespOperation ZRevRange( + this in SortedSetCommands context, + RedisKey key, + long start, + long stop); + + [RespCommand] + public static partial RespOperation ZRevRangeWithScores( + this in SortedSetCommands context, + RedisKey key, + long start, + long stop); + + [RespCommand] + public static partial RespOperation ZRevRangeByLex( + this in SortedSetCommands context, + RedisKey key, + RedisValue max, + RedisValue min); + + [RespCommand] + public static partial RespOperation ZRevRangeByScore( + this in SortedSetCommands context, + RedisKey key, + double max, + double min); + + [RespCommand] + public static partial RespOperation ZRevRangeByScoreWithScores( + this in SortedSetCommands context, + RedisKey key, + double max, + double min); + + [RespCommand] + public static partial RespOperation ZRevRank( + this in SortedSetCommands context, + RedisKey key, + RedisValue member); + + [RespCommand] + public static partial RespOperation ZScore( + this in SortedSetCommands context, + RedisKey key, + RedisValue member); + + [RespCommand] + public static partial RespOperation ZScore( + this in SortedSetCommands context, + RedisKey key, + RedisValue[] members); + + [RespCommand] + public static partial RespOperation ZUnion( + this in SortedSetCommands context, + RedisKey[] keys); + + [RespCommand] + public static partial RespOperation ZUnionWithScores( + this in SortedSetCommands context, + RedisKey[] keys); + + [RespCommand] + public static partial RespOperation ZUnionStore( + this in SortedSetCommands context, + RedisKey destination, + RedisKey[] keys); + + internal static RespOperation Combine( + this in SortedSetCommands context, + SetOperation operation, + RedisKey[] keys) => + operation switch + { + SetOperation.Difference => context.ZDiff(keys), + SetOperation.Intersect => context.ZInter(keys), + SetOperation.Union => context.ZUnion(keys), + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; + + internal static RespOperation CombineWithScores( + this in SortedSetCommands context, + SetOperation operation, + RedisKey[] keys) => + operation switch + { + SetOperation.Difference => context.ZDiffWithScores(keys), + SetOperation.Intersect => context.ZInterWithScores(keys), + SetOperation.Union => context.ZUnionWithScores(keys), + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; + + internal static RespOperation CombineAndStore( + this in SortedSetCommands context, + SetOperation operation, + RedisKey destination, + RedisKey[] keys) => + operation switch + { + SetOperation.Difference => context.ZDiffStore(destination, keys), + SetOperation.Intersect => context.ZInterStore(destination, keys), + SetOperation.Union => context.ZUnionStore(destination, keys), + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; + + internal static RespOperation ZPop( + this in SortedSetCommands context, + RedisKey key, + Order order) => + order == Order.Ascending + ? context.ZPopMin(key) + : context.ZPopMax(key); + + internal static RespOperation ZPop( + this in SortedSetCommands context, + RedisKey key, + long count, + Order order) => + order == Order.Ascending + ? context.ZPopMin(key, count) + : context.ZPopMax(key, count); + + internal static RespOperation ZRank( + this in SortedSetCommands context, + RedisKey key, + RedisValue member, + Order order) => + order == Order.Ascending + ? context.ZRank(key, member) + : context.ZRevRank(key, member); +} diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs index 9e3de9135..e30d8c9ac 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs @@ -5,105 +5,71 @@ namespace RESPite.StackExchange.Redis; internal partial class RespContextDatabase { - // Async SortedSet methods - [RespCommand("zadd")] - public partial bool SortedSetAdd( + public bool SortedSetAdd( + RedisKey key, + RedisValue member, + double score, + CommandFlags flags) => + Context(flags).SortedSets().ZAdd(key, member, score).Wait(SyncTimeout); + + public Task SortedSetAddAsync( RedisKey key, RedisValue member, double score, - CommandFlags flags); + CommandFlags flags) => + Context(flags).SortedSets().ZAdd(key, member, score).AsTask(); public bool SortedSetAdd( RedisKey key, RedisValue member, double score, When when, - CommandFlags flags) => when == When.Always - ? SortedSetAdd(key, member, score, flags) // simple mode - : SortedSetAdd(key, member, score, SortedSetWhenExtensions.Parse(when), flags); + CommandFlags flags) => + Context(flags).SortedSets().ZAdd(key, when.ToSortedSetWhen(), member, score).Wait(SyncTimeout); public Task SortedSetAddAsync( RedisKey key, RedisValue member, double score, When when, - CommandFlags flags) => when == When.Always - ? SortedSetAddAsync(key, member, score, flags) // simple mode - : SortedSetAddAsync(key, member, score, SortedSetWhenExtensions.Parse(when), flags); + CommandFlags flags) => + Context(flags).SortedSets().ZAdd(key, when.ToSortedSetWhen(), member, score).AsTask(); + + public bool SortedSetAdd( + RedisKey key, + RedisValue member, + double score, + SortedSetWhen when, + CommandFlags flags) => + Context(flags).SortedSets().ZAdd(key, when, member, score).Wait(SyncTimeout); - [RespCommand("zadd", Formatter = SortedSetAddFormatter.Formatter)] - public partial bool SortedSetAdd( + public Task SortedSetAddAsync( RedisKey key, RedisValue member, double score, SortedSetWhen when, - CommandFlags flags); - - private sealed class - SortedSetAddFormatter : IRespFormatter<(RedisKey Key, RedisValue Member, double Score, SortedSetWhen When)> - { - public const string Formatter = $"{nameof(SortedSetAddFormatter)}.{nameof(Instance)}"; - public static readonly SortedSetAddFormatter Instance = new(); - private SortedSetAddFormatter() { } - - public void Format( - scoped ReadOnlySpan command, - ref RespWriter writer, - in (RedisKey Key, RedisValue Member, double Score, SortedSetWhen When) request) - { - static int Throw(SortedSetWhen when) => throw new ArgumentOutOfRangeException( - paramName: nameof(when), - message: $"Invalid {nameof(SortedSetWhen)} value for ZADD: {when}"); - - // ZADD key [NX | XX] [GT | LT] score member - var argCount = 3 + request.When switch - { - SortedSetWhen.Always => 0, - SortedSetWhen.Exists or SortedSetWhen.NotExists => 1, - SortedSetWhen.GreaterThan or SortedSetWhen.LessThan => 1, - SortedSetWhen.GreaterThan | SortedSetWhen.Exists => 2, - SortedSetWhen.GreaterThan | SortedSetWhen.NotExists => 2, - SortedSetWhen.LessThan | SortedSetWhen.Exists => 2, - SortedSetWhen.LessThan | SortedSetWhen.NotExists => 2, - _ => Throw(request.When), - }; - - writer.WriteCommand(command, argCount); - writer.Write(request.Key); - switch (request.When & (SortedSetWhen.Exists | SortedSetWhen.NotExists)) - { - case SortedSetWhen.Exists: - writer.WriteBulkString("XX"u8); - break; - case SortedSetWhen.NotExists: - writer.WriteBulkString("NX"u8); - break; - } - - writer.WriteBulkString(request.Score); - writer.Write(request.Member); - } - } + CommandFlags flags) => + Context(flags).SortedSets().ZAdd(key, when, member, score).AsTask(); public Task SortedSetAddAsync( RedisKey key, SortedSetEntry[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZAdd(key, values).AsTask(); public Task SortedSetAddAsync( RedisKey key, SortedSetEntry[] values, When when, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZAdd(key, when.ToSortedSetWhen(), values).AsTask(); public Task SortedSetAddAsync( RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZAdd(key, when, values).AsTask(); public Task SortedSetCombineAsync( SetOperation operation, @@ -111,7 +77,7 @@ public Task SortedSetCombineAsync( double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().Combine(operation, keys).AsTask(); public Task SortedSetCombineWithScoresAsync( SetOperation operation, @@ -119,7 +85,7 @@ public Task SortedSetCombineWithScoresAsync( double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().CombineWithScores(operation, keys).AsTask(); public Task SortedSetCombineAndStoreAsync( SetOperation operation, @@ -128,7 +94,7 @@ public Task SortedSetCombineAndStoreAsync( RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().CombineAndStore(operation, destination, new[] { first, second }).AsTask(); public Task SortedSetCombineAndStoreAsync( SetOperation operation, @@ -137,7 +103,7 @@ public Task SortedSetCombineAndStoreAsync( double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().CombineAndStore(operation, destination, keys).AsTask(); public Task SortedSetDecrementAsync( RedisKey key, @@ -267,7 +233,7 @@ public Task SortedSetRangeByValueAsync( RedisValue member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZRank(key, member, order).AsTask(); public Task SortedSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -311,52 +277,26 @@ public IAsyncEnumerable SortedSetScanAsync( throw new NotImplementedException(); public Task SortedSetScoreAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZScore(key, member).AsTask(); public Task SortedSetScoresAsync( RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZScore(key, members).AsTask(); public Task SortedSetPopAsync( RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => - order == Order.Ascending - ? SortedSetPopMinCoreAsync(key, flags) - : SortedSetPopMaxCoreAsync(key, flags); + Context(flags).SortedSets().ZPop(key, order).AsTask(); public Task SortedSetPopAsync( RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - [RespCommand("zpopmin", Parser = SortedSetEntryParser.Parser)] - private partial SortedSetEntry? SortedSetPopMinCore(RedisKey key, CommandFlags flags); - - [RespCommand("zpopmax", Parser = SortedSetEntryParser.Parser)] - private partial SortedSetEntry? SortedSetPopMaxCore(RedisKey key, CommandFlags flags); - - private sealed class SortedSetEntryParser : IRespParser - { - public const string Parser = $"{nameof(SortedSetEntryParser)}.{nameof(Instance)}"; - public static readonly SortedSetEntryParser Instance = new(); - - public SortedSetEntry? Parse(ref RespReader reader) - { - if (reader.IsNull) return null; - reader.DemandAggregate(); - if (reader.AggregateLength() < 2) return null; - reader.MoveNext(); - var member = RespParsers.ReadRedisValue(ref reader); - reader.MoveNext(); - var score = reader.ReadDouble(); - return new SortedSetEntry(member, score); - } - } + Context(flags).SortedSets().ZPop(key, count, order).AsTask(); public Task SortedSetPopAsync( RedisKey[] keys, @@ -382,21 +322,21 @@ public Task SortedSetUpdateAsync( // Synchronous SortedSet methods public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZAdd(key, values).Wait(SyncTimeout); public long SortedSetAdd( RedisKey key, SortedSetEntry[] values, When when, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZAdd(key, when.ToSortedSetWhen(), values).Wait(SyncTimeout); public long SortedSetAdd( RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZAdd(key, when, values).Wait(SyncTimeout); public RedisValue[] SortedSetCombine( SetOperation operation, @@ -404,7 +344,7 @@ public RedisValue[] SortedSetCombine( double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().Combine(operation, keys).Wait(SyncTimeout); public SortedSetEntry[] SortedSetCombineWithScores( SetOperation operation, @@ -412,7 +352,7 @@ public SortedSetEntry[] SortedSetCombineWithScores( double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().CombineWithScores(operation, keys).Wait(SyncTimeout); public long SortedSetCombineAndStore( SetOperation operation, @@ -421,7 +361,7 @@ public long SortedSetCombineAndStore( RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().CombineAndStore(operation, destination, new[] { first, second }).Wait(SyncTimeout); public long SortedSetCombineAndStore( SetOperation operation, @@ -430,7 +370,7 @@ public long SortedSetCombineAndStore( double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().CombineAndStore(operation, destination, keys).Wait(SyncTimeout); public double SortedSetDecrement( RedisKey key, @@ -554,7 +494,7 @@ public RedisValue[] SortedSetRangeByValue( RedisValue member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZRank(key, member, order).Wait(SyncTimeout); public bool SortedSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -599,25 +539,23 @@ public IEnumerable SortedSetScan( throw new NotImplementedException(); public double? SortedSetScore(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZScore(key, member).Wait(SyncTimeout); public double?[] SortedSetScores(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZScore(key, members).Wait(SyncTimeout); public SortedSetEntry? SortedSetPop( RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => - order == Order.Ascending - ? SortedSetPopMinCore(key, flags) - : SortedSetPopMaxCore(key, flags); + Context(flags).SortedSets().ZPop(key, order).Wait(SyncTimeout); public SortedSetEntry[] SortedSetPop( RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZPop(key, count, order).Wait(SyncTimeout); public SortedSetPopResult SortedSetPop( RedisKey[] keys, diff --git a/src/RESPite.StackExchange.Redis/RespParsers.cs b/src/RESPite.StackExchange.Redis/RespParsers.cs index b0d7ea882..adf54a517 100644 --- a/src/RESPite.StackExchange.Redis/RespParsers.cs +++ b/src/RESPite.StackExchange.Redis/RespParsers.cs @@ -11,6 +11,8 @@ public static class RespParsers public static IRespParser RedisKey => DefaultParser.Instance; public static IRespParser> BytesLease => DefaultParser.Instance; public static IRespParser HashEntryArray => DefaultParser.Instance; + public static IRespParser SortedSetEntryArray => DefaultParser.Instance; + public static IRespParser SortedSetEntry => DefaultParser.Instance; public static IRespParser TimeSpanFromSeconds => TimeParser.FromSeconds; public static IRespParser TimeSpanArrayFromSeconds => TimeParser.FromSeconds; public static IRespParser DateTimeFromSeconds => TimeParser.FromSeconds; @@ -46,7 +48,8 @@ public static RedisKey ReadRedisKey(ref RespReader reader) private sealed class DefaultParser : IRespParser, IRespParser, IRespParser>, IRespParser, IRespParser, - IRespParser, IRespParser + IRespParser, IRespParser, IRespParser, + IRespParser { private DefaultParser() { } public static readonly DefaultParser Instance = new(); @@ -109,6 +112,27 @@ ListPopResult IRespParser.Parse(ref RespReader reader) var arr = reader.ReadArray(SharedReadRedisValue, scalar: true)!; return new(key, arr); } + + SortedSetEntry[] IRespParser.Parse(ref RespReader reader) + { + return reader.ReadPairArray( + SharedReadRedisValue, + static (ref RespReader reader) => reader.ReadDouble(), + static (x, y) => new SortedSetEntry(x, y), + scalar: true)!; + } + + SortedSetEntry? IRespParser.Parse(ref RespReader reader) + { + if (reader.IsNull) return null; + reader.DemandAggregate(); + if (reader.AggregateLength() < 2) return null; + reader.MoveNext(); + var member = ReadRedisValue(ref reader); + reader.MoveNext(); + var score = reader.ReadDouble(); + return new SortedSetEntry(member, score); + } } } diff --git a/src/StackExchange.Redis/Enums/SortedSetWhen.cs b/src/StackExchange.Redis/Enums/SortedSetWhen.cs index 517aaeaa5..c7a038325 100644 --- a/src/StackExchange.Redis/Enums/SortedSetWhen.cs +++ b/src/StackExchange.Redis/Enums/SortedSetWhen.cs @@ -45,7 +45,7 @@ internal static uint CountBits(this SortedSetWhen when) return c; } - internal static SortedSetWhen Parse(When when) => when switch + internal static SortedSetWhen ToSortedSetWhen(this When when) => when switch { When.Always => SortedSetWhen.Always, When.Exists => SortedSetWhen.Exists, diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 9b6852042..bc119a601 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -2271,7 +2271,7 @@ public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandF SortedSetAdd(key, member, score, SortedSetWhen.Always, flags); public bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) => - SortedSetAdd(key, member, score, SortedSetWhenExtensions.Parse(when), flags); + SortedSetAdd(key, member, score, SortedSetWhenExtensions.ToSortedSetWhen(when), flags); public bool SortedSetAdd(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) { @@ -2289,7 +2289,7 @@ public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, CommandFlags fla SortedSetAdd(key, values, SortedSetWhen.Always, flags); public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => - SortedSetAdd(key, values, SortedSetWhenExtensions.Parse(when), flags); + SortedSetAdd(key, values, SortedSetWhenExtensions.ToSortedSetWhen(when), flags); public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) { @@ -2307,7 +2307,7 @@ public Task SortedSetAddAsync(RedisKey key, RedisValue member, double scor SortedSetAddAsync(key, member, score, SortedSetWhen.Always, flags); public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) => - SortedSetAddAsync(key, member, score, SortedSetWhenExtensions.Parse(when), flags); + SortedSetAddAsync(key, member, score, SortedSetWhenExtensions.ToSortedSetWhen(when), flags); public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) { @@ -2325,7 +2325,7 @@ public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, Comma SortedSetAddAsync(key, values, SortedSetWhen.Always, flags); public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => - SortedSetAddAsync(key, values, SortedSetWhenExtensions.Parse(when), flags); + SortedSetAddAsync(key, values, SortedSetWhenExtensions.ToSortedSetWhen(when), flags); public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) { From 6483d0a9d4339887283eec85cd9ac5d8c3be7ca4 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 6 Oct 2025 16:11:53 +0100 Subject: [PATCH 104/108] sorted set --- .../BoundedRedisValue.cs | 53 ++++ .../RedisCommands.SortedSetCommands.cs | 231 +++++++++++++++--- .../RespContextDatabase.SortedSet.cs | 34 +-- .../RespContextExtensions.cs | 10 + .../RespFormatters.cs | 42 ++++ src/RESPite/BoundedDouble.cs | 19 ++ src/RESPite/Messages/RespWriter.cs | 90 +++++-- 7 files changed, 415 insertions(+), 64 deletions(-) create mode 100644 src/RESPite.StackExchange.Redis/BoundedRedisValue.cs create mode 100644 src/RESPite/BoundedDouble.cs diff --git a/src/RESPite.StackExchange.Redis/BoundedRedisValue.cs b/src/RESPite.StackExchange.Redis/BoundedRedisValue.cs new file mode 100644 index 000000000..351472a8e --- /dev/null +++ b/src/RESPite.StackExchange.Redis/BoundedRedisValue.cs @@ -0,0 +1,53 @@ +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +public readonly struct BoundedRedisValue : IEquatable +{ + internal readonly RedisValue ValueRaw; + public RedisValue Value => ValueRaw; + private readonly BoundType _type; + internal BoundType Type => _type; + + public BoundedRedisValue(RedisValue value, bool exclusive = false) + { + ValueRaw = value; + _type = exclusive ? BoundType.Exclusive : BoundType.Inclusive; + } + + private BoundedRedisValue(BoundType type) + { + _type = type; + ValueRaw = RedisValue.Null; + } + + internal enum BoundType : byte + { + Inclusive, + Exclusive, + MinValue, + MaxValue, + } + public bool Inclusive => _type == BoundType.Inclusive; + + public override string ToString() => _type switch + { + BoundType.Inclusive => $"[{ValueRaw}", + BoundType.Exclusive => $"({ValueRaw}", + BoundType.MinValue => "-", + BoundType.MaxValue => "+", + _ => _type.ToString(), + }; + + public override int GetHashCode() => unchecked((Value.GetHashCode() * 397) ^ _type.GetHashCode()); + + public override bool Equals(object? obj) => obj is BoundedRedisValue other && Equals(other); + bool IEquatable.Equals(BoundedRedisValue other) => Equals(other); + public bool Equals(in BoundedRedisValue other) => Value.Equals(other.Value) && Inclusive == other.Inclusive; + public static bool operator ==(BoundedRedisValue left, BoundedRedisValue right) => left.Equals(right); + public static bool operator !=(BoundedRedisValue left, BoundedRedisValue right) => !left.Equals(right); + public static implicit operator BoundedRedisValue(RedisValue value) => new(value); + + public static BoundedRedisValue MinValue => new(BoundType.MinValue); + public static BoundedRedisValue MaxValue => new(BoundType.MaxValue); +} diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs index 56849be29..3219d250d 100644 --- a/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs +++ b/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs @@ -104,18 +104,48 @@ private static void WriteWhenFlags(ref RespWriter writer, SortedSetWhen when) } } + internal static RespOperation ZCardOrCount( + this in SortedSetCommands context, + RedisKey key, + double min, + double max, + Exclude exclude) + { + if (double.IsNegativeInfinity(min) && double.IsPositiveInfinity(max)) + { + return context.ZCard(key); + } + + return context.ZCount(key, exclude.Start(min), exclude.Stop(max)); + } + [RespCommand] public static partial RespOperation ZCard(this in SortedSetCommands context, RedisKey key); + [RespCommand] + public static partial RespOperation ZCount(this in SortedSetCommands context, RedisKey key, BoundedDouble min, BoundedDouble max); + [RespCommand] public static partial RespOperation ZDiff( this in SortedSetCommands context, RedisKey[] keys); [RespCommand] + public static partial RespOperation ZDiff( + this in SortedSetCommands context, + RedisKey first, + RedisKey second); + + [RespCommand("zdiff")] public static partial RespOperation ZDiffWithScores( this in SortedSetCommands context, - RedisKey[] keys); + [RespSuffix("WITHSCORES")] RedisKey[] keys); + + [RespCommand("zdiff")] + public static partial RespOperation ZDiffWithScores( + this in SortedSetCommands context, + RedisKey first, + [RespSuffix("WITHSCORES")] RedisKey second); [RespCommand] public static partial RespOperation ZDiffStore( @@ -123,6 +153,13 @@ public static partial RespOperation ZDiffStore( RedisKey destination, RedisKey[] keys); + [RespCommand] + public static partial RespOperation ZDiffStore( + this in SortedSetCommands context, + RedisKey destination, + RedisKey first, + RedisKey second); + [RespCommand] public static partial RespOperation ZIncrBy( this in SortedSetCommands context, @@ -133,25 +170,76 @@ public static partial RespOperation ZIncrBy( [RespCommand] public static partial RespOperation ZInter( this in SortedSetCommands context, - RedisKey[] keys); + RedisKey[] keys, + [RespPrefix("WEIGHTS")] double[]? weights = null, + [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); + + [RespCommand] + public static partial RespOperation ZInter( + this in SortedSetCommands context, + RedisKey first, + RedisKey second, + [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); + + [RespCommand] + public static partial RespOperation ZInterCard( + this in SortedSetCommands context, + [RespPrefix] RedisKey[] keys, + [RespPrefix("LIMIT"), RespIgnore(0)] long limit = 0); [RespCommand] + public static partial RespOperation ZInterCard( + this in SortedSetCommands context, + [RespPrefix("2")] RedisKey first, + RedisKey second, + [RespPrefix("LIMIT"), RespIgnore(0)] long limit = 0); + + [RespCommand("zinter")] public static partial RespOperation ZInterWithScores( this in SortedSetCommands context, - RedisKey[] keys); + [RespSuffix("WITHSCORES")] RedisKey[] keys, + [RespPrefix("WEIGHTS")] double[]? weights = null, + [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); + + [RespCommand("zinter")] + public static partial RespOperation ZInterWithScores( + this in SortedSetCommands context, + RedisKey first, + [RespSuffix("WITHSCORES")] RedisKey second, + [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); [RespCommand] public static partial RespOperation ZInterStore( this in SortedSetCommands context, RedisKey destination, - RedisKey[] keys); + RedisKey[] keys, + [RespPrefix("WEIGHTS")] double[]? weights = null, + [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); + + [RespCommand] + public static partial RespOperation ZInterStore( + this in SortedSetCommands context, + RedisKey destination, + RedisKey first, + RedisKey second, + [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); [RespCommand] public static partial RespOperation ZLexCount( + this in SortedSetCommands context, + RedisKey key, + BoundedRedisValue min, + BoundedRedisValue max); + + internal static RespOperation ZLexCount( this in SortedSetCommands context, RedisKey key, RedisValue min, - RedisValue max); + RedisValue max, + Exclude exclude) => context.ZLexCount( + key, + min.IsNull ? BoundedRedisValue.MinValue : exclude.StartLex(min), + max.IsNull ? BoundedRedisValue.MaxValue : exclude.StopLex(max)); [RespCommand] public static partial RespOperation ZPopMax(this in SortedSetCommands context, RedisKey key); @@ -204,22 +292,22 @@ public static partial RespOperation ZRangeWithScores( public static partial RespOperation ZRangeByLex( this in SortedSetCommands context, RedisKey key, - RedisValue min, - RedisValue max); + BoundedRedisValue min, + BoundedRedisValue max); [RespCommand] public static partial RespOperation ZRangeByScore( this in SortedSetCommands context, RedisKey key, - double min, - double max); + BoundedDouble min, + BoundedDouble max); [RespCommand] public static partial RespOperation ZRangeByScoreWithScores( this in SortedSetCommands context, RedisKey key, - double min, - double max); + BoundedDouble min, + BoundedDouble max); [RespCommand] public static partial RespOperation ZRangeStore( @@ -251,8 +339,8 @@ public static partial RespOperation ZRem( public static partial RespOperation ZRemRangeByLex( this in SortedSetCommands context, RedisKey key, - RedisValue min, - RedisValue max); + BoundedRedisValue min, + BoundedRedisValue max); [RespCommand] public static partial RespOperation ZRemRangeByRank( @@ -265,8 +353,8 @@ public static partial RespOperation ZRemRangeByRank( public static partial RespOperation ZRemRangeByScore( this in SortedSetCommands context, RedisKey key, - double min, - double max); + BoundedDouble min, + BoundedDouble max); [RespCommand] public static partial RespOperation ZRevRange( @@ -286,22 +374,22 @@ public static partial RespOperation ZRevRangeWithScores( public static partial RespOperation ZRevRangeByLex( this in SortedSetCommands context, RedisKey key, - RedisValue max, - RedisValue min); + BoundedRedisValue max, + BoundedRedisValue min); [RespCommand] public static partial RespOperation ZRevRangeByScore( this in SortedSetCommands context, RedisKey key, - double max, - double min); + BoundedDouble max, + BoundedDouble min); [RespCommand] public static partial RespOperation ZRevRangeByScoreWithScores( this in SortedSetCommands context, RedisKey key, - double max, - double min); + BoundedDouble max, + BoundedDouble min); [RespCommand] public static partial RespOperation ZRevRank( @@ -324,40 +412,72 @@ public static partial RespOperation ZRevRangeByScoreWithScores [RespCommand] public static partial RespOperation ZUnion( this in SortedSetCommands context, - RedisKey[] keys); + RedisKey[] keys, + [RespPrefix("WEIGHTS")] double[]? weights = null, + [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); [RespCommand] + public static partial RespOperation ZUnion( + this in SortedSetCommands context, + RedisKey first, + RedisKey second, + [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); + + [RespCommand("zunion")] public static partial RespOperation ZUnionWithScores( this in SortedSetCommands context, - RedisKey[] keys); + [RespSuffix("WITHSCORES")] RedisKey[] keys, + [RespPrefix("WEIGHTS")] double[]? weights = null, + [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); + + [RespCommand("zunion")] + public static partial RespOperation ZUnionWithScores( + this in SortedSetCommands context, + RedisKey first, + [RespSuffix("WITHSCORES")] RedisKey second, + [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); [RespCommand] public static partial RespOperation ZUnionStore( this in SortedSetCommands context, RedisKey destination, - RedisKey[] keys); + RedisKey[] keys, + [RespPrefix("WEIGHTS")] double[]? weights = null, + [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); + + [RespCommand] + public static partial RespOperation ZUnionStore( + this in SortedSetCommands context, + RedisKey destination, + RedisKey first, + RedisKey second, + [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); internal static RespOperation Combine( this in SortedSetCommands context, SetOperation operation, - RedisKey[] keys) => + RedisKey[] keys, + double[]? weights = null, + Aggregate? aggregate = null) => operation switch { SetOperation.Difference => context.ZDiff(keys), - SetOperation.Intersect => context.ZInter(keys), - SetOperation.Union => context.ZUnion(keys), + SetOperation.Intersect => context.ZInter(keys, weights, aggregate), + SetOperation.Union => context.ZUnion(keys, weights, aggregate), _ => throw new ArgumentOutOfRangeException(nameof(operation)), }; internal static RespOperation CombineWithScores( this in SortedSetCommands context, SetOperation operation, - RedisKey[] keys) => + RedisKey[] keys, + double[]? weights = null, + Aggregate? aggregate = null) => operation switch { SetOperation.Difference => context.ZDiffWithScores(keys), - SetOperation.Intersect => context.ZInterWithScores(keys), - SetOperation.Union => context.ZUnionWithScores(keys), + SetOperation.Intersect => context.ZInterWithScores(keys, weights, aggregate), + SetOperation.Union => context.ZUnionWithScores(keys, weights, aggregate), _ => throw new ArgumentOutOfRangeException(nameof(operation)), }; @@ -365,12 +485,57 @@ internal static RespOperation CombineAndStore( this in SortedSetCommands context, SetOperation operation, RedisKey destination, - RedisKey[] keys) => + RedisKey[] keys, + double[]? weights = null, + Aggregate? aggregate = null) => operation switch { SetOperation.Difference => context.ZDiffStore(destination, keys), - SetOperation.Intersect => context.ZInterStore(destination, keys), - SetOperation.Union => context.ZUnionStore(destination, keys), + SetOperation.Intersect => context.ZInterStore(destination, keys, weights, aggregate), + SetOperation.Union => context.ZUnionStore(destination, keys, weights, aggregate), + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; + + internal static RespOperation Combine( + this in SortedSetCommands context, + SetOperation operation, + RedisKey first, + RedisKey second, + Aggregate? aggregate = null) => + operation switch + { + SetOperation.Difference => context.ZDiff(first, second), + SetOperation.Intersect => context.ZInter(first, second, aggregate), + SetOperation.Union => context.ZUnion(first, second, aggregate), + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; + + internal static RespOperation CombineWithScores( + this in SortedSetCommands context, + SetOperation operation, + RedisKey first, + RedisKey second, + Aggregate? aggregate = null) => + operation switch + { + SetOperation.Difference => context.ZDiffWithScores(first, second), + SetOperation.Intersect => context.ZInterWithScores(first, second, aggregate), + SetOperation.Union => context.ZUnionWithScores(first, second, aggregate), + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; + + internal static RespOperation CombineAndStore( + this in SortedSetCommands context, + SetOperation operation, + RedisKey destination, + RedisKey first, + RedisKey second, + Aggregate? aggregate = null) => + operation switch + { + SetOperation.Difference => context.ZDiffStore(destination, first, second), + SetOperation.Intersect => context.ZInterStore(destination, first, second, aggregate), + SetOperation.Union => context.ZUnionStore(destination, first, second, aggregate), _ => throw new ArgumentOutOfRangeException(nameof(operation)), }; diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs index e30d8c9ac..fdadf1766 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs @@ -77,7 +77,7 @@ public Task SortedSetCombineAsync( double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().Combine(operation, keys).AsTask(); + Context(flags).SortedSets().Combine(operation, keys, weights, aggregate).AsTask(); public Task SortedSetCombineWithScoresAsync( SetOperation operation, @@ -85,7 +85,7 @@ public Task SortedSetCombineWithScoresAsync( double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().CombineWithScores(operation, keys).AsTask(); + Context(flags).SortedSets().CombineWithScores(operation, keys, weights, aggregate).AsTask(); public Task SortedSetCombineAndStoreAsync( SetOperation operation, @@ -94,7 +94,7 @@ public Task SortedSetCombineAndStoreAsync( RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().CombineAndStore(operation, destination, new[] { first, second }).AsTask(); + Context(flags).SortedSets().CombineAndStore(operation, destination, first, second, aggregate).AsTask(); public Task SortedSetCombineAndStoreAsync( SetOperation operation, @@ -103,27 +103,27 @@ public Task SortedSetCombineAndStoreAsync( double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().CombineAndStore(operation, destination, keys).AsTask(); + Context(flags).SortedSets().CombineAndStore(operation, destination, keys, weights, aggregate).AsTask(); public Task SortedSetDecrementAsync( RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZIncrBy(key, member, -value).AsTask(); public Task SortedSetIncrementAsync( RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZIncrBy(key, member, value).AsTask(); public Task SortedSetIntersectionLengthAsync( RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZInterCard(keys, limit).AsTask(); public Task SortedSetLengthAsync( RedisKey key, @@ -131,7 +131,7 @@ public Task SortedSetLengthAsync( double max = double.PositiveInfinity, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZCardOrCount(key, min, max, exclude).AsTask(); public Task SortedSetLengthByValueAsync( RedisKey key, @@ -139,7 +139,7 @@ public Task SortedSetLengthByValueAsync( RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZLexCount(key, min, max, exclude).AsTask(); public Task SortedSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => throw new NotImplementedException(); @@ -344,7 +344,7 @@ public RedisValue[] SortedSetCombine( double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().Combine(operation, keys).Wait(SyncTimeout); + Context(flags).SortedSets().Combine(operation, keys, weights, aggregate).Wait(SyncTimeout); public SortedSetEntry[] SortedSetCombineWithScores( SetOperation operation, @@ -352,7 +352,7 @@ public SortedSetEntry[] SortedSetCombineWithScores( double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().CombineWithScores(operation, keys).Wait(SyncTimeout); + Context(flags).SortedSets().CombineWithScores(operation, keys, weights, aggregate).Wait(SyncTimeout); public long SortedSetCombineAndStore( SetOperation operation, @@ -361,7 +361,7 @@ public long SortedSetCombineAndStore( RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().CombineAndStore(operation, destination, new[] { first, second }).Wait(SyncTimeout); + Context(flags).SortedSets().CombineAndStore(operation, destination, first, second, aggregate).Wait(SyncTimeout); public long SortedSetCombineAndStore( SetOperation operation, @@ -370,24 +370,24 @@ public long SortedSetCombineAndStore( double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().CombineAndStore(operation, destination, keys).Wait(SyncTimeout); + Context(flags).SortedSets().CombineAndStore(operation, destination, keys, weights, aggregate).Wait(SyncTimeout); public double SortedSetDecrement( RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZIncrBy(key, member, -value).Wait(SyncTimeout); public double SortedSetIncrement( RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZIncrBy(key, member, value).Wait(SyncTimeout); public long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZInterCard(keys, limit).Wait(SyncTimeout); public long SortedSetLength( RedisKey key, @@ -395,7 +395,7 @@ public long SortedSetLength( double max = double.PositiveInfinity, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Context(flags).SortedSets().ZCardOrCount(key, min, max, exclude).Wait(SyncTimeout); public long SortedSetLengthByValue( RedisKey key, diff --git a/src/RESPite.StackExchange.Redis/RespContextExtensions.cs b/src/RESPite.StackExchange.Redis/RespContextExtensions.cs index b03b1019f..aaea5066e 100644 --- a/src/RESPite.StackExchange.Redis/RespContextExtensions.cs +++ b/src/RESPite.StackExchange.Redis/RespContextExtensions.cs @@ -17,4 +17,14 @@ internal static RespContext With(this in RespContext context, int db, CommandFla | RespContext.RespContextFlags.NoScriptCache; return context.With(db, (RespContext.RespContextFlags)flags, FlagMask); } + + internal static BoundedDouble Start(this Exclude exclude, double value) + => new(value, (exclude & Exclude.Start) != 0); + internal static BoundedDouble Stop(this Exclude exclude, double value) + => new(value, (exclude & Exclude.Stop) != 0); + + internal static BoundedRedisValue StartLex(this Exclude exclude, RedisValue value) + => new(value, (exclude & Exclude.Start) != 0); + internal static BoundedRedisValue StopLex(this Exclude exclude, RedisValue value) + => new(value, (exclude & Exclude.Stop) != 0); } diff --git a/src/RESPite.StackExchange.Redis/RespFormatters.cs b/src/RESPite.StackExchange.Redis/RespFormatters.cs index c2d09a640..a8d8055fa 100644 --- a/src/RESPite.StackExchange.Redis/RespFormatters.cs +++ b/src/RESPite.StackExchange.Redis/RespFormatters.cs @@ -126,6 +126,26 @@ internal static void WriteBulkString(this ref RespWriter writer, ListSide side) } } + internal static void WriteBulkString(this ref RespWriter writer, Aggregate? aggregate) + { + switch (aggregate!.Value) + { + case Aggregate.Sum: + writer.WriteRaw("$3\r\nSUM\r\n"u8); + break; + case Aggregate.Min: + writer.WriteRaw("$3\r\nMIN\r\n"u8); + break; + case Aggregate.Max: + writer.WriteRaw("$3\r\nMAX\r\n"u8); + break; + default: + Throw(); + static void Throw() => throw new ArgumentOutOfRangeException(nameof(aggregate)); + break; + } + } + // ReSharper disable once MemberCanBePrivate.Global public static void Write(this ref RespWriter writer, in RedisValue value) { @@ -156,4 +176,26 @@ public static void Write(this ref RespWriter writer, in RedisValue value) static void Throw(StorageType type) => throw new InvalidOperationException($"Unexpected {type} value."); } + + internal static void WriteBulkString(this ref RespWriter writer, in BoundedRedisValue value) + { + switch (value.Type) + { + case BoundedRedisValue.BoundType.MinValue: + writer.WriteRaw("$1\r\n-\r\n"u8); + break; + case BoundedRedisValue.BoundType.MaxValue: + writer.WriteRaw("$1\r\n+\r\n"u8); + break; + default: + var len = value.ValueRaw.GetByteCount(); + byte[]? lease = null; + var span = len < 128 ? stackalloc byte[128] : (lease = ArrayPool.Shared.Rent(len)); + span[0] = value.Inclusive ? (byte)'[' : (byte)'('; + value.ValueRaw.CopyTo(span.Slice(1)); // allow for the prefix + writer.WriteBulkString(span.Slice(0, len + 1)); + if (lease is not null) ArrayPool.Shared.Return(lease); + break; + } + } } diff --git a/src/RESPite/BoundedDouble.cs b/src/RESPite/BoundedDouble.cs new file mode 100644 index 000000000..896e1a476 --- /dev/null +++ b/src/RESPite/BoundedDouble.cs @@ -0,0 +1,19 @@ +namespace RESPite; + +public readonly struct BoundedDouble(double value, bool exclusive = false) : IEquatable +{ + public double Value { get; } = value; + public bool Inclusive { get; } = !exclusive; + public override string ToString() => Inclusive ? $"{Value}" : $"({Value}"; + public override int GetHashCode() => unchecked((Value.GetHashCode() * 397) ^ Inclusive.GetHashCode()); + + public override bool Equals(object? obj) => obj is BoundedDouble other && Equals(other); + bool IEquatable.Equals(BoundedDouble other) => Equals(other); + public bool Equals(in BoundedDouble other) => Value.Equals(other.Value) && Inclusive == other.Inclusive; + public static bool operator ==(BoundedDouble left, BoundedDouble right) => left.Equals(right); + public static bool operator !=(BoundedDouble left, BoundedDouble right) => !left.Equals(right); + public static implicit operator BoundedDouble(double value) => new(value); + + public static BoundedDouble MinValue => new(double.MinValue); + public static BoundedDouble MaxValue => new(double.MaxValue); +} diff --git a/src/RESPite/Messages/RespWriter.cs b/src/RESPite/Messages/RespWriter.cs index 4a10f3816..b4f38c8b9 100644 --- a/src/RESPite/Messages/RespWriter.cs +++ b/src/RESPite/Messages/RespWriter.cs @@ -213,7 +213,7 @@ public void WriteRaw(scoped ReadOnlySpan buffer) public void WriteCommand(scoped ReadOnlySpan command, int args) { if (args < 0) Throw(); - WritePrefixedInteger(RespPrefix.Array, args + 1); + WritePrefixInteger(RespPrefix.Array, args + 1); if (command.IsEmpty) ThrowEmptyCommand(); if (CommandMap is { } map) { @@ -360,16 +360,31 @@ public void WriteBulkString(in SimpleString value) /// public void WriteBulkString(bool value) => WriteBulkString(value ? 1 : 0); + /// + /// Write a bounded floating point as a bulk string. + /// + public void WriteBulkString(in BoundedDouble value) + { + if (value.Inclusive) + { + WriteBulkString(value.Value); + } + else + { + WriteBulkStringExclusive(value.Value); + } + } + /// /// Write a floating point as a bulk string. /// - public void WriteBulkString(double value) + public void WriteBulkString(double value) // implicitly: inclusive { if (value == 0.0 | double.IsNaN(value) | double.IsInfinity(value)) { - WriteKnownDouble(ref this, value); + WriteKnownDoubleInclusive(ref this, value); - static void WriteKnownDouble(ref RespWriter writer, double value) + static void WriteKnownDoubleInclusive(ref RespWriter writer, double value) { if (value == 0.0) { @@ -396,11 +411,58 @@ static void WriteKnownDouble(ref RespWriter writer, double value) } else { - Debug.Assert(RespConstants.MaxProtocolBytesBytesNumber <= 32); - Span scratch = stackalloc byte[24]; + Debug.Assert((RespConstants.MaxProtocolBytesBytesNumber + 1) <= 32); + Span scratch = stackalloc byte[32]; if (!Utf8Formatter.TryFormat(value, scratch, out int bytes, G17)) ThrowFormatException(); - WritePrefixedInteger(RespPrefix.BulkString, bytes); + + WritePrefixInteger(RespPrefix.BulkString, bytes); + WriteRaw(scratch.Slice(0, bytes)); + WriteCrLf(); + } + } + + private void WriteBulkStringExclusive(double value) + { + if (value == 0.0 | double.IsNaN(value) | double.IsInfinity(value)) + { + WriteKnownDoubleExclusive(ref this, value); + + static void WriteKnownDoubleExclusive(ref RespWriter writer, double value) + { + if (value == 0.0) + { + writer.WriteRaw("$2\r\n(0\r\n"u8); + } + else if (double.IsNaN(value)) + { + writer.WriteRaw("$4\r\n(nan\r\n"u8); + } + else if (double.IsPositiveInfinity(value)) + { + writer.WriteRaw("$4\r\n(inf\r\n"u8); + } + else if (double.IsNegativeInfinity(value)) + { + writer.WriteRaw("$5\r\n(-inf\r\n"u8); + } + else + { + Throw(); + static void Throw() => throw new ArgumentOutOfRangeException(nameof(value)); + } + } + } + else + { + Debug.Assert((RespConstants.MaxProtocolBytesBytesNumber + 1) <= 32); + Span scratch = stackalloc byte[32]; + scratch[0] = (byte)'('; + if (!Utf8Formatter.TryFormat(value, scratch.Slice(1), out int bytes, G17)) + ThrowFormatException(); + bytes++; + + WritePrefixInteger(RespPrefix.BulkString, bytes); WriteRaw(scratch.Slice(0, bytes)); WriteCrLf(); } @@ -468,7 +530,7 @@ public void WriteBulkString(long value) Span scratch = stackalloc byte[24]; if (!Utf8Formatter.TryFormat(value, scratch, out int bytes)) ThrowFormatException(); - WritePrefixedInteger(RespPrefix.BulkString, bytes); + WritePrefixInteger(RespPrefix.BulkString, bytes); WriteRaw(scratch.Slice(0, bytes)); WriteCrLf(); } @@ -505,7 +567,7 @@ public void WriteBulkString(ulong value) private static void ThrowFormatException() => throw new FormatException(); - private void WritePrefixedInteger(RespPrefix prefix, int length) + private void WritePrefixInteger(RespPrefix prefix, int length) { if (Available >= RespConstants.MaxProtocolBytesIntegerInt32) { @@ -584,7 +646,7 @@ internal void WriteBulkStringUnoptimized(string? value) else { var byteCount = RespConstants.UTF8.GetByteCount(value); - WritePrefixedInteger(RespPrefix.BulkString, byteCount); + WritePrefixInteger(RespPrefix.BulkString, byteCount); if (Available >= byteCount) { var actual = RespConstants.UTF8.GetBytes(value.AsSpan(), Tail); @@ -815,7 +877,7 @@ internal void WriteBulkStringUnoptimized(int value) Span scratch = stackalloc byte[16]; if (!Utf8Formatter.TryFormat(value, scratch, out int bytes)) ThrowFormatException(); - WritePrefixedInteger(RespPrefix.BulkString, bytes); + WritePrefixInteger(RespPrefix.BulkString, bytes); WriteRaw(scratch.Slice(0, bytes)); WriteCrLf(); } @@ -870,7 +932,7 @@ public void WriteArray(int count) } } - WritePrefixedInteger(RespPrefix.Array, count); + WritePrefixInteger(RespPrefix.Array, count); } private void WriteBulkStringHeader(int count) @@ -918,10 +980,10 @@ private void WriteBulkStringHeader(int count) } } - WritePrefixedInteger(RespPrefix.BulkString, count); + WritePrefixInteger(RespPrefix.BulkString, count); } - internal void WriteArrayUnpotimized(int count) => WritePrefixedInteger(RespPrefix.Array, count); + internal void WriteArrayUnpotimized(int count) => WritePrefixInteger(RespPrefix.Array, count); private void WriteRawPrechecked(ulong value, int count) { From 8eb7bc7d48d46e146f1a4a097ca690fad7e8bdb3 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 6 Oct 2025 17:09:32 +0100 Subject: [PATCH 105/108] more z --- .../RedisCommands.SortedSetCommands.cs | 193 ++++- .../RespContextDatabase.SortedSet.cs | 671 +++++++++--------- 2 files changed, 510 insertions(+), 354 deletions(-) diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs index 3219d250d..d6d88f692 100644 --- a/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs +++ b/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs @@ -136,12 +136,12 @@ public static partial RespOperation ZDiff( RedisKey first, RedisKey second); - [RespCommand("zdiff")] + [RespCommand(nameof(ZDiff))] public static partial RespOperation ZDiffWithScores( this in SortedSetCommands context, [RespSuffix("WITHSCORES")] RedisKey[] keys); - [RespCommand("zdiff")] + [RespCommand(nameof(ZDiff))] public static partial RespOperation ZDiffWithScores( this in SortedSetCommands context, RedisKey first, @@ -194,14 +194,14 @@ public static partial RespOperation ZInterCard( RedisKey second, [RespPrefix("LIMIT"), RespIgnore(0)] long limit = 0); - [RespCommand("zinter")] + [RespCommand(nameof(ZInter))] public static partial RespOperation ZInterWithScores( this in SortedSetCommands context, [RespSuffix("WITHSCORES")] RedisKey[] keys, [RespPrefix("WEIGHTS")] double[]? weights = null, [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); - [RespCommand("zinter")] + [RespCommand(nameof(ZInter))] public static partial RespOperation ZInterWithScores( this in SortedSetCommands context, RedisKey first, @@ -268,25 +268,163 @@ public static partial RespOperation ZRandMember( RedisKey key, long count); - [RespCommand] + [RespCommand(nameof(ZRandMember))] public static partial RespOperation ZRandMemberWithScores( this in SortedSetCommands context, RedisKey key, - long count); + [RespSuffix("WITHSCORES")] long count); - [RespCommand] + [RespCommand(Formatter = "ZRangeFormatter.NoScores")] public static partial RespOperation ZRange( this in SortedSetCommands context, RedisKey key, long start, - long stop); + long stop, + Order order = Order.Ascending, + long offset = 0, + long count = long.MaxValue); - [RespCommand] + [RespCommand(Formatter = "ZRangeFormatter.WithScores")] public static partial RespOperation ZRangeWithScores( this in SortedSetCommands context, RedisKey key, long start, - long stop); + long stop, + Order order = Order.Ascending, + long offset = 0, + long count = long.MaxValue); + + [RespCommand(Formatter = "ZRangeFormatter.NoScores")] // by lex + internal static partial RespOperation ZRange( + this in SortedSetCommands context, + RedisKey key, + BoundedRedisValue start, + BoundedRedisValue stop, + Order order = Order.Ascending, + long offset = 0, + long count = long.MaxValue); + + [RespCommand(Formatter = "ZRangeFormatter.WithScores")] // by lex + internal static partial RespOperation ZRangeWithScores( + this in SortedSetCommands context, + RedisKey key, + BoundedRedisValue start, + BoundedRedisValue stop, + Order order = Order.Ascending, + long offset = 0, + long count = long.MaxValue); + + [RespCommand(Formatter = "ZRangeFormatter.NoScores")] // by score + internal static partial RespOperation ZRange( + this in SortedSetCommands context, + RedisKey key, + BoundedDouble start, + BoundedDouble stop, + Order order = Order.Ascending, + long offset = 0, + long count = long.MaxValue); + + [RespCommand(Formatter = "ZRangeFormatter.WithScores")] // byscore + internal static partial RespOperation ZRangeWithScores( + this in SortedSetCommands context, + RedisKey key, + BoundedDouble start, + BoundedDouble stop, + Order order = Order.Ascending, + long offset = 0, + long count = long.MaxValue); + + private sealed class ZRangeFormatter : + IRespFormatter<(RedisKey Key, long Start, long Stop, Order Order, long Offset, long Count)>, + IRespFormatter<(RedisKey Key, BoundedDouble Start, BoundedDouble Stop, Order Order, long Offset, long Count)>, + IRespFormatter<(RedisKey Key, BoundedRedisValue Start, BoundedRedisValue Stop, Order Order, long Offset, long Count)> + { + private readonly bool _withScores; + private ZRangeFormatter(bool withScores) => _withScores = withScores; + public static readonly ZRangeFormatter WithScores = new(true), NoScores = new(false); + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, long Start, long Stop, Order Order, long Offset, long Count) request) + { + bool writeLimit = request.Offset != 0 || request.Count != long.MaxValue; + var argCount = 3 + (writeLimit ? 3 : 0) + (_withScores ? 1 : 0) + (request.Order == Order.Descending ? 1 : 0); + writer.WriteCommand(command, argCount); + writer.Write(request.Key); + writer.WriteBulkString(request.Start); + writer.WriteBulkString(request.Stop); + if (request.Order == Order.Descending) + { + writer.WriteRaw("$3\r\nREV\r\n"u8); + } + if (writeLimit) + { + writer.WriteRaw("$5\r\nLIMIT\r\n"u8); + writer.WriteBulkString(request.Offset); + writer.WriteBulkString(request.Count); + } + if (_withScores) + { + writer.WriteRaw("$10\r\nWITHSCORES\r\n"u8); + } + } + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, BoundedDouble Start, BoundedDouble Stop, Order Order, long Offset, long Count) request) + { + bool writeLimit = request.Offset != 0 || request.Count != long.MaxValue; + var argCount = 4 + (writeLimit ? 3 : 0) + (_withScores ? 1 : 0) + (request.Order == Order.Descending ? 1 : 0); + writer.WriteCommand(command, argCount); + writer.Write(request.Key); + writer.WriteBulkString(request.Start); + writer.WriteBulkString(request.Stop); + writer.WriteRaw("$7\r\nBYSCORE\r\n"u8); + if (request.Order == Order.Descending) + { + writer.WriteRaw("$3\r\nREV\r\n"u8); + } + if (writeLimit) + { + writer.WriteRaw("$5\r\nLIMIT\r\n"u8); + writer.WriteBulkString(request.Offset); + writer.WriteBulkString(request.Count); + } + if (_withScores) + { + writer.WriteRaw("$10\r\nWITHSCORES\r\n"u8); + } + } + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, BoundedRedisValue Start, BoundedRedisValue Stop, Order Order, long Offset, long Count) request) + { + bool writeLimit = request.Offset != 0 || request.Count != long.MaxValue; + var argCount = 4 + (writeLimit ? 3 : 0) + (_withScores ? 1 : 0) + (request.Order == Order.Descending ? 1 : 0); + writer.WriteCommand(command, argCount); + writer.Write(request.Key); + writer.WriteBulkString(request.Start); + writer.WriteBulkString(request.Stop); + writer.WriteRaw("$5\r\nBYLEX\r\n"u8); + if (request.Order == Order.Descending) + { + writer.WriteRaw("$3\r\nREV\r\n"u8); + } + if (writeLimit) + { + writer.WriteRaw("$5\r\nLIMIT\r\n"u8); + writer.WriteBulkString(request.Offset); + writer.WriteBulkString(request.Count); + } + if (_withScores) + { + writer.WriteRaw("$10\r\nWITHSCORES\r\n"u8); + } + } + } [RespCommand] public static partial RespOperation ZRangeByLex( @@ -302,12 +440,12 @@ public static partial RespOperation ZRangeByScore( BoundedDouble min, BoundedDouble max); - [RespCommand] + [RespCommand(nameof(ZRangeByScore))] public static partial RespOperation ZRangeByScoreWithScores( this in SortedSetCommands context, RedisKey key, BoundedDouble min, - BoundedDouble max); + [RespSuffix("WITHSCORES")] BoundedDouble max); [RespCommand] public static partial RespOperation ZRangeStore( @@ -363,12 +501,12 @@ public static partial RespOperation ZRevRange( long start, long stop); - [RespCommand] + [RespCommand(nameof(ZRevRange))] public static partial RespOperation ZRevRangeWithScores( this in SortedSetCommands context, RedisKey key, long start, - long stop); + [RespSuffix("WITHSCORES")] long stop); [RespCommand] public static partial RespOperation ZRevRangeByLex( @@ -384,12 +522,12 @@ public static partial RespOperation ZRevRangeByScore( BoundedDouble max, BoundedDouble min); - [RespCommand] + [RespCommand(nameof(ZRevRangeByScore))] public static partial RespOperation ZRevRangeByScoreWithScores( this in SortedSetCommands context, RedisKey key, BoundedDouble max, - BoundedDouble min); + [RespSuffix("WITHSCORES")] BoundedDouble min); [RespCommand] public static partial RespOperation ZRevRank( @@ -423,14 +561,14 @@ public static partial RespOperation ZUnion( RedisKey second, [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); - [RespCommand("zunion")] + [RespCommand(nameof(ZUnion))] public static partial RespOperation ZUnionWithScores( this in SortedSetCommands context, [RespSuffix("WITHSCORES")] RedisKey[] keys, [RespPrefix("WEIGHTS")] double[]? weights = null, [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); - [RespCommand("zunion")] + [RespCommand(nameof(ZUnion))] public static partial RespOperation ZUnionWithScores( this in SortedSetCommands context, RedisKey first, @@ -539,6 +677,25 @@ internal static RespOperation CombineAndStore( _ => throw new ArgumentOutOfRangeException(nameof(operation)), }; + public static RespOperation ZMPop( + this in SortedSetCommands context, + RedisKey[] keys, + Order order = Order.Ascending, + long count = 1) + => order == Order.Ascending ? context.ZMPopMin(keys, count) : context.ZMPopMax(keys, count); + + [RespCommand(nameof(ZMPop))] + private static partial RespOperation ZMPopMin( + this in SortedSetCommands context, + [RespPrefix, RespSuffix("MIN")] RedisKey[] keys, + [RespIgnore(1), RespPrefix("COUNT")] long count); + + [RespCommand(nameof(ZMPop))] + private static partial RespOperation ZMPopMax( + this in SortedSetCommands context, + [RespPrefix, RespSuffix("MAX")] RedisKey[] keys, + [RespIgnore(1), RespPrefix("COUNT")] long count); + internal static RespOperation ZPop( this in SortedSetCommands context, RedisKey key, diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs index fdadf1766..e331944c3 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs @@ -9,83 +9,118 @@ public bool SortedSetAdd( RedisKey key, RedisValue member, double score, - CommandFlags flags) => - Context(flags).SortedSets().ZAdd(key, member, score).Wait(SyncTimeout); + CommandFlags flags) + => Context(flags).SortedSets().ZAdd(key, member, score).Wait(SyncTimeout); - public Task SortedSetAddAsync( + public bool SortedSetAdd( RedisKey key, RedisValue member, double score, - CommandFlags flags) => - Context(flags).SortedSets().ZAdd(key, member, score).AsTask(); + When when, + CommandFlags flags) + => Context(flags).SortedSets().ZAdd(key, when.ToSortedSetWhen(), member, score).Wait(SyncTimeout); public bool SortedSetAdd( RedisKey key, RedisValue member, double score, + SortedSetWhen when, + CommandFlags flags) + => Context(flags).SortedSets().ZAdd(key, when, member, score).Wait(SyncTimeout); + + public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZAdd(key, values).Wait(SyncTimeout); + + public long SortedSetAdd( + RedisKey key, + SortedSetEntry[] values, When when, - CommandFlags flags) => - Context(flags).SortedSets().ZAdd(key, when.ToSortedSetWhen(), member, score).Wait(SyncTimeout); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZAdd(key, when.ToSortedSetWhen(), values).Wait(SyncTimeout); + + public long SortedSetAdd( + RedisKey key, + SortedSetEntry[] values, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZAdd(key, when, values).Wait(SyncTimeout); public Task SortedSetAddAsync( RedisKey key, RedisValue member, double score, - When when, - CommandFlags flags) => - Context(flags).SortedSets().ZAdd(key, when.ToSortedSetWhen(), member, score).AsTask(); + CommandFlags flags) + => Context(flags).SortedSets().ZAdd(key, member, score).AsTask(); - public bool SortedSetAdd( + public Task SortedSetAddAsync( RedisKey key, RedisValue member, double score, - SortedSetWhen when, - CommandFlags flags) => - Context(flags).SortedSets().ZAdd(key, when, member, score).Wait(SyncTimeout); + When when, + CommandFlags flags) + => Context(flags).SortedSets().ZAdd(key, when.ToSortedSetWhen(), member, score).AsTask(); public Task SortedSetAddAsync( RedisKey key, RedisValue member, double score, SortedSetWhen when, - CommandFlags flags) => - Context(flags).SortedSets().ZAdd(key, when, member, score).AsTask(); + CommandFlags flags) + => Context(flags).SortedSets().ZAdd(key, when, member, score).AsTask(); public Task SortedSetAddAsync( RedisKey key, SortedSetEntry[] values, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZAdd(key, values).AsTask(); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZAdd(key, values).AsTask(); public Task SortedSetAddAsync( RedisKey key, SortedSetEntry[] values, When when, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZAdd(key, when.ToSortedSetWhen(), values).AsTask(); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZAdd(key, when.ToSortedSetWhen(), values).AsTask(); public Task SortedSetAddAsync( RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZAdd(key, when, values).AsTask(); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZAdd(key, when, values).AsTask(); - public Task SortedSetCombineAsync( + public RedisValue[] SortedSetCombine( SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().Combine(operation, keys, weights, aggregate).AsTask(); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().Combine(operation, keys, weights, aggregate).Wait(SyncTimeout); - public Task SortedSetCombineWithScoresAsync( + public long SortedSetCombineAndStore( + SetOperation operation, + RedisKey destination, + RedisKey first, + RedisKey second, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().CombineAndStore(operation, destination, first, second, aggregate).Wait(SyncTimeout); + + public long SortedSetCombineAndStore( + SetOperation operation, + RedisKey destination, + RedisKey[] keys, + double[]? weights = null, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().CombineAndStore(operation, destination, keys, weights, aggregate).Wait(SyncTimeout); + + public Task SortedSetCombineAsync( SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().CombineWithScores(operation, keys, weights, aggregate).AsTask(); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().Combine(operation, keys, weights, aggregate).AsTask(); public Task SortedSetCombineAndStoreAsync( SetOperation operation, @@ -93,8 +128,8 @@ public Task SortedSetCombineAndStoreAsync( RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().CombineAndStore(operation, destination, first, second, aggregate).AsTask(); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().CombineAndStore(operation, destination, first, second, aggregate).AsTask(); public Task SortedSetCombineAndStoreAsync( SetOperation operation, @@ -102,330 +137,205 @@ public Task SortedSetCombineAndStoreAsync( RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().CombineAndStore(operation, destination, keys, weights, aggregate).AsTask(); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().CombineAndStore(operation, destination, keys, weights, aggregate).AsTask(); + + public SortedSetEntry[] SortedSetCombineWithScores( + SetOperation operation, + RedisKey[] keys, + double[]? weights = null, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().CombineWithScores(operation, keys, weights, aggregate).Wait(SyncTimeout); + + public Task SortedSetCombineWithScoresAsync( + SetOperation operation, + RedisKey[] keys, + double[]? weights = null, + Aggregate aggregate = Aggregate.Sum, + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().CombineWithScores(operation, keys, weights, aggregate).AsTask(); + + public double SortedSetDecrement( + RedisKey key, + RedisValue member, + double value, + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZIncrBy(key, member, -value).Wait(SyncTimeout); public Task SortedSetDecrementAsync( RedisKey key, RedisValue member, double value, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZIncrBy(key, member, -value).AsTask(); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZIncrBy(key, member, -value).AsTask(); + + public double SortedSetIncrement( + RedisKey key, + RedisValue member, + double value, + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZIncrBy(key, member, value).Wait(SyncTimeout); public Task SortedSetIncrementAsync( RedisKey key, RedisValue member, double value, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZIncrBy(key, member, value).AsTask(); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZIncrBy(key, member, value).AsTask(); + + public long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZInterCard(keys, limit).Wait(SyncTimeout); public Task SortedSetIntersectionLengthAsync( RedisKey[] keys, long limit = 0, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZInterCard(keys, limit).AsTask(); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZInterCard(keys, limit).AsTask(); - public Task SortedSetLengthAsync( + public long SortedSetLength( RedisKey key, double min = double.NegativeInfinity, double max = double.PositiveInfinity, Exclude exclude = Exclude.None, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZCardOrCount(key, min, max, exclude).AsTask(); - - public Task SortedSetLengthByValueAsync( - RedisKey key, - RedisValue min, - RedisValue max, - Exclude exclude = Exclude.None, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZLexCount(key, min, max, exclude).AsTask(); - - public Task SortedSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRandomMembersAsync( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRandomMembersWithScoresAsync( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeByRankAsync( - RedisKey key, - long start = 0, - long stop = -1, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZCardOrCount(key, min, max, exclude).Wait(SyncTimeout); - public Task SortedSetRangeAndStoreAsync( - RedisKey sourceKey, - RedisKey destinationKey, - RedisValue start, - RedisValue stop, - SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, - Exclude exclude = Exclude.None, - Order order = Order.Ascending, - long skip = 0, - long? take = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeByRankWithScoresAsync( - RedisKey key, - long start = 0, - long stop = -1, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeByScoreAsync( - RedisKey key, - double start = double.NegativeInfinity, - double stop = double.PositiveInfinity, - Exclude exclude = Exclude.None, - Order order = Order.Ascending, - long skip = 0, - long take = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeByScoreWithScoresAsync( - RedisKey key, - double start = double.NegativeInfinity, - double stop = double.PositiveInfinity, - Exclude exclude = Exclude.None, - Order order = Order.Ascending, - long skip = 0, - long take = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRangeByValueAsync( + public Task SortedSetLengthAsync( RedisKey key, - RedisValue min = default, - RedisValue max = default, + double min = double.NegativeInfinity, + double max = double.PositiveInfinity, Exclude exclude = Exclude.None, - long skip = 0, - long take = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZCardOrCount(key, min, max, exclude).AsTask(); - public Task SortedSetRangeByValueAsync( + public long SortedSetLengthByValue( RedisKey key, RedisValue min, RedisValue max, - Exclude exclude, - Order order, - long skip = 0, - long take = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRankAsync( - RedisKey key, - RedisValue member, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZRank(key, member, order).AsTask(); - - public Task SortedSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRemoveAsync( - RedisKey key, - RedisValue[] members, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRemoveRangeByRankAsync( - RedisKey key, - long start, - long stop, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetRemoveRangeByScoreAsync( - RedisKey key, - double start, - double stop, Exclude exclude = Exclude.None, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZLexCount(key, min, max, exclude).Wait(SyncTimeout); - public Task SortedSetRemoveRangeByValueAsync( + public Task SortedSetLengthByValueAsync( RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZLexCount(key, min, max, exclude).AsTask(); - public IAsyncEnumerable SortedSetScanAsync( + public SortedSetEntry? SortedSetPop( RedisKey key, - RedisValue pattern = default, - int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, - long cursor = RedisBase.CursorUtils.Origin, - int pageOffset = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public Task SortedSetScoreAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZScore(key, member).AsTask(); + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZPop(key, order).Wait(SyncTimeout); - public Task SortedSetScoresAsync( + public SortedSetEntry[] SortedSetPop( RedisKey key, - RedisValue[] members, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZScore(key, members).AsTask(); + long count, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZPop(key, count, order).Wait(SyncTimeout); + + public SortedSetPopResult SortedSetPop( + RedisKey[] keys, + long count, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZMPop(keys, order, count).Wait(SyncTimeout); public Task SortedSetPopAsync( RedisKey key, Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZPop(key, order).AsTask(); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZPop(key, order).AsTask(); public Task SortedSetPopAsync( RedisKey key, long count, Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZPop(key, count, order).AsTask(); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZPop(key, count, order).AsTask(); public Task SortedSetPopAsync( RedisKey[] keys, long count, Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZMPop(keys, order, count).AsTask(); - public Task SortedSetUpdateAsync( - RedisKey key, - RedisValue member, - double score, - SortedSetWhen when = SortedSetWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public RedisValue SortedSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZRandMember(key).Wait(SyncTimeout); - public Task SortedSetUpdateAsync( - RedisKey key, - SortedSetEntry[] values, - SortedSetWhen when = SortedSetWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task SortedSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZRandMember(key).AsTask(); - // Synchronous SortedSet methods - public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZAdd(key, values).Wait(SyncTimeout); + public RedisValue[] SortedSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZRandMember(key, count).Wait(SyncTimeout); - public long SortedSetAdd( + public Task SortedSetRandomMembersAsync( RedisKey key, - SortedSetEntry[] values, - When when, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZAdd(key, when.ToSortedSetWhen(), values).Wait(SyncTimeout); + long count, + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZRandMember(key, count).AsTask(); - public long SortedSetAdd( + public SortedSetEntry[] SortedSetRandomMembersWithScores( RedisKey key, - SortedSetEntry[] values, - SortedSetWhen when = SortedSetWhen.Always, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZAdd(key, when, values).Wait(SyncTimeout); - - public RedisValue[] SortedSetCombine( - SetOperation operation, - RedisKey[] keys, - double[]? weights = null, - Aggregate aggregate = Aggregate.Sum, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().Combine(operation, keys, weights, aggregate).Wait(SyncTimeout); - - public SortedSetEntry[] SortedSetCombineWithScores( - SetOperation operation, - RedisKey[] keys, - double[]? weights = null, - Aggregate aggregate = Aggregate.Sum, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().CombineWithScores(operation, keys, weights, aggregate).Wait(SyncTimeout); - - public long SortedSetCombineAndStore( - SetOperation operation, - RedisKey destination, - RedisKey first, - RedisKey second, - Aggregate aggregate = Aggregate.Sum, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().CombineAndStore(operation, destination, first, second, aggregate).Wait(SyncTimeout); + long count, + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZRandMemberWithScores(key, count).Wait(SyncTimeout); - public long SortedSetCombineAndStore( - SetOperation operation, - RedisKey destination, - RedisKey[] keys, - double[]? weights = null, - Aggregate aggregate = Aggregate.Sum, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().CombineAndStore(operation, destination, keys, weights, aggregate).Wait(SyncTimeout); + public Task SortedSetRandomMembersWithScoresAsync( + RedisKey key, + long count, + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZRandMemberWithScores(key, count).AsTask(); - public double SortedSetDecrement( + public long? SortedSetRank( RedisKey key, RedisValue member, - double value, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZIncrBy(key, member, -value).Wait(SyncTimeout); + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZRank(key, member, order).Wait(SyncTimeout); - public double SortedSetIncrement( + public Task SortedSetRankAsync( RedisKey key, RedisValue member, - double value, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZIncrBy(key, member, value).Wait(SyncTimeout); - - public long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZInterCard(keys, limit).Wait(SyncTimeout); + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZRank(key, member, order).AsTask(); - public long SortedSetLength( + public RedisValue[] SortedSetRangeByRank( RedisKey key, - double min = double.NegativeInfinity, - double max = double.PositiveInfinity, - Exclude exclude = Exclude.None, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZCardOrCount(key, min, max, exclude).Wait(SyncTimeout); + long start = 0, + long stop = -1, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public long SortedSetLengthByValue( - RedisKey key, - RedisValue min, - RedisValue max, + public long SortedSetRangeAndStore( + RedisKey sourceKey, + RedisKey destinationKey, + RedisValue start, + RedisValue stop, + SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, Exclude exclude = Exclude.None, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue SortedSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public RedisValue[] SortedSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); - - public SortedSetEntry[] SortedSetRandomMembersWithScores( - RedisKey key, - long count, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + Order order = Order.Ascending, + long skip = 0, + long? take = null, + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public RedisValue[] SortedSetRangeByRank( + public Task SortedSetRangeByRankAsync( RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public long SortedSetRangeAndStore( + public Task SortedSetRangeAndStoreAsync( RedisKey sourceKey, RedisKey destinationKey, RedisValue start, @@ -435,16 +345,24 @@ public long SortedSetRangeAndStore( Order order = Order.Ascending, long skip = 0, long? take = null, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); public SortedSetEntry[] SortedSetRangeByRankWithScores( RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); + + public Task SortedSetRangeByRankWithScoresAsync( + RedisKey key, + long start = 0, + long stop = -1, + Order order = Order.Ascending, + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); public RedisValue[] SortedSetRangeByScore( RedisKey key, @@ -454,8 +372,19 @@ public RedisValue[] SortedSetRangeByScore( Order order = Order.Ascending, long skip = 0, long take = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); + + public Task SortedSetRangeByScoreAsync( + RedisKey key, + double start = double.NegativeInfinity, + double stop = double.PositiveInfinity, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); public SortedSetEntry[] SortedSetRangeByScoreWithScores( RedisKey key, @@ -465,8 +394,19 @@ public SortedSetEntry[] SortedSetRangeByScoreWithScores( Order order = Order.Ascending, long skip = 0, long take = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); + + public Task SortedSetRangeByScoreWithScoresAsync( + RedisKey key, + double start = double.NegativeInfinity, + double stop = double.PositiveInfinity, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); public RedisValue[] SortedSetRangeByValue( RedisKey key, @@ -475,8 +415,8 @@ public RedisValue[] SortedSetRangeByValue( Exclude exclude = Exclude.None, long skip = 0, long take = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); public RedisValue[] SortedSetRangeByValue( RedisKey key, @@ -486,48 +426,94 @@ public RedisValue[] SortedSetRangeByValue( Order order, long skip = 0, long take = -1, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public long? SortedSetRank( + public Task SortedSetRangeByValueAsync( RedisKey key, - RedisValue member, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZRank(key, member, order).Wait(SyncTimeout); + RedisValue min = default, + RedisValue max = default, + Exclude exclude = Exclude.None, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); + + public Task SortedSetRangeByValueAsync( + RedisKey key, + RedisValue min, + RedisValue max, + Exclude exclude, + Order order, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); + + public bool SortedSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public bool SortedSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public long SortedSetRemove(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public long SortedSetRemove(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task SortedSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); + + public Task SortedSetRemoveAsync( + RedisKey key, + RedisValue[] members, + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); public long SortedSetRemoveRangeByRank( RedisKey key, long start, long stop, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); + + public Task SortedSetRemoveRangeByRankAsync( + RedisKey key, + long start, + long stop, + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); public long SortedSetRemoveRangeByScore( RedisKey key, double start, double stop, Exclude exclude = Exclude.None, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); + + public Task SortedSetRemoveRangeByScoreAsync( + RedisKey key, + double start, + double stop, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); public long SortedSetRemoveRangeByValue( RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); + + public Task SortedSetRemoveRangeByValueAsync( + RedisKey key, + RedisValue min, + RedisValue max, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); public IEnumerable - SortedSetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) => - throw new NotImplementedException(); + SortedSetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) + => throw new NotImplementedException(); public IEnumerable SortedSetScan( RedisKey key, @@ -535,47 +521,60 @@ public IEnumerable SortedSetScan( int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public double? SortedSetScore(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZScore(key, member).Wait(SyncTimeout); + public IAsyncEnumerable SortedSetScanAsync( + RedisKey key, + RedisValue pattern = default, + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); - public double?[] SortedSetScores(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZScore(key, members).Wait(SyncTimeout); + public double? SortedSetScore(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZScore(key, member).Wait(SyncTimeout); - public SortedSetEntry? SortedSetPop( - RedisKey key, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZPop(key, order).Wait(SyncTimeout); + public Task SortedSetScoreAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZScore(key, member).AsTask(); - public SortedSetEntry[] SortedSetPop( - RedisKey key, - long count, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - Context(flags).SortedSets().ZPop(key, count, order).Wait(SyncTimeout); + public double?[] SortedSetScores(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZScore(key, members).Wait(SyncTimeout); - public SortedSetPopResult SortedSetPop( - RedisKey[] keys, - long count, - Order order = Order.Ascending, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + public Task SortedSetScoresAsync( + RedisKey key, + RedisValue[] members, + CommandFlags flags = CommandFlags.None) + => Context(flags).SortedSets().ZScore(key, members).AsTask(); public bool SortedSetUpdate( RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); public long SortedSetUpdate( RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, - CommandFlags flags = CommandFlags.None) => - throw new NotImplementedException(); + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); + + public Task SortedSetUpdateAsync( + RedisKey key, + RedisValue member, + double score, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); + + public Task SortedSetUpdateAsync( + RedisKey key, + SortedSetEntry[] values, + SortedSetWhen when = SortedSetWhen.Always, + CommandFlags flags = CommandFlags.None) + => throw new NotImplementedException(); } From 34ca6a31fb466d7ead0ffb4f0ff2dabeb2adf5c1 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 7 Oct 2025 14:31:13 +0100 Subject: [PATCH 106/108] zrange --- .../BoundedRedisValue.cs | 53 -- .../RedisCommands.SortedSetCommands.cs | 896 ++++++++++-------- .../RespContextDatabase.SortedSet.cs | 8 +- .../RespContextExtensions.cs | 10 - .../RespFormatters.cs | 22 - src/RESPite/BoundedDouble.cs | 19 - src/RESPite/Messages/RespWriter.cs | 17 +- 7 files changed, 514 insertions(+), 511 deletions(-) delete mode 100644 src/RESPite.StackExchange.Redis/BoundedRedisValue.cs delete mode 100644 src/RESPite/BoundedDouble.cs diff --git a/src/RESPite.StackExchange.Redis/BoundedRedisValue.cs b/src/RESPite.StackExchange.Redis/BoundedRedisValue.cs deleted file mode 100644 index 351472a8e..000000000 --- a/src/RESPite.StackExchange.Redis/BoundedRedisValue.cs +++ /dev/null @@ -1,53 +0,0 @@ -using StackExchange.Redis; - -namespace RESPite.StackExchange.Redis; - -public readonly struct BoundedRedisValue : IEquatable -{ - internal readonly RedisValue ValueRaw; - public RedisValue Value => ValueRaw; - private readonly BoundType _type; - internal BoundType Type => _type; - - public BoundedRedisValue(RedisValue value, bool exclusive = false) - { - ValueRaw = value; - _type = exclusive ? BoundType.Exclusive : BoundType.Inclusive; - } - - private BoundedRedisValue(BoundType type) - { - _type = type; - ValueRaw = RedisValue.Null; - } - - internal enum BoundType : byte - { - Inclusive, - Exclusive, - MinValue, - MaxValue, - } - public bool Inclusive => _type == BoundType.Inclusive; - - public override string ToString() => _type switch - { - BoundType.Inclusive => $"[{ValueRaw}", - BoundType.Exclusive => $"({ValueRaw}", - BoundType.MinValue => "-", - BoundType.MaxValue => "+", - _ => _type.ToString(), - }; - - public override int GetHashCode() => unchecked((Value.GetHashCode() * 397) ^ _type.GetHashCode()); - - public override bool Equals(object? obj) => obj is BoundedRedisValue other && Equals(other); - bool IEquatable.Equals(BoundedRedisValue other) => Equals(other); - public bool Equals(in BoundedRedisValue other) => Value.Equals(other.Value) && Inclusive == other.Inclusive; - public static bool operator ==(BoundedRedisValue left, BoundedRedisValue right) => left.Equals(right); - public static bool operator !=(BoundedRedisValue left, BoundedRedisValue right) => !left.Equals(right); - public static implicit operator BoundedRedisValue(RedisValue value) => new(value); - - public static BoundedRedisValue MinValue => new(BoundType.MinValue); - public static BoundedRedisValue MaxValue => new(BoundType.MaxValue); -} diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs index d6d88f692..8fdc13fa4 100644 --- a/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs +++ b/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Runtime.CompilerServices; using RESPite.Messages; using StackExchange.Redis; @@ -16,6 +17,155 @@ public static ref readonly SortedSetCommands SortedSets(this in RespContext cont public readonly struct SortedSetCommands(in RespContext context) { public readonly RespContext Context = context; // important: this is the only field + + public abstract class ZRangeRequest + { + [Flags] + internal enum ModeFlags + { + None = 0, + WithScores = 1, + ByLex = 2, + ByScore = 4, + } + + private void DemandType(Type type, string factory) + { + if (GetType() != type) Throw(factory); + static void Throw(string factory) => throw new InvalidOperationException($"The request for this operation must be created via {factory}"); + } + + /// + /// Indicates whether the data should be reversed. + /// + public bool Reverse { get; set; } + + /// + /// The offset into the sub-range for the matching elements. + /// + public long Offset { get; set; } + + /// + /// The number of elements to return. A netative value returns all elements from the . + /// + public long Count { get; set; } = -1; + + internal void Write( + scoped ReadOnlySpan command, + ref RespWriter writer, + RedisKey source, + RedisKey destination, + ModeFlags flags) + { + bool writeLimit = Offset != 0 || Count >= 0; + ReadOnlySpan by = default; + switch (flags & (ModeFlags.ByLex | ModeFlags.ByScore)) + { + case ModeFlags.ByLex: + DemandType(typeof(ZRangeRequestByLex), nameof(ByLex)); + break; + case ModeFlags.ByScore: + DemandType(typeof(ZRangeRequestByScore), nameof(ByScore)); + break; + default: + by = this switch + { + ZRangeRequestByLex => "$5\r\nBYLEX\r\n"u8, + ZRangeRequestByScore => "$7\r\nBYSCORE\r\n"u8, + _ => default, + }; + break; + } + + bool withScores = (flags & ModeFlags.WithScores) != 0; + int argCount = (by.IsEmpty ? 3 : 4) + + (withScores ? 1 : 0) + + (Reverse ? 1 : 0) + (writeLimit ? 3 : 0) + + (destination.IsNull ? 0 : 1); + writer.WriteCommand(command, argCount); + if (!destination.IsNull) writer.Write(destination); + writer.Write(source); + WriteStartStop(ref writer); + if (!by.IsEmpty) writer.WriteRaw(by); + if (Reverse) writer.WriteRaw("$3\r\nREV\r]\n"u8); + if (writeLimit) + { + writer.WriteRaw("$5\r\nLIMIT\r\n"u8); + writer.WriteBulkString(Offset); + writer.WriteBulkString(Count); + } + if (withScores) writer.WriteRaw("$10\r\nWITHSCORES\r\n"u8); + } + protected abstract void WriteStartStop(ref RespWriter writer); + private protected ZRangeRequest() { } + + public static ZRangeRequest ByRank(long start, long stop) + => new ZRangeRequestByRank(start, stop); + + public static ZRangeRequest ByLex(RedisValue start, RedisValue stop, Exclude exclude) + => new ZRangeRequestByLex(start, stop, exclude); + + public static ZRangeRequest ByScore(double start, double stop, Exclude exclude) + => new ZRangeRequestByScore(start, stop, exclude); + + private sealed class ZRangeRequestByRank(long start, long stop) : ZRangeRequest + { + protected override void WriteStartStop(ref RespWriter writer) + { + writer.WriteBulkString(start); + writer.WriteBulkString(stop); + } + } + private sealed class ZRangeRequestByLex(RedisValue start, RedisValue stop, Exclude exclude) : ZRangeRequest + { + protected override void WriteStartStop(ref RespWriter writer) + { + Write(ref writer, start, exclude, true); + Write(ref writer, stop, exclude, false); + } + } + + internal static void Write(ref RespWriter writer, in RedisValue value, Exclude exclude, bool isStart) + { + bool exclusive = (exclude & (isStart ? Exclude.Start : Exclude.Stop)) != 0; + if (value.IsNull) + { + writer.WriteRaw(isStart ? "$1\r\n-\r\n"u8 : "$1\r\n+\r\n"u8); + } + else + { + var len = value.GetByteCount(); + byte[]? lease = null; + var span = len < 128 ? stackalloc byte[128] : (lease = ArrayPool.Shared.Rent(len)); + span[0] = exclusive ? (byte)'(' : (byte)'['; + value.CopyTo(span.Slice(1)); // allow for the prefix + writer.WriteBulkString(span.Slice(0, len + 1)); + if (lease is not null) ArrayPool.Shared.Return(lease); + } + } + + private sealed class ZRangeRequestByScore(double start, double stop, Exclude exclude) : ZRangeRequest + { + protected override void WriteStartStop(ref RespWriter writer) + { + Write(ref writer, start, exclude, true); + Write(ref writer, stop, exclude, false); + } + } + + internal static void Write(ref RespWriter writer, double value, Exclude exclude, bool isStart) + { + bool exclusive = (exclude & (isStart ? Exclude.Start : Exclude.Stop)) != 0; + if (exclusive) + { + writer.WriteBulkStringExclusive(value); + } + else + { + writer.WriteBulkString(value); + } + } + } } internal static partial class SortedSetCommandsExtensions @@ -49,82 +199,135 @@ public static partial RespOperation ZAdd( SortedSetWhen when, SortedSetEntry[] values); - private sealed class ZAddFormatter : - IRespFormatter<(RedisKey Key, SortedSetWhen When, RedisValue Member, double Score)>, - IRespFormatter<(RedisKey Key, SortedSetWhen When, SortedSetEntry[] Values)> - { - private ZAddFormatter() { } - public static readonly ZAddFormatter Instance = new(); + [RespCommand] + public static partial RespOperation ZCard(this in SortedSetCommands context, RedisKey key); - public void Format( - scoped ReadOnlySpan command, - ref RespWriter writer, - in (RedisKey Key, SortedSetWhen When, RedisValue Member, double Score) request) + internal static RespOperation ZCardOrCount( + this in SortedSetCommands context, + RedisKey key, + double min, + double max, + Exclude exclude) + { + if (double.IsNegativeInfinity(min) && double.IsPositiveInfinity(max)) { - var argCount = 3 + GetWhenFlagCount(request.When); - writer.WriteCommand(command, argCount); - writer.Write(request.Key); - WriteWhenFlags(ref writer, request.When); - writer.WriteBulkString(request.Score); - writer.Write(request.Member); + return context.ZCard(key); } - public void Format( - scoped ReadOnlySpan command, - ref RespWriter writer, - in (RedisKey Key, SortedSetWhen When, SortedSetEntry[] Values) request) + return context.ZCount(key, min, max, exclude); + } + + internal static RespOperation Combine( + this in SortedSetCommands context, + SetOperation operation, + RedisKey[] keys, + double[]? weights = null, + Aggregate? aggregate = null) => + operation switch { - var argCount = 1 + GetWhenFlagCount(request.When) + (request.Values.Length * 2); - writer.WriteCommand(command, argCount); - writer.Write(request.Key); - WriteWhenFlags(ref writer, request.When); - foreach (var entry in request.Values) - { - writer.WriteBulkString(entry.Score); - writer.Write(entry.Element); - } - } + SetOperation.Difference => context.ZDiff(keys), + SetOperation.Intersect => context.ZInter(keys, weights, aggregate), + SetOperation.Union => context.ZUnion(keys, weights, aggregate), + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; - private static int GetWhenFlagCount(SortedSetWhen when) + internal static RespOperation Combine( + this in SortedSetCommands context, + SetOperation operation, + RedisKey first, + RedisKey second, + Aggregate? aggregate = null) => + operation switch { - when &= SortedSetWhen.NotExists | SortedSetWhen.Exists | SortedSetWhen.GreaterThan | SortedSetWhen.LessThan; - return (int)when.CountBits(); - } + SetOperation.Difference => context.ZDiff(first, second), + SetOperation.Intersect => context.ZInter(first, second, aggregate), + SetOperation.Union => context.ZUnion(first, second, aggregate), + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; - private static void WriteWhenFlags(ref RespWriter writer, SortedSetWhen when) + internal static RespOperation CombineAndStore( + this in SortedSetCommands context, + SetOperation operation, + RedisKey destination, + RedisKey[] keys, + double[]? weights = null, + Aggregate? aggregate = null) => + operation switch { - if ((when & SortedSetWhen.NotExists) != 0) - writer.WriteBulkString("NX"u8); - if ((when & SortedSetWhen.Exists) != 0) - writer.WriteBulkString("XX"u8); - if ((when & SortedSetWhen.GreaterThan) != 0) - writer.WriteBulkString("GT"u8); - if ((when & SortedSetWhen.LessThan) != 0) - writer.WriteBulkString("LT"u8); - } - } + SetOperation.Difference => context.ZDiffStore(destination, keys), + SetOperation.Intersect => context.ZInterStore(destination, keys, weights, aggregate), + SetOperation.Union => context.ZUnionStore(destination, keys, weights, aggregate), + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; - internal static RespOperation ZCardOrCount( + internal static RespOperation CombineAndStore( + this in SortedSetCommands context, + SetOperation operation, + RedisKey destination, + RedisKey first, + RedisKey second, + Aggregate? aggregate = null) => + operation switch + { + SetOperation.Difference => context.ZDiffStore(destination, first, second), + SetOperation.Intersect => context.ZInterStore(destination, first, second, aggregate), + SetOperation.Union => context.ZUnionStore(destination, first, second, aggregate), + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; + + internal static RespOperation CombineWithScores( + this in SortedSetCommands context, + SetOperation operation, + RedisKey[] keys, + double[]? weights = null, + Aggregate? aggregate = null) => + operation switch + { + SetOperation.Difference => context.ZDiffWithScores(keys), + SetOperation.Intersect => context.ZInterWithScores(keys, weights, aggregate), + SetOperation.Union => context.ZUnionWithScores(keys, weights, aggregate), + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; + + internal static RespOperation CombineWithScores( + this in SortedSetCommands context, + SetOperation operation, + RedisKey first, + RedisKey second, + Aggregate? aggregate = null) => + operation switch + { + SetOperation.Difference => context.ZDiffWithScores(first, second), + SetOperation.Intersect => context.ZInterWithScores(first, second, aggregate), + SetOperation.Union => context.ZUnionWithScores(first, second, aggregate), + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; + + [RespCommand(Formatter = "ZCountFormatter.Instance")] + public static partial RespOperation ZCount( this in SortedSetCommands context, RedisKey key, double min, double max, - Exclude exclude) + Exclude exclude = Exclude.None); + + private sealed class ZCountFormatter : IRespFormatter<(RedisKey Key, double Min, double Max, Exclude Exclude)> { - if (double.IsNegativeInfinity(min) && double.IsPositiveInfinity(max)) + private ZCountFormatter() { } + public static readonly ZCountFormatter Instance = new(); + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, double Min, double Max, Exclude Exclude) request) { - return context.ZCard(key); + writer.WriteCommand(command, 3); + writer.Write(request.Key); + SortedSetCommands.ZRangeRequest.Write(ref writer, request.Min, request.Exclude, true); + SortedSetCommands.ZRangeRequest.Write(ref writer, request.Max, request.Exclude, false); } - - return context.ZCount(key, exclude.Start(min), exclude.Stop(max)); } - [RespCommand] - public static partial RespOperation ZCard(this in SortedSetCommands context, RedisKey key); - - [RespCommand] - public static partial RespOperation ZCount(this in SortedSetCommands context, RedisKey key, BoundedDouble min, BoundedDouble max); - [RespCommand] public static partial RespOperation ZDiff( this in SortedSetCommands context, @@ -136,17 +339,6 @@ public static partial RespOperation ZDiff( RedisKey first, RedisKey second); - [RespCommand(nameof(ZDiff))] - public static partial RespOperation ZDiffWithScores( - this in SortedSetCommands context, - [RespSuffix("WITHSCORES")] RedisKey[] keys); - - [RespCommand(nameof(ZDiff))] - public static partial RespOperation ZDiffWithScores( - this in SortedSetCommands context, - RedisKey first, - [RespSuffix("WITHSCORES")] RedisKey second); - [RespCommand] public static partial RespOperation ZDiffStore( this in SortedSetCommands context, @@ -160,6 +352,17 @@ public static partial RespOperation ZDiffStore( RedisKey first, RedisKey second); + [RespCommand(nameof(ZDiff))] + public static partial RespOperation ZDiffWithScores( + this in SortedSetCommands context, + [RespSuffix("WITHSCORES")] RedisKey[] keys); + + [RespCommand(nameof(ZDiff))] + public static partial RespOperation ZDiffWithScores( + this in SortedSetCommands context, + RedisKey first, + [RespSuffix("WITHSCORES")] RedisKey second); + [RespCommand] public static partial RespOperation ZIncrBy( this in SortedSetCommands context, @@ -194,6 +397,22 @@ public static partial RespOperation ZInterCard( RedisKey second, [RespPrefix("LIMIT"), RespIgnore(0)] long limit = 0); + [RespCommand] + public static partial RespOperation ZInterStore( + this in SortedSetCommands context, + RedisKey destination, + RedisKey[] keys, + [RespPrefix("WEIGHTS")] double[]? weights = null, + [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); + + [RespCommand] + public static partial RespOperation ZInterStore( + this in SortedSetCommands context, + RedisKey destination, + RedisKey first, + RedisKey second, + [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); + [RespCommand(nameof(ZInter))] public static partial RespOperation ZInterWithScores( this in SortedSetCommands context, @@ -208,38 +427,66 @@ public static partial RespOperation ZInterWithScores( [RespSuffix("WITHSCORES")] RedisKey second, [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); - [RespCommand] - public static partial RespOperation ZInterStore( + [RespCommand(Formatter = "ZLexCountFormatter.Instance")] + public static partial RespOperation ZLexCount( this in SortedSetCommands context, - RedisKey destination, - RedisKey[] keys, - [RespPrefix("WEIGHTS")] double[]? weights = null, - [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); + RedisKey key, + RedisValue min, + RedisValue max, + Exclude exclude = Exclude.None); - [RespCommand] - public static partial RespOperation ZInterStore( + private sealed class ZLexCountFormatter : IRespFormatter<(RedisKey Key, RedisValue Min, RedisValue Max, Exclude Exclude)> + { + private ZLexCountFormatter() { } + public static readonly ZLexCountFormatter Instance = new(); + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, RedisValue Min, RedisValue Max, Exclude Exclude) request) + { + writer.WriteCommand(command, 3); + writer.Write(request.Key); + SortedSetCommands.ZRangeRequest.Write(ref writer, request.Min, request.Exclude, true); + SortedSetCommands.ZRangeRequest.Write(ref writer, request.Max, request.Exclude, false); + } + } + + [RespCommand(nameof(ZMPop))] + private static partial RespOperation ZMPopMax( this in SortedSetCommands context, - RedisKey destination, - RedisKey first, - RedisKey second, - [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); + [RespPrefix, RespSuffix("MAX")] RedisKey[] keys, + [RespIgnore(1), RespPrefix("COUNT")] long count); - [RespCommand] - public static partial RespOperation ZLexCount( + [RespCommand(nameof(ZMPop))] + private static partial RespOperation ZMPopMin( + this in SortedSetCommands context, + [RespPrefix, RespSuffix("MIN")] RedisKey[] keys, + [RespIgnore(1), RespPrefix("COUNT")] long count); + + public static RespOperation ZMPop( + this in SortedSetCommands context, + RedisKey[] keys, + Order order = Order.Ascending, + long count = 1) + => order == Order.Ascending ? context.ZMPopMin(keys, count) : context.ZMPopMax(keys, count); + + internal static RespOperation ZPop( this in SortedSetCommands context, RedisKey key, - BoundedRedisValue min, - BoundedRedisValue max); + Order order) => + order == Order.Ascending + ? context.ZPopMin(key) + : context.ZPopMax(key); - internal static RespOperation ZLexCount( + internal static RespOperation ZPop( this in SortedSetCommands context, RedisKey key, - RedisValue min, - RedisValue max, - Exclude exclude) => context.ZLexCount( - key, - min.IsNull ? BoundedRedisValue.MinValue : exclude.StartLex(min), - max.IsNull ? BoundedRedisValue.MaxValue : exclude.StopLex(max)); + long count, + Order order) => + order == Order.Ascending + ? context.ZPopMin(key, count) + : context.ZPopMax(key, count); [RespCommand] public static partial RespOperation ZPopMax(this in SortedSetCommands context, RedisKey key); @@ -274,186 +521,136 @@ public static partial RespOperation ZRandMemberWithScores( RedisKey key, [RespSuffix("WITHSCORES")] long count); - [RespCommand(Formatter = "ZRangeFormatter.NoScores")] - public static partial RespOperation ZRange( + private sealed class ZRangeFormatter : IRespFormatter<(RedisKey Key, SortedSetCommands.ZRangeRequest Request)>, + IRespFormatter<(RedisKey Destination, RedisKey Source, SortedSetCommands.ZRangeRequest Request)> + { + private readonly SortedSetCommands.ZRangeRequest.ModeFlags _flags; + private ZRangeFormatter(SortedSetCommands.ZRangeRequest.ModeFlags flags) => _flags = flags; + public static readonly ZRangeFormatter NoScores = new(SortedSetCommands.ZRangeRequest.ModeFlags.None); + public static readonly ZRangeFormatter WithScores = new(SortedSetCommands.ZRangeRequest.ModeFlags.WithScores); + public static readonly ZRangeFormatter ByLexNoScores = new(SortedSetCommands.ZRangeRequest.ModeFlags.ByLex); + public static readonly ZRangeFormatter ByLexWithScores = new(SortedSetCommands.ZRangeRequest.ModeFlags.WithScores | SortedSetCommands.ZRangeRequest.ModeFlags.ByLex); + public static readonly ZRangeFormatter ByScoreNoScores = new(SortedSetCommands.ZRangeRequest.ModeFlags.ByScore); + public static readonly ZRangeFormatter ByScoreWithScores = new(SortedSetCommands.ZRangeRequest.ModeFlags.WithScores | SortedSetCommands.ZRangeRequest.ModeFlags.ByScore); + + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, SortedSetCommands.ZRangeRequest Request) request) + => request.Request.Write(command, ref writer, RedisKey.Null, request.Key, _flags); + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Destination, RedisKey Source, SortedSetCommands.ZRangeRequest Request) request) + => request.Request.Write(command, ref writer, request.Destination, request.Source, _flags); + } + + [RespCommand] // by rank + private static partial RespOperation ZRange( this in SortedSetCommands context, RedisKey key, - long start, - long stop, - Order order = Order.Ascending, - long offset = 0, - long count = long.MaxValue); + long min, + long max); - [RespCommand(Formatter = "ZRangeFormatter.WithScores")] - public static partial RespOperation ZRangeWithScores( + [RespCommand(Formatter = "ZRangeFormatter.NoScores")] // flexible + private static partial RespOperation ZRange( this in SortedSetCommands context, RedisKey key, - long start, - long stop, - Order order = Order.Ascending, - long offset = 0, - long count = long.MaxValue); + SortedSetCommands.ZRangeRequest request); - [RespCommand(Formatter = "ZRangeFormatter.NoScores")] // by lex - internal static partial RespOperation ZRange( + internal static RespOperation ZRange( this in SortedSetCommands context, RedisKey key, - BoundedRedisValue start, - BoundedRedisValue stop, - Order order = Order.Ascending, - long offset = 0, - long count = long.MaxValue); + long min, + long max, + Order order) => order == Order.Ascending ? context.ZRange(key, min, max) : context.ZRevRange(key, max, min); - [RespCommand(Formatter = "ZRangeFormatter.WithScores")] // by lex - internal static partial RespOperation ZRangeWithScores( + [RespCommand(nameof(ZRange))] // by rank, with scores + private static partial RespOperation ZRangeWithScores( this in SortedSetCommands context, RedisKey key, - BoundedRedisValue start, - BoundedRedisValue stop, - Order order = Order.Ascending, - long offset = 0, - long count = long.MaxValue); + long min, + [RespSuffix("WITHSCORES")] long max); - [RespCommand(Formatter = "ZRangeFormatter.NoScores")] // by score - internal static partial RespOperation ZRange( + [RespCommand(nameof(ZRange), Formatter = "ZRangeFormatter.WithScores")] // flexible, with scores + private static partial RespOperation ZRangeWithScores( this in SortedSetCommands context, RedisKey key, - BoundedDouble start, - BoundedDouble stop, - Order order = Order.Ascending, - long offset = 0, - long count = long.MaxValue); + SortedSetCommands.ZRangeRequest request); - [RespCommand(Formatter = "ZRangeFormatter.WithScores")] // byscore - internal static partial RespOperation ZRangeWithScores( + [RespCommand(Formatter = "ZRangeFormatter.ByLexNoScores")] + public static partial RespOperation ZRangeByLex( this in SortedSetCommands context, RedisKey key, - BoundedDouble start, - BoundedDouble stop, - Order order = Order.Ascending, - long offset = 0, - long count = long.MaxValue); - - private sealed class ZRangeFormatter : - IRespFormatter<(RedisKey Key, long Start, long Stop, Order Order, long Offset, long Count)>, - IRespFormatter<(RedisKey Key, BoundedDouble Start, BoundedDouble Stop, Order Order, long Offset, long Count)>, - IRespFormatter<(RedisKey Key, BoundedRedisValue Start, BoundedRedisValue Stop, Order Order, long Offset, long Count)> - { - private readonly bool _withScores; - private ZRangeFormatter(bool withScores) => _withScores = withScores; - public static readonly ZRangeFormatter WithScores = new(true), NoScores = new(false); - public void Format( - scoped ReadOnlySpan command, - ref RespWriter writer, - in (RedisKey Key, long Start, long Stop, Order Order, long Offset, long Count) request) - { - bool writeLimit = request.Offset != 0 || request.Count != long.MaxValue; - var argCount = 3 + (writeLimit ? 3 : 0) + (_withScores ? 1 : 0) + (request.Order == Order.Descending ? 1 : 0); - writer.WriteCommand(command, argCount); - writer.Write(request.Key); - writer.WriteBulkString(request.Start); - writer.WriteBulkString(request.Stop); - if (request.Order == Order.Descending) - { - writer.WriteRaw("$3\r\nREV\r\n"u8); - } - if (writeLimit) - { - writer.WriteRaw("$5\r\nLIMIT\r\n"u8); - writer.WriteBulkString(request.Offset); - writer.WriteBulkString(request.Count); - } - if (_withScores) - { - writer.WriteRaw("$10\r\nWITHSCORES\r\n"u8); - } - } - - public void Format( - scoped ReadOnlySpan command, - ref RespWriter writer, - in (RedisKey Key, BoundedDouble Start, BoundedDouble Stop, Order Order, long Offset, long Count) request) - { - bool writeLimit = request.Offset != 0 || request.Count != long.MaxValue; - var argCount = 4 + (writeLimit ? 3 : 0) + (_withScores ? 1 : 0) + (request.Order == Order.Descending ? 1 : 0); - writer.WriteCommand(command, argCount); - writer.Write(request.Key); - writer.WriteBulkString(request.Start); - writer.WriteBulkString(request.Stop); - writer.WriteRaw("$7\r\nBYSCORE\r\n"u8); - if (request.Order == Order.Descending) - { - writer.WriteRaw("$3\r\nREV\r\n"u8); - } - if (writeLimit) - { - writer.WriteRaw("$5\r\nLIMIT\r\n"u8); - writer.WriteBulkString(request.Offset); - writer.WriteBulkString(request.Count); - } - if (_withScores) - { - writer.WriteRaw("$10\r\nWITHSCORES\r\n"u8); - } - } - - public void Format( - scoped ReadOnlySpan command, - ref RespWriter writer, - in (RedisKey Key, BoundedRedisValue Start, BoundedRedisValue Stop, Order Order, long Offset, long Count) request) - { - bool writeLimit = request.Offset != 0 || request.Count != long.MaxValue; - var argCount = 4 + (writeLimit ? 3 : 0) + (_withScores ? 1 : 0) + (request.Order == Order.Descending ? 1 : 0); - writer.WriteCommand(command, argCount); - writer.Write(request.Key); - writer.WriteBulkString(request.Start); - writer.WriteBulkString(request.Stop); - writer.WriteRaw("$5\r\nBYLEX\r\n"u8); - if (request.Order == Order.Descending) - { - writer.WriteRaw("$3\r\nREV\r\n"u8); - } - if (writeLimit) - { - writer.WriteRaw("$5\r\nLIMIT\r\n"u8); - writer.WriteBulkString(request.Offset); - writer.WriteBulkString(request.Count); - } - if (_withScores) - { - writer.WriteRaw("$10\r\nWITHSCORES\r\n"u8); - } - } - } + SortedSetCommands.ZRangeRequest request); - [RespCommand] - public static partial RespOperation ZRangeByLex( + [RespCommand(nameof(ZRangeByLex), Formatter = "ZRangeFormatter.ByLexWithScores")] + public static partial RespOperation ZRangeByLexWithScores( this in SortedSetCommands context, RedisKey key, - BoundedRedisValue min, - BoundedRedisValue max); + SortedSetCommands.ZRangeRequest request); - [RespCommand] + [RespCommand(Formatter = "ZRangeFormatter.ByScoreNoScores")] public static partial RespOperation ZRangeByScore( this in SortedSetCommands context, RedisKey key, - BoundedDouble min, - BoundedDouble max); + SortedSetCommands.ZRangeRequest request); - [RespCommand(nameof(ZRangeByScore))] - public static partial RespOperation ZRangeByScoreWithScores( + [RespCommand(nameof(ZRangeByScore), Formatter = "ZRangeFormatter.ByScoreWithScores")] + public static partial RespOperation ZRangeByScoreWithScores( this in SortedSetCommands context, RedisKey key, - BoundedDouble min, - [RespSuffix("WITHSCORES")] BoundedDouble max); + SortedSetCommands.ZRangeRequest request); - [RespCommand] + [RespCommand] // by rank public static partial RespOperation ZRangeStore( this in SortedSetCommands context, RedisKey destination, RedisKey source, - long start, - long stop); + long min, + long max); + + [RespCommand(Formatter = "ZRangeFormatter.NoScores")] // flexible + public static partial RespOperation ZRangeStore( + this in SortedSetCommands context, + RedisKey destination, + RedisKey source, + SortedSetCommands.ZRangeRequest request); + + internal static RespOperation ZRangeStore( + this in SortedSetCommands context, + RedisKey sourceKey, + RedisKey destinationKey, + RedisValue start, + RedisValue stop, + SortedSetOrder sortedSetOrder, + Exclude exclude, + Order order, + long skip, + long? take) + { + SortedSetCommands.ZRangeRequest request = + sortedSetOrder switch + { + SortedSetOrder.ByRank => SortedSetCommands.ZRangeRequest.ByRank((long)start, (long)stop), + SortedSetOrder.ByLex => SortedSetCommands.ZRangeRequest.ByLex(start, stop, exclude), + SortedSetOrder.ByScore => SortedSetCommands.ZRangeRequest.ByScore((double)start, (double)stop, exclude), + _ => throw new ArgumentOutOfRangeException(nameof(sortedSetOrder)), + }; + request.Offset = skip; + if (take is not null) request.Count = take.Value; + request.Reverse = order == Order.Descending; + return context.ZRangeStore(destinationKey, sourceKey, request); + } + + internal static RespOperation ZRank( + this in SortedSetCommands context, + RedisKey key, + RedisValue member, + Order order) => + order == Order.Ascending + ? context.ZRank(key, member) + : context.ZRevRank(key, member); [RespCommand] public static partial RespOperation ZRank( @@ -473,12 +670,11 @@ public static partial RespOperation ZRem( RedisKey key, RedisValue[] members); - [RespCommand] + [RespCommand(Formatter = "ZRangeFormatter.ByLexNoScores")] public static partial RespOperation ZRemRangeByLex( this in SortedSetCommands context, RedisKey key, - BoundedRedisValue min, - BoundedRedisValue max); + SortedSetCommands.ZRangeRequest request); [RespCommand] public static partial RespOperation ZRemRangeByRank( @@ -487,12 +683,11 @@ public static partial RespOperation ZRemRangeByRank( long start, long stop); - [RespCommand] + [RespCommand(Formatter = "ZRangeFormatter.ByScoreNoScores")] public static partial RespOperation ZRemRangeByScore( this in SortedSetCommands context, RedisKey key, - BoundedDouble min, - BoundedDouble max); + SortedSetCommands.ZRangeRequest request); [RespCommand] public static partial RespOperation ZRevRange( @@ -508,26 +703,29 @@ public static partial RespOperation ZRevRangeWithScores( long start, [RespSuffix("WITHSCORES")] long stop); - [RespCommand] + [RespCommand(Formatter = "ZRangeFormatter.ByLexNoScores")] public static partial RespOperation ZRevRangeByLex( this in SortedSetCommands context, RedisKey key, - BoundedRedisValue max, - BoundedRedisValue min); + SortedSetCommands.ZRangeRequest request); - [RespCommand] + [RespCommand(nameof(ZRevRangeByLex), Formatter = "ZRangeFormatter.ByLexWithScores")] + public static partial RespOperation ZRevRangeByLexWithScores( + this in SortedSetCommands context, + RedisKey key, + SortedSetCommands.ZRangeRequest request); + + [RespCommand(Formatter = "ZRangeFormatter.ByScoreNoScores")] public static partial RespOperation ZRevRangeByScore( this in SortedSetCommands context, RedisKey key, - BoundedDouble max, - BoundedDouble min); + SortedSetCommands.ZRangeRequest request); - [RespCommand(nameof(ZRevRangeByScore))] + [RespCommand(Formatter = "ZRangeFormatter.ByScoreWithScores")] public static partial RespOperation ZRevRangeByScoreWithScores( this in SortedSetCommands context, RedisKey key, - BoundedDouble max, - [RespSuffix("WITHSCORES")] BoundedDouble min); + SortedSetCommands.ZRangeRequest request); [RespCommand] public static partial RespOperation ZRevRank( @@ -561,20 +759,6 @@ public static partial RespOperation ZUnion( RedisKey second, [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); - [RespCommand(nameof(ZUnion))] - public static partial RespOperation ZUnionWithScores( - this in SortedSetCommands context, - [RespSuffix("WITHSCORES")] RedisKey[] keys, - [RespPrefix("WEIGHTS")] double[]? weights = null, - [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); - - [RespCommand(nameof(ZUnion))] - public static partial RespOperation ZUnionWithScores( - this in SortedSetCommands context, - RedisKey first, - [RespSuffix("WITHSCORES")] RedisKey second, - [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); - [RespCommand] public static partial RespOperation ZUnionStore( this in SortedSetCommands context, @@ -591,134 +775,72 @@ public static partial RespOperation ZUnionStore( RedisKey second, [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); - internal static RespOperation Combine( + [RespCommand(nameof(ZUnion))] + public static partial RespOperation ZUnionWithScores( this in SortedSetCommands context, - SetOperation operation, - RedisKey[] keys, - double[]? weights = null, - Aggregate? aggregate = null) => - operation switch - { - SetOperation.Difference => context.ZDiff(keys), - SetOperation.Intersect => context.ZInter(keys, weights, aggregate), - SetOperation.Union => context.ZUnion(keys, weights, aggregate), - _ => throw new ArgumentOutOfRangeException(nameof(operation)), - }; + [RespSuffix("WITHSCORES")] RedisKey[] keys, + [RespPrefix("WEIGHTS")] double[]? weights = null, + [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); - internal static RespOperation CombineWithScores( + [RespCommand(nameof(ZUnion))] + public static partial RespOperation ZUnionWithScores( this in SortedSetCommands context, - SetOperation operation, - RedisKey[] keys, - double[]? weights = null, - Aggregate? aggregate = null) => - operation switch - { - SetOperation.Difference => context.ZDiffWithScores(keys), - SetOperation.Intersect => context.ZInterWithScores(keys, weights, aggregate), - SetOperation.Union => context.ZUnionWithScores(keys, weights, aggregate), - _ => throw new ArgumentOutOfRangeException(nameof(operation)), - }; + RedisKey first, + [RespSuffix("WITHSCORES")] RedisKey second, + [RespPrefix("AGGREGATE")] Aggregate? aggregate = null); - internal static RespOperation CombineAndStore( - this in SortedSetCommands context, - SetOperation operation, - RedisKey destination, - RedisKey[] keys, - double[]? weights = null, - Aggregate? aggregate = null) => - operation switch - { - SetOperation.Difference => context.ZDiffStore(destination, keys), - SetOperation.Intersect => context.ZInterStore(destination, keys, weights, aggregate), - SetOperation.Union => context.ZUnionStore(destination, keys, weights, aggregate), - _ => throw new ArgumentOutOfRangeException(nameof(operation)), - }; + private sealed class ZAddFormatter : + IRespFormatter<(RedisKey Key, SortedSetWhen When, RedisValue Member, double Score)>, + IRespFormatter<(RedisKey Key, SortedSetWhen When, SortedSetEntry[] Values)> + { + private ZAddFormatter() { } + public static readonly ZAddFormatter Instance = new(); - internal static RespOperation Combine( - this in SortedSetCommands context, - SetOperation operation, - RedisKey first, - RedisKey second, - Aggregate? aggregate = null) => - operation switch + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, SortedSetWhen When, RedisValue Member, double Score) request) { - SetOperation.Difference => context.ZDiff(first, second), - SetOperation.Intersect => context.ZInter(first, second, aggregate), - SetOperation.Union => context.ZUnion(first, second, aggregate), - _ => throw new ArgumentOutOfRangeException(nameof(operation)), - }; + var argCount = 3 + GetWhenFlagCount(request.When); + writer.WriteCommand(command, argCount); + writer.Write(request.Key); + WriteWhenFlags(ref writer, request.When); + writer.WriteBulkString(request.Score); + writer.Write(request.Member); + } - internal static RespOperation CombineWithScores( - this in SortedSetCommands context, - SetOperation operation, - RedisKey first, - RedisKey second, - Aggregate? aggregate = null) => - operation switch + public void Format( + scoped ReadOnlySpan command, + ref RespWriter writer, + in (RedisKey Key, SortedSetWhen When, SortedSetEntry[] Values) request) { - SetOperation.Difference => context.ZDiffWithScores(first, second), - SetOperation.Intersect => context.ZInterWithScores(first, second, aggregate), - SetOperation.Union => context.ZUnionWithScores(first, second, aggregate), - _ => throw new ArgumentOutOfRangeException(nameof(operation)), - }; + var argCount = 1 + GetWhenFlagCount(request.When) + (request.Values.Length * 2); + writer.WriteCommand(command, argCount); + writer.Write(request.Key); + WriteWhenFlags(ref writer, request.When); + foreach (var entry in request.Values) + { + writer.WriteBulkString(entry.Score); + writer.Write(entry.Element); + } + } - internal static RespOperation CombineAndStore( - this in SortedSetCommands context, - SetOperation operation, - RedisKey destination, - RedisKey first, - RedisKey second, - Aggregate? aggregate = null) => - operation switch + private static int GetWhenFlagCount(SortedSetWhen when) { - SetOperation.Difference => context.ZDiffStore(destination, first, second), - SetOperation.Intersect => context.ZInterStore(destination, first, second, aggregate), - SetOperation.Union => context.ZUnionStore(destination, first, second, aggregate), - _ => throw new ArgumentOutOfRangeException(nameof(operation)), - }; - - public static RespOperation ZMPop( - this in SortedSetCommands context, - RedisKey[] keys, - Order order = Order.Ascending, - long count = 1) - => order == Order.Ascending ? context.ZMPopMin(keys, count) : context.ZMPopMax(keys, count); - - [RespCommand(nameof(ZMPop))] - private static partial RespOperation ZMPopMin( - this in SortedSetCommands context, - [RespPrefix, RespSuffix("MIN")] RedisKey[] keys, - [RespIgnore(1), RespPrefix("COUNT")] long count); - - [RespCommand(nameof(ZMPop))] - private static partial RespOperation ZMPopMax( - this in SortedSetCommands context, - [RespPrefix, RespSuffix("MAX")] RedisKey[] keys, - [RespIgnore(1), RespPrefix("COUNT")] long count); - - internal static RespOperation ZPop( - this in SortedSetCommands context, - RedisKey key, - Order order) => - order == Order.Ascending - ? context.ZPopMin(key) - : context.ZPopMax(key); - - internal static RespOperation ZPop( - this in SortedSetCommands context, - RedisKey key, - long count, - Order order) => - order == Order.Ascending - ? context.ZPopMin(key, count) - : context.ZPopMax(key, count); + when &= SortedSetWhen.NotExists | SortedSetWhen.Exists | SortedSetWhen.GreaterThan | SortedSetWhen.LessThan; + return (int)when.CountBits(); + } - internal static RespOperation ZRank( - this in SortedSetCommands context, - RedisKey key, - RedisValue member, - Order order) => - order == Order.Ascending - ? context.ZRank(key, member) - : context.ZRevRank(key, member); + private static void WriteWhenFlags(ref RespWriter writer, SortedSetWhen when) + { + if ((when & SortedSetWhen.NotExists) != 0) + writer.WriteBulkString("NX"u8); + if ((when & SortedSetWhen.Exists) != 0) + writer.WriteBulkString("XX"u8); + if ((when & SortedSetWhen.GreaterThan) != 0) + writer.WriteBulkString("GT"u8); + if ((when & SortedSetWhen.LessThan) != 0) + writer.WriteBulkString("LT"u8); + } + } } diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs index e331944c3..45e343298 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs @@ -312,7 +312,7 @@ public RedisValue[] SortedSetRangeByRank( long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRange(key, start, stop, order).Wait(SyncTimeout); public long SortedSetRangeAndStore( RedisKey sourceKey, @@ -325,7 +325,7 @@ public long SortedSetRangeAndStore( long skip = 0, long? take = null, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRangeStore(sourceKey, destinationKey, start, stop, sortedSetOrder, exclude, order, skip, take).Wait(SyncTimeout); public Task SortedSetRangeByRankAsync( RedisKey key, @@ -333,7 +333,7 @@ public Task SortedSetRangeByRankAsync( long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRange(key, start, stop, order).AsTask(); public Task SortedSetRangeAndStoreAsync( RedisKey sourceKey, @@ -346,7 +346,7 @@ public Task SortedSetRangeAndStoreAsync( long skip = 0, long? take = null, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRangeStore(sourceKey, destinationKey, start, stop, sortedSetOrder, exclude, order, skip, take).AsTask(); public SortedSetEntry[] SortedSetRangeByRankWithScores( RedisKey key, diff --git a/src/RESPite.StackExchange.Redis/RespContextExtensions.cs b/src/RESPite.StackExchange.Redis/RespContextExtensions.cs index aaea5066e..b03b1019f 100644 --- a/src/RESPite.StackExchange.Redis/RespContextExtensions.cs +++ b/src/RESPite.StackExchange.Redis/RespContextExtensions.cs @@ -17,14 +17,4 @@ internal static RespContext With(this in RespContext context, int db, CommandFla | RespContext.RespContextFlags.NoScriptCache; return context.With(db, (RespContext.RespContextFlags)flags, FlagMask); } - - internal static BoundedDouble Start(this Exclude exclude, double value) - => new(value, (exclude & Exclude.Start) != 0); - internal static BoundedDouble Stop(this Exclude exclude, double value) - => new(value, (exclude & Exclude.Stop) != 0); - - internal static BoundedRedisValue StartLex(this Exclude exclude, RedisValue value) - => new(value, (exclude & Exclude.Start) != 0); - internal static BoundedRedisValue StopLex(this Exclude exclude, RedisValue value) - => new(value, (exclude & Exclude.Stop) != 0); } diff --git a/src/RESPite.StackExchange.Redis/RespFormatters.cs b/src/RESPite.StackExchange.Redis/RespFormatters.cs index a8d8055fa..3a4f497f5 100644 --- a/src/RESPite.StackExchange.Redis/RespFormatters.cs +++ b/src/RESPite.StackExchange.Redis/RespFormatters.cs @@ -176,26 +176,4 @@ public static void Write(this ref RespWriter writer, in RedisValue value) static void Throw(StorageType type) => throw new InvalidOperationException($"Unexpected {type} value."); } - - internal static void WriteBulkString(this ref RespWriter writer, in BoundedRedisValue value) - { - switch (value.Type) - { - case BoundedRedisValue.BoundType.MinValue: - writer.WriteRaw("$1\r\n-\r\n"u8); - break; - case BoundedRedisValue.BoundType.MaxValue: - writer.WriteRaw("$1\r\n+\r\n"u8); - break; - default: - var len = value.ValueRaw.GetByteCount(); - byte[]? lease = null; - var span = len < 128 ? stackalloc byte[128] : (lease = ArrayPool.Shared.Rent(len)); - span[0] = value.Inclusive ? (byte)'[' : (byte)'('; - value.ValueRaw.CopyTo(span.Slice(1)); // allow for the prefix - writer.WriteBulkString(span.Slice(0, len + 1)); - if (lease is not null) ArrayPool.Shared.Return(lease); - break; - } - } } diff --git a/src/RESPite/BoundedDouble.cs b/src/RESPite/BoundedDouble.cs deleted file mode 100644 index 896e1a476..000000000 --- a/src/RESPite/BoundedDouble.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace RESPite; - -public readonly struct BoundedDouble(double value, bool exclusive = false) : IEquatable -{ - public double Value { get; } = value; - public bool Inclusive { get; } = !exclusive; - public override string ToString() => Inclusive ? $"{Value}" : $"({Value}"; - public override int GetHashCode() => unchecked((Value.GetHashCode() * 397) ^ Inclusive.GetHashCode()); - - public override bool Equals(object? obj) => obj is BoundedDouble other && Equals(other); - bool IEquatable.Equals(BoundedDouble other) => Equals(other); - public bool Equals(in BoundedDouble other) => Value.Equals(other.Value) && Inclusive == other.Inclusive; - public static bool operator ==(BoundedDouble left, BoundedDouble right) => left.Equals(right); - public static bool operator !=(BoundedDouble left, BoundedDouble right) => !left.Equals(right); - public static implicit operator BoundedDouble(double value) => new(value); - - public static BoundedDouble MinValue => new(double.MinValue); - public static BoundedDouble MaxValue => new(double.MaxValue); -} diff --git a/src/RESPite/Messages/RespWriter.cs b/src/RESPite/Messages/RespWriter.cs index b4f38c8b9..88b1caa1e 100644 --- a/src/RESPite/Messages/RespWriter.cs +++ b/src/RESPite/Messages/RespWriter.cs @@ -360,21 +360,6 @@ public void WriteBulkString(in SimpleString value) /// public void WriteBulkString(bool value) => WriteBulkString(value ? 1 : 0); - /// - /// Write a bounded floating point as a bulk string. - /// - public void WriteBulkString(in BoundedDouble value) - { - if (value.Inclusive) - { - WriteBulkString(value.Value); - } - else - { - WriteBulkStringExclusive(value.Value); - } - } - /// /// Write a floating point as a bulk string. /// @@ -422,7 +407,7 @@ static void WriteKnownDoubleInclusive(ref RespWriter writer, double value) } } - private void WriteBulkStringExclusive(double value) + internal void WriteBulkStringExclusive(double value) { if (value == 0.0 | double.IsNaN(value) | double.IsInfinity(value)) { From c7ee9782bf2c389b5ee3d60714b5bfde1a998939 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 7 Oct 2025 14:55:12 +0100 Subject: [PATCH 107/108] everything except zscan --- .../RedisCommands.SortedSetCommands.cs | 39 +++++++++-- .../RespContextDatabase.SortedSet.cs | 64 ++++++++++++------- 2 files changed, 72 insertions(+), 31 deletions(-) diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs index 8fdc13fa4..30c3728bd 100644 --- a/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs +++ b/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs @@ -546,14 +546,14 @@ public void Format( } [RespCommand] // by rank - private static partial RespOperation ZRange( + public static partial RespOperation ZRange( this in SortedSetCommands context, RedisKey key, long min, long max); [RespCommand(Formatter = "ZRangeFormatter.NoScores")] // flexible - private static partial RespOperation ZRange( + public static partial RespOperation ZRange( this in SortedSetCommands context, RedisKey key, SortedSetCommands.ZRangeRequest request); @@ -566,14 +566,21 @@ internal static RespOperation ZRange( Order order) => order == Order.Ascending ? context.ZRange(key, min, max) : context.ZRevRange(key, max, min); [RespCommand(nameof(ZRange))] // by rank, with scores - private static partial RespOperation ZRangeWithScores( + public static partial RespOperation ZRangeWithScores( this in SortedSetCommands context, RedisKey key, long min, [RespSuffix("WITHSCORES")] long max); + internal static RespOperation ZRangeWithScores( + this in SortedSetCommands context, + RedisKey key, + long min, + long max, + Order order) => order == Order.Ascending ? context.ZRangeWithScores(key, min, max) : context.ZRevRangeWithScores(key, max, min); + [RespCommand(nameof(ZRange), Formatter = "ZRangeFormatter.WithScores")] // flexible, with scores - private static partial RespOperation ZRangeWithScores( + public static partial RespOperation ZRangeWithScores( this in SortedSetCommands context, RedisKey key, SortedSetCommands.ZRangeRequest request); @@ -584,8 +591,14 @@ public static partial RespOperation ZRangeByLex( RedisKey key, SortedSetCommands.ZRangeRequest request); + internal static RespOperation ZRangeByLex( + this in SortedSetCommands context, + RedisKey key, + SortedSetCommands.ZRangeRequest request, + Order order) => order == Order.Ascending ? context.ZRangeByLex(key, request) : context.ZRevRangeByLex(key, request); + [RespCommand(nameof(ZRangeByLex), Formatter = "ZRangeFormatter.ByLexWithScores")] - public static partial RespOperation ZRangeByLexWithScores( + public static partial RespOperation ZRangeByLexWithScores( this in SortedSetCommands context, RedisKey key, SortedSetCommands.ZRangeRequest request); @@ -596,12 +609,24 @@ public static partial RespOperation ZRangeByScore( RedisKey key, SortedSetCommands.ZRangeRequest request); + internal static RespOperation ZRangeByScore( + this in SortedSetCommands context, + RedisKey key, + SortedSetCommands.ZRangeRequest request, + Order order) => order == Order.Ascending ? context.ZRangeByScore(key, request) : context.ZRevRangeByScore(key, request); + [RespCommand(nameof(ZRangeByScore), Formatter = "ZRangeFormatter.ByScoreWithScores")] - public static partial RespOperation ZRangeByScoreWithScores( + public static partial RespOperation ZRangeByScoreWithScores( this in SortedSetCommands context, RedisKey key, SortedSetCommands.ZRangeRequest request); + internal static RespOperation ZRangeByScoreWithScores( + this in SortedSetCommands context, + RedisKey key, + SortedSetCommands.ZRangeRequest request, + Order order) => order == Order.Ascending ? context.ZRangeByScoreWithScores(key, request) : context.ZRevRangeByScoreWithScores(key, request); + [RespCommand] // by rank public static partial RespOperation ZRangeStore( this in SortedSetCommands context, @@ -659,7 +684,7 @@ internal static RespOperation ZRangeStore( RedisValue member); [RespCommand] - public static partial RespOperation ZRem( + public static partial RespOperation ZRem( this in SortedSetCommands context, RedisKey key, RedisValue member); diff --git a/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs b/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs index 45e343298..d4b870261 100644 --- a/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs +++ b/src/RESPite.StackExchange.Redis/RespContextDatabase.SortedSet.cs @@ -354,7 +354,7 @@ public SortedSetEntry[] SortedSetRangeByRankWithScores( long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRangeWithScores(key, start, stop, order).Wait(SyncTimeout); public Task SortedSetRangeByRankWithScoresAsync( RedisKey key, @@ -362,7 +362,7 @@ public Task SortedSetRangeByRankWithScoresAsync( long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRangeWithScores(key, start, stop, order).AsTask(); public RedisValue[] SortedSetRangeByScore( RedisKey key, @@ -373,7 +373,15 @@ public RedisValue[] SortedSetRangeByScore( long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRangeByScore(key, ByScore(start, stop, exclude, skip, take), order).Wait(SyncTimeout); + + private static SortedSetCommands.ZRangeRequest ByScore(double start, double stop, Exclude exclude, long skip, long take) + { + var req = SortedSetCommands.ZRangeRequest.ByScore(start, stop, exclude); + req.Offset = skip; + req.Count = take; + return req; + } public Task SortedSetRangeByScoreAsync( RedisKey key, @@ -384,7 +392,7 @@ public Task SortedSetRangeByScoreAsync( long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRangeByScore(key, ByScore(start, stop, exclude, skip, take), order).AsTask(); public SortedSetEntry[] SortedSetRangeByScoreWithScores( RedisKey key, @@ -395,7 +403,7 @@ public SortedSetEntry[] SortedSetRangeByScoreWithScores( long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRangeByScoreWithScores(key, ByScore(start, stop, exclude, skip, take), order).Wait(SyncTimeout); public Task SortedSetRangeByScoreWithScoresAsync( RedisKey key, @@ -406,7 +414,15 @@ public Task SortedSetRangeByScoreWithScoresAsync( long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRangeByScoreWithScores(key, ByScore(start, stop, exclude, skip, take), order).AsTask(); + + private static SortedSetCommands.ZRangeRequest ByLex(RedisValue start, RedisValue stop, Exclude exclude, long skip, long take) + { + var req = SortedSetCommands.ZRangeRequest.ByLex(start, stop, exclude); + req.Offset = skip; + req.Count = take; + return req; + } public RedisValue[] SortedSetRangeByValue( RedisKey key, @@ -416,7 +432,7 @@ public RedisValue[] SortedSetRangeByValue( long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRangeByLex(key, ByLex(min, max, exclude, skip, take)).Wait(SyncTimeout); public RedisValue[] SortedSetRangeByValue( RedisKey key, @@ -427,7 +443,7 @@ public RedisValue[] SortedSetRangeByValue( long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRangeByLex(key, ByLex(min, max, exclude, skip, take), order).Wait(SyncTimeout); public Task SortedSetRangeByValueAsync( RedisKey key, @@ -437,7 +453,7 @@ public Task SortedSetRangeByValueAsync( long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRangeByLex(key, ByLex(min, max, exclude, skip, take)).AsTask(); public Task SortedSetRangeByValueAsync( RedisKey key, @@ -448,36 +464,36 @@ public Task SortedSetRangeByValueAsync( long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRangeByLex(key, ByLex(min, max, exclude, skip, take), order).AsTask(); public bool SortedSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRem(key, member).Wait(SyncTimeout); public long SortedSetRemove(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRem(key, members).Wait(SyncTimeout); public Task SortedSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRem(key, member).AsTask(); public Task SortedSetRemoveAsync( RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRem(key, members).AsTask(); public long SortedSetRemoveRangeByRank( RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRemRangeByRank(key, start, stop).Wait(SyncTimeout); public Task SortedSetRemoveRangeByRankAsync( RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRemRangeByRank(key, start, stop).AsTask(); public long SortedSetRemoveRangeByScore( RedisKey key, @@ -485,7 +501,7 @@ public long SortedSetRemoveRangeByScore( double stop, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRemRangeByScore(key, SortedSetCommands.ZRangeRequest.ByScore(start, stop, exclude)).Wait(SyncTimeout); public Task SortedSetRemoveRangeByScoreAsync( RedisKey key, @@ -493,7 +509,7 @@ public Task SortedSetRemoveRangeByScoreAsync( double stop, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRemRangeByScore(key, SortedSetCommands.ZRangeRequest.ByScore(start, stop, exclude)).AsTask(); public long SortedSetRemoveRangeByValue( RedisKey key, @@ -501,7 +517,7 @@ public long SortedSetRemoveRangeByValue( RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRemRangeByScore(key, SortedSetCommands.ZRangeRequest.ByLex(min, max, exclude)).Wait(SyncTimeout); public Task SortedSetRemoveRangeByValueAsync( RedisKey key, @@ -509,7 +525,7 @@ public Task SortedSetRemoveRangeByValueAsync( RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZRemRangeByScore(key, SortedSetCommands.ZRangeRequest.ByLex(min, max, exclude)).AsTask(); public IEnumerable SortedSetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) @@ -554,14 +570,14 @@ public bool SortedSetUpdate( double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZAdd(key, when, member, score).Wait(SyncTimeout); public long SortedSetUpdate( RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZAdd(key, when, values).Wait(SyncTimeout); public Task SortedSetUpdateAsync( RedisKey key, @@ -569,12 +585,12 @@ public Task SortedSetUpdateAsync( double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZAdd(key, when, member, score).AsTask(); public Task SortedSetUpdateAsync( RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) - => throw new NotImplementedException(); + => Context(flags).SortedSets().ZAdd(key, when, values).AsTask(); } From ebdc3017fe6b71edce3320a64c904403398e81b9 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 8 Oct 2025 17:01:07 +0100 Subject: [PATCH 108/108] zscan --- .../RespCommandGenerator.cs | 23 ++++++---- .../RedisCommands.SortedSetCommands.cs | 8 ++++ .../RedisCommands.cs | 36 +++++++++++++++- .../RespParsers.ScanParsers.cs | 36 ++++++++++++++++ .../RespParsers.cs | 24 +++++++---- src/RESPite/Messages/RespReader.cs | 42 +++++++++++++++++++ src/RESPite/RespIgnoreAttribute.cs | 8 ++-- 7 files changed, 155 insertions(+), 22 deletions(-) create mode 100644 src/RESPite.StackExchange.Redis/RespParsers.ScanParsers.cs diff --git a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs index 0b7ee8875..3ebef397a 100644 --- a/eng/StackExchange.Redis.Build/RespCommandGenerator.cs +++ b/eng/StackExchange.Redis.Build/RespCommandGenerator.cs @@ -134,6 +134,8 @@ private static bool IsRESPite(ITypeSymbol? symbol, RESPite type) private enum SERedis { CommandFlags, + RedisValue, + RedisKey, } private static bool IsSERedis(ITypeSymbol? symbol, SERedis type) @@ -141,6 +143,8 @@ private static bool IsSERedis(ITypeSymbol? symbol, SERedis type) static string NameOf(SERedis type) => type switch { SERedis.CommandFlags => nameof(SERedis.CommandFlags), + SERedis.RedisValue => nameof(SERedis.RedisValue), + SERedis.RedisKey => nameof(SERedis.RedisKey), _ => type.ToString(), }; @@ -481,14 +485,15 @@ void AddLiteral(string token, LiteralFlags literalFlags) var val = attrib.ConstructorArguments[0].Value; var expr = val switch { - string s => CodeLiteral(s), - bool b => b ? "true" : "false", + null when IsSERedis(param.Type, SERedis.RedisValue) | IsSERedis(param.Type, SERedis.RedisKey) => ".IsNull is false", + string s => " != " + CodeLiteral(s), + bool b => b ? " is false" : " is true", // if we *ignore* true, then "incN = foo is false" long l when attrib.ConstructorArguments[0].Type is INamedTypeSymbol { EnumUnderlyingType: not null } enumType - => GetEnumExpression(enumType, l), - long l => l.ToString(CultureInfo.InvariantCulture), + => " != " + GetEnumExpression(enumType, l), + long l => " != " + l.ToString(CultureInfo.InvariantCulture), int i when attrib.ConstructorArguments[0].Type is INamedTypeSymbol { EnumUnderlyingType: not null } enumType - => GetEnumExpression(enumType, i), - int i => i.ToString(CultureInfo.InvariantCulture), + => " != " + GetEnumExpression(enumType, i), + int i => " != " + i.ToString(CultureInfo.InvariantCulture), _ => null, }; @@ -1016,10 +1021,10 @@ void WriteParameterName(in ParameterTuple p, StringBuilder? target = null) case ParameterFlags.Nullable | ParameterFlags.IgnoreExpression: sb.Append(" is { } __val").Append(parameter.ArgIndex) .Append(" && __val").Append(parameter.ArgIndex) - .Append(" != ").Append(parameter.IgnoreExpression); + .Append(parameter.IgnoreExpression); break; case ParameterFlags.IgnoreExpression: - sb.Append(" != ").Append(parameter.IgnoreExpression); + sb.Append(parameter.IgnoreExpression); break; case ParameterFlags.Collection: // non-nullable collection; literals already handled @@ -1103,7 +1108,7 @@ void WriteParameterName(in ParameterTuple p, StringBuilder? target = null) // help identify what this is (not needed for collections, since foo.Count etc) sb.Append(" // "); WriteParameterName(parameter); - if (argCount != 1) sb.Append(" (").Append(parameter.Name).Append(")"); // give an example + if (tuple.Value.ShareCount != 1) sb.Append(" (").Append(parameter.Name).Append(")"); // give an example } if (literalCount != 0) diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs index 30c3728bd..176352720 100644 --- a/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs +++ b/src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs @@ -758,6 +758,14 @@ public static partial RespOperation ZRevRangeByScoreWithScores RedisKey key, RedisValue member); + [RespCommand(Parser = "RespParsers.ZScanSimple")] + public static partial RespOperation> ZScan( + this in SortedSetCommands context, + RedisKey key, + long cursor, + [RespPrefix("MATCH"), RespIgnore] RedisValue pattern = default, + [RespPrefix("COUNT"), RespIgnore(10)] long count = 10); + [RespCommand] public static partial RespOperation ZScore( this in SortedSetCommands context, diff --git a/src/RESPite.StackExchange.Redis/RedisCommands.cs b/src/RESPite.StackExchange.Redis/RedisCommands.cs index 603b7d6e7..35c604715 100644 --- a/src/RESPite.StackExchange.Redis/RedisCommands.cs +++ b/src/RESPite.StackExchange.Redis/RedisCommands.cs @@ -1,7 +1,41 @@ -namespace RESPite.StackExchange.Redis; +using System.Buffers; +using System.Runtime.CompilerServices; + +namespace RESPite.StackExchange.Redis; internal static partial class RedisCommands { public static ref readonly RespContext Self(this in RespContext context) => ref context; // this just proves that the above are well-defined in terms of escape analysis } + +public readonly struct ScanResult +{ + private const int MSB = 1 << 31; + private readonly int _countAndIsPooled; // and use MSB for "ispooled" + private readonly T[] values; + + public ScanResult(long cursor, T[] values) + { + Cursor = cursor; + this.values = values; + _countAndIsPooled = values.Length; + } + internal ScanResult(long cursor, T[] values, int count) + { + this.Cursor = cursor; + this.values = values; + _countAndIsPooled = count | MSB; + } + + public long Cursor { get; } + public ReadOnlySpan Values => new(values, 0, _countAndIsPooled & ~MSB); + + internal void UnsafeRecycle() + { + var arr = values; + bool recycle = (_countAndIsPooled & MSB) != 0; + Unsafe.AsRef(in this) = default; // best effort at salting the earth + if (recycle && arr is not null) ArrayPool.Shared.Return(arr); + } +} diff --git a/src/RESPite.StackExchange.Redis/RespParsers.ScanParsers.cs b/src/RESPite.StackExchange.Redis/RespParsers.ScanParsers.cs new file mode 100644 index 000000000..ad562468f --- /dev/null +++ b/src/RESPite.StackExchange.Redis/RespParsers.ScanParsers.cs @@ -0,0 +1,36 @@ +using RESPite.Messages; +using StackExchange.Redis; + +namespace RESPite.StackExchange.Redis; + +public static partial class RespParsers +{ + internal static IRespParser> ZScanSimple = ScanResultParser.NonLeased; + internal static IRespParser> ZScanLeased = ScanResultParser.Leased; + + private sealed class ScanResultParser : IRespParser> + { + public static readonly ScanResultParser NonLeased = new(false); + public static readonly ScanResultParser Leased = new(true); + private readonly bool _leased; + private ScanResultParser(bool leased) => _leased = leased; + + ScanResult IRespParser>.Parse(ref RespReader reader) + { + reader.DemandAggregate(); + reader.MoveNextScalar(); + var cursor = reader.ReadInt64(); + reader.MoveNextAggregate(); + if (_leased) + { + var values = DefaultParser.ReadLeasedSortedSetEntryArray(ref reader, out int count); + return new(cursor, values, count); + } + else + { + var values = DefaultParser.ReadSortedSetEntryArray(ref reader); + return new(cursor, values); + } + } + } +} diff --git a/src/RESPite.StackExchange.Redis/RespParsers.cs b/src/RESPite.StackExchange.Redis/RespParsers.cs index adf54a517..f0a4f0258 100644 --- a/src/RESPite.StackExchange.Redis/RespParsers.cs +++ b/src/RESPite.StackExchange.Redis/RespParsers.cs @@ -4,7 +4,7 @@ namespace RESPite.StackExchange.Redis; -public static class RespParsers +public static partial class RespParsers { public static IRespParser RedisValue => DefaultParser.Instance; public static IRespParser RedisValueArray => DefaultParser.Instance; @@ -114,13 +114,21 @@ ListPopResult IRespParser.Parse(ref RespReader reader) } SortedSetEntry[] IRespParser.Parse(ref RespReader reader) - { - return reader.ReadPairArray( - SharedReadRedisValue, - static (ref RespReader reader) => reader.ReadDouble(), - static (x, y) => new SortedSetEntry(x, y), - scalar: true)!; - } + => ReadSortedSetEntryArray(ref reader); + + internal static SortedSetEntry[] ReadSortedSetEntryArray(ref RespReader reader) => reader.ReadPairArray( + SharedReadRedisValue, + static (ref RespReader reader) => reader.ReadDouble(), + static (x, y) => new SortedSetEntry(x, y), + scalar: true)!; + + internal static SortedSetEntry[] ReadLeasedSortedSetEntryArray(ref RespReader reader, out int count) + => reader.ReadLeasedPairArray( + SharedReadRedisValue, + static (ref RespReader reader) => reader.ReadDouble(), + static (x, y) => new SortedSetEntry(x, y), + out count, + scalar: true)!; SortedSetEntry? IRespParser.Parse(ref RespReader reader) { diff --git a/src/RESPite/Messages/RespReader.cs b/src/RESPite/Messages/RespReader.cs index 0b2bd6764..4c99187d3 100644 --- a/src/RESPite/Messages/RespReader.cs +++ b/src/RESPite/Messages/RespReader.cs @@ -1727,4 +1727,46 @@ public readonly T ReadEnum(T unknownValue = default) where T : struct, Enum } return result; } + internal TResult[]? ReadLeasedPairArray( + Projection first, + Projection second, + Func combine, + out int count, + bool scalar = true) + { + DemandAggregate(); + if (IsNull) + { + count = 0; + return null; + } + int sourceLength = AggregateLength(); + count = sourceLength >> 1; + if (count is 0) return []; + + var oversized = ArrayPool.Shared.Rent(count); + var result = oversized.AsSpan(0, count); + if (scalar) + { + // if the data to be consumed is simple (scalar), we can use + // a simpler path that doesn't need to worry about RESP subtrees + for (int i = 0; i < result.Length; i++) + { + MoveNextScalar(); + var x = first(ref this); + MoveNextScalar(); + var y = second(ref this); + result[i] = combine(x, y); + } + // if we have an odd number of source elements, skip the last one + if ((sourceLength & 1) != 0) MoveNextScalar(); + } + else + { + var agg = AggregateChildren(); + agg.FillAll(result, first, second, combine); + agg.MovePast(out this); + } + return oversized; + } } diff --git a/src/RESPite/RespIgnoreAttribute.cs b/src/RESPite/RespIgnoreAttribute.cs index 614218b1c..a79a96b61 100644 --- a/src/RESPite/RespIgnoreAttribute.cs +++ b/src/RESPite/RespIgnoreAttribute.cs @@ -5,9 +5,9 @@ namespace RESPite; [AttributeUsage(AttributeTargets.Parameter)] [Conditional("DEBUG"), ImmutableObject(true)] -public sealed class RespIgnoreAttribute : Attribute +public sealed class RespIgnoreAttribute(object? value = null) : Attribute { - private readonly object _value; - public object Value => _value; - public RespIgnoreAttribute(object value) => _value = value; + // note; nulls are always ignored (taking NRTs into account); the purpose + // of an explicit null is for RedisValue - this prompts HasValue checks (i.e. non-trivial value). + public object? Value => value; }