172 lines
5.1 KiB
C#
172 lines
5.1 KiB
C#
using System.Buffers;
|
|
using System.IO.Pipelines;
|
|
using System.Threading;
|
|
|
|
namespace AyCode.Services.Server.Tests.SignalRs;
|
|
|
|
/// <summary>
|
|
/// Pipe-based test transport for AsyncSegment protocol path.
|
|
///
|
|
/// Unlike <see cref="SlabTransportWriter"/> (IBufferWriter only), this exposes a real
|
|
/// <see cref="PipeWriter"/>, so <c>AcBinaryHubProtocol.WriteMessage</c> can enter
|
|
/// AsyncSegment chunked mode (<c>output is PipeWriter</c> check).
|
|
///
|
|
/// The internal <see cref="Pipe"/> uses a slab-like memory pool with fixed segment size,
|
|
/// random offsets and size jitter to better simulate transport behavior.
|
|
/// </summary>
|
|
internal sealed class AsyncSegmentPipeTransportWriter : IDisposable
|
|
{
|
|
private readonly Pipe _pipe;
|
|
private bool _disposed;
|
|
private bool _writerCompleted;
|
|
|
|
public AsyncSegmentPipeTransportWriter(int segmentSize = 256, int seed = 42)
|
|
{
|
|
if (segmentSize <= 0)
|
|
throw new ArgumentOutOfRangeException(nameof(segmentSize));
|
|
|
|
_pipe = new Pipe(new PipeOptions(
|
|
pool: new SlabSimulatingPool(segmentSize, seed),
|
|
readerScheduler: PipeScheduler.Inline,
|
|
writerScheduler: PipeScheduler.Inline,
|
|
pauseWriterThreshold: 0,
|
|
resumeWriterThreshold: 0,
|
|
minimumSegmentSize: segmentSize,
|
|
useSynchronizationContext: false));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the PipeWriter that must be passed to protocol <c>WriteMessage</c>
|
|
/// to activate AsyncSegment chunked write path.
|
|
/// </summary>
|
|
public PipeWriter Writer => _pipe.Writer;
|
|
|
|
/// <summary>
|
|
/// Gets the paired PipeReader for test-side inspection and parsing.
|
|
/// </summary>
|
|
public PipeReader Reader => _pipe.Reader;
|
|
|
|
/// <summary>
|
|
/// Completes only the writer side of the internal pipe.
|
|
/// Reader remains open so tests can continue draining buffered data.
|
|
/// </summary>
|
|
public void CompleteWriter()
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
|
|
if (_writerCompleted)
|
|
return;
|
|
|
|
_writerCompleted = true;
|
|
_pipe.Writer.Complete();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Drains all currently available bytes from the reader into a contiguous array.
|
|
/// Does not require completing the writer.
|
|
/// </summary>
|
|
public byte[] DrainAvailableBytes()
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
|
|
var output = new ArrayBufferWriter<byte>();
|
|
|
|
while (_pipe.Reader.TryRead(out var result))
|
|
{
|
|
var buffer = result.Buffer;
|
|
foreach (var segment in buffer)
|
|
output.Write(segment.Span);
|
|
|
|
_pipe.Reader.AdvanceTo(buffer.End);
|
|
|
|
if (result.IsCompleted)
|
|
break;
|
|
}
|
|
|
|
return output.WrittenSpan.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asynchronously drains all data until the writer is completed and the pipe is exhausted.
|
|
/// </summary>
|
|
public async Task<byte[]> DrainAllAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
|
|
var output = new ArrayBufferWriter<byte>();
|
|
|
|
while (true)
|
|
{
|
|
var result = await _pipe.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
|
var buffer = result.Buffer;
|
|
|
|
foreach (var segment in buffer)
|
|
output.Write(segment.Span);
|
|
|
|
_pipe.Reader.AdvanceTo(buffer.End);
|
|
|
|
if (result.IsCompleted)
|
|
break;
|
|
}
|
|
|
|
return output.WrittenSpan.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Completes writer and reader sides of the internal pipe.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
_disposed = true;
|
|
if (!_writerCompleted)
|
|
{
|
|
_writerCompleted = true;
|
|
_pipe.Writer.Complete();
|
|
}
|
|
_pipe.Reader.Complete();
|
|
}
|
|
|
|
private sealed class SlabSimulatingPool : MemoryPool<byte>
|
|
{
|
|
private readonly int _segmentSize;
|
|
private readonly Random _rng;
|
|
|
|
public SlabSimulatingPool(int segmentSize, int seed)
|
|
{
|
|
_segmentSize = segmentSize;
|
|
_rng = new Random(seed);
|
|
}
|
|
|
|
public override int MaxBufferSize => _segmentSize;
|
|
|
|
public override IMemoryOwner<byte> Rent(int minBufferSize = -1)
|
|
{
|
|
var requested = minBufferSize > 0 ? minBufferSize : _segmentSize;
|
|
var size = Math.Max(requested, _segmentSize);
|
|
|
|
var offset = _rng.Next(0, Math.Max(1, _segmentSize));
|
|
var jitter = _rng.Next(-_segmentSize / 4, _segmentSize / 4 + 1);
|
|
var actualSize = Math.Max(16, size + jitter);
|
|
|
|
var array = new byte[actualSize + offset];
|
|
return new Owner(array, offset, actualSize);
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
}
|
|
|
|
private sealed class Owner(byte[] array, int offset, int length) : IMemoryOwner<byte>
|
|
{
|
|
public Memory<byte> Memory { get; } = array.AsMemory(offset, length);
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|