baae59ce3b
Gateway server-side named-pipe frame path, wire format byte-identical: - Writer (GWC-08/IPC-13): serialize once into a single ArrayPool-rented buffer holding the 4-byte LE length prefix + payload, then one WriteAsync. Removes the second serialization pass (ToByteArray re-ran CalculateSize), the separate prefix array + second stream write, and per-frame heap allocation. - Reader (IPC-14): rent the payload buffer from ArrayPool instead of new byte[] per frame; read/parse bounded to the real length, return in finally. Matches the worker side which already pools. - Correctness: length tracked separately from (larger) pool capacity; every rented buffer returned exactly once in finally incl. the malformed-protobuf path; ParseFrom copies into the message so the envelope never aliases the returned buffer. Cross-checked LE-prefix framing agrees across gateway+worker writer/reader. Server build clean (0 warnings); WorkerFrameProtocol tests 10/10 incl. a new large-payload-near-cap round-trip (forces pool buffer > frame). The 2 failing WorkerClient/FakeWorkerHarness tests are the known macOS UDS 104-char path limit, not a regression. Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
79 lines
3.1 KiB
C#
79 lines
3.1 KiB
C#
using System.Buffers;
|
|
using System.Buffers.Binary;
|
|
using Google.Protobuf;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Server.Workers;
|
|
|
|
/// <summary>
|
|
/// Writes length-prefixed WorkerEnvelope protobuf messages to a stream.
|
|
/// </summary>
|
|
public sealed class WorkerFrameWriter
|
|
{
|
|
private readonly WorkerFrameProtocolOptions _options;
|
|
private readonly Stream _stream;
|
|
|
|
/// <summary>
|
|
/// Initializes the writer with a stream and frame protocol options.
|
|
/// </summary>
|
|
/// <param name="stream">Stream to write frames to.</param>
|
|
/// <param name="options">Frame protocol configuration.</param>
|
|
public WorkerFrameWriter(
|
|
Stream stream,
|
|
WorkerFrameProtocolOptions options)
|
|
{
|
|
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
|
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a WorkerEnvelope as a length-prefixed message to the stream.
|
|
/// </summary>
|
|
/// <param name="envelope">Worker envelope message to write.</param>
|
|
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
public async ValueTask WriteAsync(
|
|
WorkerEnvelope envelope,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(envelope);
|
|
WorkerEnvelopeValidator.Validate(envelope, _options);
|
|
|
|
int payloadLength = envelope.CalculateSize();
|
|
if (payloadLength == 0)
|
|
{
|
|
throw new WorkerFrameProtocolException(
|
|
WorkerFrameProtocolErrorCode.InvalidEnvelope,
|
|
"Worker envelope cannot serialize to an empty payload.");
|
|
}
|
|
|
|
if (payloadLength > _options.MaxMessageBytes)
|
|
{
|
|
throw new WorkerFrameProtocolException(
|
|
WorkerFrameProtocolErrorCode.MessageTooLarge,
|
|
$"Worker envelope payload length {payloadLength} exceeds the configured maximum of {_options.MaxMessageBytes} bytes.");
|
|
}
|
|
|
|
// Serialize once into a single pooled buffer that carries the 4-byte little-endian length
|
|
// prefix followed by the payload, then issue one stream write. This avoids a second
|
|
// serialization pass (ToByteArray re-runs CalculateSize), a separate prefix array and its
|
|
// own write, and any per-frame heap allocation (GWC-08, IPC-13). The rented buffer may be
|
|
// larger than requested, so only the first frameLength bytes are ever written.
|
|
int frameLength = sizeof(uint) + payloadLength;
|
|
byte[] frame = ArrayPool<byte>.Shared.Rent(frameLength);
|
|
try
|
|
{
|
|
BinaryPrimitives.WriteUInt32LittleEndian(frame, (uint)payloadLength);
|
|
envelope.WriteTo(new Span<byte>(frame, sizeof(uint), payloadLength));
|
|
|
|
await _stream
|
|
.WriteAsync(new ReadOnlyMemory<byte>(frame, 0, frameLength), cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
ArrayPool<byte>.Shared.Return(frame);
|
|
}
|
|
}
|
|
}
|