From 35e77396ceecec3d910bb44c029ec6f41dead4d6 Mon Sep 17 00:00:00 2001 From: ITikhonov Date: Wed, 17 Jun 2026 17:27:25 +0300 Subject: [PATCH 1/3] add ResponseArrayPool --- src/StackExchange.Redis/ConfigurationOptions.cs | 12 ++++++++++++ src/StackExchange.Redis/Lease.cs | 11 +++++++---- src/StackExchange.Redis/PhysicalConnection.Read.cs | 8 +++++--- .../PublicAPI/PublicAPI.Shipped.txt | 2 +- .../PublicAPI/PublicAPI.Unshipped.txt | 2 ++ src/StackExchange.Redis/RespReaderExtensions.cs | 7 ++++--- src/StackExchange.Redis/ResultProcessor.Lease.cs | 4 ++-- 7 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 91de712f4..31ae2c8f4 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; @@ -1438,5 +1439,16 @@ public CycleBufferPool? RequestCycleBufferPool [Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] set; } + + /// + /// The array pool to use when buffering responses. + /// + public ArrayPool? ResponseArrayPool + { + [Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] + get; + [Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] + set; + } } } diff --git a/src/StackExchange.Redis/Lease.cs b/src/StackExchange.Redis/Lease.cs index a5a88e4eb..9c6395f86 100644 --- a/src/StackExchange.Redis/Lease.cs +++ b/src/StackExchange.Redis/Lease.cs @@ -16,6 +16,7 @@ public sealed class Lease : IMemoryOwner /// public static Lease Empty { get; } = new Lease(System.Array.Empty(), 0); + private readonly ArrayPool? _pool; private T[]? _arr; /// @@ -33,16 +34,18 @@ public sealed class Lease : IMemoryOwner /// /// The size required. /// Whether to erase the memory. - public static Lease Create(int length, bool clear = true) + /// Pool. + public static Lease Create(int length, bool clear = true, ArrayPool? pool = null) { if (length == 0) return Empty; - var arr = ArrayPool.Shared.Rent(length); + var arr = (pool ?? ArrayPool.Shared).Rent(length); if (clear) System.Array.Clear(arr, 0, length); return new Lease(arr, length); } - private Lease(T[] arr, int length) + private Lease(T[] arr, int length, ArrayPool? pool = null) { + _pool = pool; _arr = arr; Length = length; } @@ -55,7 +58,7 @@ public void Dispose() if (Length != 0) { var arr = Interlocked.Exchange(ref _arr, null); - if (arr != null) ArrayPool.Shared.Return(arr); + if (arr != null) (_pool ?? ArrayPool.Shared).Return(arr); } } diff --git a/src/StackExchange.Redis/PhysicalConnection.Read.cs b/src/StackExchange.Redis/PhysicalConnection.Read.cs index 774db2c6b..8cd9713da 100644 --- a/src/StackExchange.Redis/PhysicalConnection.Read.cs +++ b/src/StackExchange.Redis/PhysicalConnection.Read.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Buffers; using System.Buffers.Binary; using System.Diagnostics; @@ -401,14 +401,16 @@ private void OnResponseFrame(RespPrefix prefix, ReadOnlySequence payload) else { var len = checked((int)payload.Length); - byte[]? oversized = ArrayPool.Shared.Rent(len); + var arrayPool = BridgeCouldBeNull?.Multiplexer.RawConfig.ResponseArrayPool ?? ArrayPool.Shared; + + byte[]? oversized = arrayPool.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); + arrayPool.Return(oversized); } } } diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 269cc11d2..921dc0863 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1733,7 +1733,7 @@ static StackExchange.Redis.HashEntry.implicit operator System.Collections.Generi static StackExchange.Redis.HashEntry.operator !=(StackExchange.Redis.HashEntry x, StackExchange.Redis.HashEntry y) -> bool static StackExchange.Redis.HashEntry.operator ==(StackExchange.Redis.HashEntry x, StackExchange.Redis.HashEntry y) -> bool static StackExchange.Redis.KeyspaceIsolation.DatabaseExtensions.WithKeyPrefix(this StackExchange.Redis.IDatabase! database, StackExchange.Redis.RedisKey keyPrefix) -> StackExchange.Redis.IDatabase! -static StackExchange.Redis.Lease.Create(int length, bool clear = true) -> StackExchange.Redis.Lease! +static StackExchange.Redis.Lease.Create(int length, bool clear = true, System.Buffers.ArrayPool? pool = null) -> StackExchange.Redis.Lease! static StackExchange.Redis.Lease.Empty.get -> StackExchange.Redis.Lease! static StackExchange.Redis.ListPopResult.Null.get -> StackExchange.Redis.ListPopResult static StackExchange.Redis.LuaScript.GetCachedScriptCount() -> int diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 4a8bf8b68..ab0325aac 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #nullable enable +[SER004]StackExchange.Redis.ConfigurationOptions.ResponseArrayPool.get -> System.Buffers.ArrayPool? +[SER004]StackExchange.Redis.ConfigurationOptions.ResponseArrayPool.set -> void [SER004]StackExchange.Redis.ConfigurationOptions.RequestCycleBufferPool.get -> RESPite.Buffers.CycleBufferPool? [SER004]StackExchange.Redis.ConfigurationOptions.RequestCycleBufferPool.set -> void [SER004]StackExchange.Redis.ConfigurationOptions.ResponseCycleBufferPool.get -> RESPite.Buffers.CycleBufferPool? diff --git a/src/StackExchange.Redis/RespReaderExtensions.cs b/src/StackExchange.Redis/RespReaderExtensions.cs index a5bb77bdd..628f7b120 100644 --- a/src/StackExchange.Redis/RespReaderExtensions.cs +++ b/src/StackExchange.Redis/RespReaderExtensions.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Buffers; using System.Diagnostics; using System.Threading.Tasks; using RESPite.Messages; @@ -135,7 +136,7 @@ public void MovePastBof() public RedisValue[]? ReadPastRedisValues() => reader.ReadPastArray(static (ref r) => r.ReadRedisValue(), scalar: true); - public Lease? AsLease() + public Lease? AsLease(ArrayPool? pool = null) { if (!reader.IsScalar) throw new InvalidCastException("Cannot convert to Lease: " + reader.Prefix); if (reader.IsNull) return null; @@ -143,7 +144,7 @@ public void MovePastBof() var length = reader.ScalarLength(); if (length == 0) return Lease.Empty; - var lease = Lease.Create(length, clear: false); + var lease = Lease.Create(length, clear: false, pool); if (reader.TryGetSpan(out var span)) { span.CopyTo(lease.Span); diff --git a/src/StackExchange.Redis/ResultProcessor.Lease.cs b/src/StackExchange.Redis/ResultProcessor.Lease.cs index 91dfcd2ad..b239d5ef9 100644 --- a/src/StackExchange.Redis/ResultProcessor.Lease.cs +++ b/src/StackExchange.Redis/ResultProcessor.Lease.cs @@ -152,7 +152,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { if (reader.IsScalar) { - SetResult(message, reader.AsLease()!); + SetResult(message, reader.AsLease(connection.BridgeCouldBeNull?.Multiplexer.RawConfig.ResponseArrayPool)!); return true; } return false; @@ -167,7 +167,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes && reader.TryMoveNext() && reader.IsScalar) { // treat an array of 1 like a single reply - SetResult(message, reader.AsLease()!); + SetResult(message, reader.AsLease(connection.BridgeCouldBeNull?.Multiplexer.RawConfig.ResponseArrayPool)!); return true; } return false; From 73b85f799a9c738307aff5fa41e363990170d321 Mon Sep 17 00:00:00 2001 From: ITikhonov Date: Wed, 17 Jun 2026 20:14:58 +0300 Subject: [PATCH 2/3] Lease --- .../ConfigurationOptions.cs | 4 +- src/StackExchange.Redis/Lease.cs | 73 ++++++++++++++----- .../PhysicalConnection.Read.cs | 2 +- .../PublicAPI/PublicAPI.Shipped.txt | 3 +- .../PublicAPI/PublicAPI.Unshipped.txt | 4 +- .../RespReaderExtensions.cs | 4 +- .../ResultProcessor.Lease.cs | 4 +- 7 files changed, 65 insertions(+), 29 deletions(-) diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 31ae2c8f4..8ad1662be 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -1441,9 +1441,9 @@ public CycleBufferPool? RequestCycleBufferPool } /// - /// The array pool to use when buffering responses. + /// The memory pool to use when buffering responses. /// - public ArrayPool? ResponseArrayPool + public MemoryPool? ResponseMemoryPool { [Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] get; diff --git a/src/StackExchange.Redis/Lease.cs b/src/StackExchange.Redis/Lease.cs index 9c6395f86..c77f6d6e9 100644 --- a/src/StackExchange.Redis/Lease.cs +++ b/src/StackExchange.Redis/Lease.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading; namespace StackExchange.Redis @@ -16,8 +17,7 @@ public sealed class Lease : IMemoryOwner /// public static Lease Empty { get; } = new Lease(System.Array.Empty(), 0); - private readonly ArrayPool? _pool; - private T[]? _arr; + private object? _buffer; /// /// Gets whether this lease is empty. @@ -34,19 +34,37 @@ public sealed class Lease : IMemoryOwner /// /// The size required. /// Whether to erase the memory. - /// Pool. - public static Lease Create(int length, bool clear = true, ArrayPool? pool = null) + public static Lease Create(int length, bool clear = true) { if (length == 0) return Empty; - var arr = (pool ?? ArrayPool.Shared).Rent(length); + var arr = ArrayPool.Shared.Rent(length); if (clear) System.Array.Clear(arr, 0, length); return new Lease(arr, length); } - private Lease(T[] arr, int length, ArrayPool? pool = null) + /// + /// Create a new lease. + /// + /// The size required. + /// Buffer. + public static Lease Create(int length, IMemoryOwner memoryOwner) { - _pool = pool; - _arr = arr; + if (length == 0) return Empty; + if ((uint)length > memoryOwner.Memory.Length) + throw new ArgumentOutOfRangeException(nameof(length)); + + return new Lease(memoryOwner, length); + } + + private Lease(T[] arr, int length) + { + _buffer = arr; + Length = length; + } + + private Lease(IMemoryOwner memoryOwner, int length) + { + _buffer = memoryOwner; Length = length; } @@ -57,33 +75,50 @@ public void Dispose() { if (Length != 0) { - var arr = Interlocked.Exchange(ref _arr, null); - if (arr != null) (_pool ?? ArrayPool.Shared).Return(arr); + var buffer = Interlocked.Exchange(ref _buffer, null); + if (buffer != null) + { + if (buffer is T[] arr) + ArrayPool.Shared.Return(arr); + else + ((IMemoryOwner)buffer).Dispose(); + } } } [MethodImpl(MethodImplOptions.NoInlining)] private static T[] ThrowDisposed() => throw new ObjectDisposedException(nameof(Lease)); - private T[] Array - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _arr ?? ThrowDisposed(); - } - /// /// The data as a . /// - public Memory Memory => new Memory(Array, 0, Length); + public Memory Memory => _buffer is IMemoryOwner memoryOwner + ? memoryOwner.Memory.Slice(0, Length) + : new Memory((T[]?)_buffer ?? ThrowDisposed(), 0, Length); /// /// The data as a . /// - public Span Span => new Span(Array, 0, Length); + public Span Span => _buffer is IMemoryOwner memoryOwner + ? memoryOwner.Memory.Span.Slice(0, Length) + : new Span((T[]?)_buffer ?? ThrowDisposed(), 0, Length); /// /// The data as an . /// - public ArraySegment ArraySegment => new ArraySegment(Array, 0, Length); + public ArraySegment ArraySegment + { + get + { + if (_buffer is IMemoryOwner memoryOwner) + { + if (!MemoryMarshal.TryGetArray((ReadOnlyMemory)memoryOwner.Memory, out var segment)) + throw new NotSupportedException("Only array-backed buffers are supported"); + + return new ArraySegment(segment.Array!, segment.Offset, Length); + } + return new ArraySegment((T[]?)_buffer ?? ThrowDisposed(), 0, Length); + } + } } } diff --git a/src/StackExchange.Redis/PhysicalConnection.Read.cs b/src/StackExchange.Redis/PhysicalConnection.Read.cs index 8cd9713da..60064b625 100644 --- a/src/StackExchange.Redis/PhysicalConnection.Read.cs +++ b/src/StackExchange.Redis/PhysicalConnection.Read.cs @@ -401,7 +401,7 @@ private void OnResponseFrame(RespPrefix prefix, ReadOnlySequence payload) else { var len = checked((int)payload.Length); - var arrayPool = BridgeCouldBeNull?.Multiplexer.RawConfig.ResponseArrayPool ?? ArrayPool.Shared; + var arrayPool = ArrayPool.Shared; byte[]? oversized = arrayPool.Rent(len); payload.CopyTo(oversized); diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 921dc0863..47422be39 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1733,7 +1733,8 @@ static StackExchange.Redis.HashEntry.implicit operator System.Collections.Generi static StackExchange.Redis.HashEntry.operator !=(StackExchange.Redis.HashEntry x, StackExchange.Redis.HashEntry y) -> bool static StackExchange.Redis.HashEntry.operator ==(StackExchange.Redis.HashEntry x, StackExchange.Redis.HashEntry y) -> bool static StackExchange.Redis.KeyspaceIsolation.DatabaseExtensions.WithKeyPrefix(this StackExchange.Redis.IDatabase! database, StackExchange.Redis.RedisKey keyPrefix) -> StackExchange.Redis.IDatabase! -static StackExchange.Redis.Lease.Create(int length, bool clear = true, System.Buffers.ArrayPool? pool = null) -> StackExchange.Redis.Lease! +static StackExchange.Redis.Lease.Create(int length, bool clear = true) -> StackExchange.Redis.Lease! +static StackExchange.Redis.Lease.Create(int length, System.Buffers.IMemoryOwner! memoryOwner) -> StackExchange.Redis.Lease! static StackExchange.Redis.Lease.Empty.get -> StackExchange.Redis.Lease! static StackExchange.Redis.ListPopResult.Null.get -> StackExchange.Redis.ListPopResult static StackExchange.Redis.LuaScript.GetCachedScriptCount() -> int diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index ab0325aac..648acbb60 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,6 +1,6 @@ #nullable enable -[SER004]StackExchange.Redis.ConfigurationOptions.ResponseArrayPool.get -> System.Buffers.ArrayPool? -[SER004]StackExchange.Redis.ConfigurationOptions.ResponseArrayPool.set -> void +[SER004]StackExchange.Redis.ConfigurationOptions.ResponseMemoryPool.get -> System.Buffers.MemoryPool? +[SER004]StackExchange.Redis.ConfigurationOptions.ResponseMemoryPool.set -> void [SER004]StackExchange.Redis.ConfigurationOptions.RequestCycleBufferPool.get -> RESPite.Buffers.CycleBufferPool? [SER004]StackExchange.Redis.ConfigurationOptions.RequestCycleBufferPool.set -> void [SER004]StackExchange.Redis.ConfigurationOptions.ResponseCycleBufferPool.get -> RESPite.Buffers.CycleBufferPool? diff --git a/src/StackExchange.Redis/RespReaderExtensions.cs b/src/StackExchange.Redis/RespReaderExtensions.cs index 628f7b120..4884690fe 100644 --- a/src/StackExchange.Redis/RespReaderExtensions.cs +++ b/src/StackExchange.Redis/RespReaderExtensions.cs @@ -136,7 +136,7 @@ public void MovePastBof() public RedisValue[]? ReadPastRedisValues() => reader.ReadPastArray(static (ref r) => r.ReadRedisValue(), scalar: true); - public Lease? AsLease(ArrayPool? pool = null) + public Lease? AsLease() { if (!reader.IsScalar) throw new InvalidCastException("Cannot convert to Lease: " + reader.Prefix); if (reader.IsNull) return null; @@ -144,7 +144,7 @@ public void MovePastBof() var length = reader.ScalarLength(); if (length == 0) return Lease.Empty; - var lease = Lease.Create(length, clear: false, pool); + var lease = Lease.Create(length, clear: false); if (reader.TryGetSpan(out var span)) { span.CopyTo(lease.Span); diff --git a/src/StackExchange.Redis/ResultProcessor.Lease.cs b/src/StackExchange.Redis/ResultProcessor.Lease.cs index b239d5ef9..91dfcd2ad 100644 --- a/src/StackExchange.Redis/ResultProcessor.Lease.cs +++ b/src/StackExchange.Redis/ResultProcessor.Lease.cs @@ -152,7 +152,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { if (reader.IsScalar) { - SetResult(message, reader.AsLease(connection.BridgeCouldBeNull?.Multiplexer.RawConfig.ResponseArrayPool)!); + SetResult(message, reader.AsLease()!); return true; } return false; @@ -167,7 +167,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes && reader.TryMoveNext() && reader.IsScalar) { // treat an array of 1 like a single reply - SetResult(message, reader.AsLease(connection.BridgeCouldBeNull?.Multiplexer.RawConfig.ResponseArrayPool)!); + SetResult(message, reader.AsLease()!); return true; } return false; From 9a2c942a041bd3a01391580613d298e06cb0414b Mon Sep 17 00:00:00 2001 From: ITikhonov Date: Wed, 17 Jun 2026 20:52:43 +0300 Subject: [PATCH 3/3] IMemoryOwner? --- .../PhysicalConnection.Read.cs | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/StackExchange.Redis/PhysicalConnection.Read.cs b/src/StackExchange.Redis/PhysicalConnection.Read.cs index 60064b625..7ee1897c4 100644 --- a/src/StackExchange.Redis/PhysicalConnection.Read.cs +++ b/src/StackExchange.Redis/PhysicalConnection.Read.cs @@ -205,7 +205,7 @@ private bool ShouldTransitionToAsync() private bool ForceReconnect => BridgeCouldBeNull?.NeedsReconnect == true; - private static byte[]? SharedNoLease; + private static IMemoryOwner? SharedNoLease; private CycleBuffer _readBuffer; private RespScanState _readState = default; @@ -401,17 +401,15 @@ private void OnResponseFrame(RespPrefix prefix, ReadOnlySequence payload) else { var len = checked((int)payload.Length); - var arrayPool = ArrayPool.Shared; + var memoryPool = BridgeCouldBeNull?.Multiplexer.RawConfig.ResponseMemoryPool ?? MemoryPool.Shared; + var memoryOwner = memoryPool.Rent(len); + Span oversized = memoryOwner.Memory.Span.Slice(0, len); - byte[]? oversized = arrayPool.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.Return(oversized); - } + OnResponseFrame(prefix, oversized, ref memoryOwner); + + memoryOwner?.Dispose(); } } @@ -422,7 +420,7 @@ private void UpdateBufferStats(int lastResult, long inBuffer) bytesLastResult = lastResult; } - private void OnResponseFrame(RespPrefix prefix, ReadOnlySpan frame, ref byte[]? lease) + private void OnResponseFrame(RespPrefix prefix, ReadOnlySpan frame, ref IMemoryOwner? memoryOwner) { DebugValidateSingleFrame(frame); _readStatus = ReadStatus.MatchResult; @@ -433,7 +431,7 @@ private void OnResponseFrame(RespPrefix prefix, ReadOnlySpan frame, ref by case RespPrefix.Array when (_protocol is RedisProtocol.Resp2 & connectionType is ConnectionType.Subscription) && !IsArrayPong(frame): // could be a RESP2 pub/sub payload // out-of-band; pub/sub etc - if (OnOutOfBand(frame, ref lease)) + if (OnOutOfBand(frame, ref memoryOwner)) { OnDetailLog($"out-of-band message, not dequeuing: {prefix}"); return; @@ -504,7 +502,7 @@ internal static ReadOnlySpan StackCopyLengthChecked(scoped in RespReader r return buffer.Slice(0, len); } - private bool OnOutOfBand(ReadOnlySpan payload, ref byte[]? lease) + private bool OnOutOfBand(ReadOnlySpan payload, ref IMemoryOwner? memoryOwner) { var muxer = BridgeCouldBeNull?.Multiplexer; if (muxer is null) return true; // consume it blindly