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

82 lines
3.1 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.
///
/// Write side: uses Pipe (not ArrayBufferWriter) so GetSpan/GetMemory return stable slab segments
/// — matching Kestrel's memory pool behavior. This ensures Span back-patching for length prefixes works.
///
/// Read side: splits the serialized bytes into 256-byte segments before parsing,
/// exercising SequenceBinaryInput cross-boundary reads at every boundary.
/// </summary>
internal class TestMultiSegmentProtocol : AyCodeBinaryHubProtocol
{
private const int SegmentSize = 256;
/// <summary>
/// Serialize via Pipe (production-like stable memory blocks) instead of ArrayBufferWriter.
/// </summary>
public new ReadOnlyMemory<byte> GetMessageBytes(HubMessage message)
{
var pipe = new Pipe();
WriteMessage(message, pipe.Writer);
pipe.Writer.Complete();
pipe.Reader.TryRead(out var result);
var bytes = result.Buffer.ToArray();
pipe.Reader.Complete();
return bytes;
}
/// <summary>
/// Split input into 256-byte segments before parsing — forces multi-segment ReadOnlySequence
/// through SequenceBinaryInput, exercising cross-boundary reads on every test.
/// </summary>
public override bool TryParseMessage(ref ReadOnlySequence<byte> input, IInvocationBinder binder,
[NotNullWhen(true)] out HubMessage? message)
{
var multiSegment = CreateMultiSegmentSequence(input, SegmentSize);
return base.TryParseMessage(ref multiSegment, binder, out message);
}
private static ReadOnlySequence<byte> CreateMultiSegmentSequence(ReadOnlySequence<byte> source, int chunkSize)
{
var bytes = source.ToArray();
// Each segment gets its own byte[] — matching Kestrel pool slab behavior
// where each pipe segment is a separate memory block.
var firstChunk = new byte[Math.Min(chunkSize, bytes.Length)];
Buffer.BlockCopy(bytes, 0, firstChunk, 0, firstChunk.Length);
var first = new MemorySegment(firstChunk);
var current = first;
for (var offset = chunkSize; offset < bytes.Length; offset += chunkSize)
{
var length = Math.Min(chunkSize, bytes.Length - offset);
var chunk = new byte[length];
Buffer.BlockCopy(bytes, offset, chunk, 0, length);
current = current.Append(chunk);
}
return new ReadOnlySequence<byte>(first, 0, current, current.Memory.Length);
}
private sealed class MemorySegment : ReadOnlySequenceSegment<byte>
{
public MemorySegment(ReadOnlyMemory<byte> memory) => Memory = memory;
public MemorySegment Append(ReadOnlyMemory<byte> memory)
{
var next = new MemorySegment(memory) { RunningIndex = RunningIndex + Memory.Length };
Next = next;
return next;
}
}
}