AyCode.Core/AyCode.Services.Server.Tests/SignalRs/TestMultiSegmentProtocol.cs

103 lines
4.0 KiB
C#

using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.IO.Pipelines;
using AyCode.Services.SignalRs;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Protocol;
namespace AyCode.Services.Server.Tests.SignalRs;
/// <summary>
/// Test protocol that simulates production Kestrel pipe behavior with 256-byte segments.
///
/// Production: SignalR → WriteMessage(PipeWriter) → Kestrel slab 4096-byte segments → PipeReader → TryParseMessage
/// Test: SignalR → WriteMessage(PipeWriter) → FixedSizePool 256-byte segments → PipeReader → TryParseMessage
///
/// Both sides go through a real Pipe with fixed-size memory segments,
/// exercising BWO chunk writes and SequenceBinaryInput cross-boundary reads.
/// </summary>
internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol
{
private const int SegmentSize = 256;
public TestMultiSegmentProtocol()
{
Options.BufferWriterChunkSize = SegmentSize;
}
/// <summary>
/// Write side: WriteMessage → PipeWriter backed by 256-byte pool segments.
/// Same code path as production, just smaller segments.
/// </summary>
public new ReadOnlyMemory<byte> GetMessageBytes(HubMessage message)
{
var pipe = new Pipe(new PipeOptions(pool: new SlabSimulatingPool(SegmentSize)));
WriteMessage(message, pipe.Writer);
pipe.Writer.Complete();
pipe.Reader.TryRead(out var result);
var bytes = result.Buffer.ToArray();
pipe.Reader.Complete();
return bytes;
}
/// <summary>
/// Read side: fill PipeWriter 256 bytes at a time → PipeReader gives multi-segment sequence.
/// Same as production Kestrel PipeReader delivering 4096-byte segments.
/// </summary>
public override bool TryParseMessage(ref ReadOnlySequence<byte> input, IInvocationBinder binder,
[NotNullWhen(true)] out HubMessage? message)
{
var bytes = input.ToArray();
var pipe = new Pipe(new PipeOptions(pool: new SlabSimulatingPool(SegmentSize)));
var writer = pipe.Writer;
// Write in chunks — GetMemory may return less than requested (like Kestrel slab)
var remaining = bytes.AsSpan();
while (remaining.Length > 0)
{
var mem = writer.GetMemory(Math.Min(SegmentSize, remaining.Length));
var chunk = Math.Min(mem.Length, remaining.Length);
remaining[..chunk].CopyTo(mem.Span);
writer.Advance(chunk);
remaining = remaining[chunk..];
}
writer.Complete();
pipe.Reader.TryRead(out var result);
var seq = result.Buffer;
var success = base.TryParseMessage(ref seq, binder, out message);
pipe.Reader.Complete();
return success;
}
/// <summary>
/// MemoryPool that returns <paramref name="segmentSize"/>-byte blocks at random offsets
/// within a larger backing array — simulating Kestrel's slab allocator where segments
/// share a large slab and have non-zero offsets.
/// </summary>
private sealed class SlabSimulatingPool(int segmentSize) : MemoryPool<byte>
{
private readonly Random _rng = new(42); // deterministic seed for reproducibility
public override int MaxBufferSize => segmentSize;
public override IMemoryOwner<byte> Rent(int minBufferSize = -1)
{
var size = Math.Max(minBufferSize, segmentSize);
var offset = _rng.Next(0, segmentSize); // random slab offset
var jitter = _rng.Next(-1, 2); // -1, 0, or +1
var actualSize = Math.Max(1, size + jitter); // random segment size variance
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() { }
}
}
}