using System.Buffers; using System.Buffers.Binary; using Google.Protobuf; using ZB.MOM.WW.MxGateway.Contracts.Proto; namespace ZB.MOM.WW.MxGateway.Server.Workers; /// /// Reads length-prefixed WorkerEnvelope protobuf frames from a stream. /// /// /// is not reentrant: the reader keeps a per-instance length-prefix scratch /// buffer, so exactly one consumer may be inside a read at a time. That matches how the reader is /// used — a single read loop per WorkerClient, with handshake reads completing before the /// loop starts. /// public sealed class WorkerFrameReader { private readonly WorkerFrameProtocolOptions _options; private readonly Stream _stream; // Reused across frames rather than allocated per read (GWC-30). Safe because ReadAsync is // single-consumer by construction; the prefix is fully overwritten by every read. private readonly byte[] _lengthPrefix = new byte[sizeof(uint)]; /// /// Initializes a new instance of . /// /// Stream to read frames from. /// Frame protocol options. public WorkerFrameReader( Stream stream, WorkerFrameProtocolOptions options) { _stream = stream ?? throw new ArgumentNullException(nameof(stream)); _options = options ?? throw new ArgumentNullException(nameof(options)); } /// /// Reads a worker envelope frame from the stream asynchronously. /// /// Cancellation token. /// Parsed worker envelope. public async ValueTask ReadAsync(CancellationToken cancellationToken = default) { await ReadExactlyOrThrowAsync(_lengthPrefix, cancellationToken).ConfigureAwait(false); uint payloadLength = BinaryPrimitives.ReadUInt32LittleEndian(_lengthPrefix); if (payloadLength == 0) { throw new WorkerFrameProtocolException( WorkerFrameProtocolErrorCode.MalformedLength, "Worker frame payload length must be greater than zero."); } if (payloadLength > _options.MaxMessageBytes) { throw new WorkerFrameProtocolException( WorkerFrameProtocolErrorCode.MessageTooLarge, $"Worker frame payload length {payloadLength} exceeds the configured maximum of {_options.MaxMessageBytes} bytes."); } // Rent the payload buffer from the shared pool rather than allocating a fresh byte[] per // frame; large event frames near the cap would otherwise allocate an LOH buffer each time // (IPC-14). ParseFrom copies whatever it needs into the parsed message, so the rented buffer // can be returned as soon as parsing completes without the envelope aliasing it. The rented // buffer may be larger than requested, so the read and parse are bounded to length. int length = checked((int)payloadLength); byte[] payload = ArrayPool.Shared.Rent(length); WorkerEnvelope envelope; try { await ReadExactlyOrThrowAsync(new Memory(payload, 0, length), cancellationToken) .ConfigureAwait(false); try { envelope = WorkerEnvelope.Parser.ParseFrom(payload, 0, length); } catch (InvalidProtocolBufferException exception) { throw new WorkerFrameProtocolException( WorkerFrameProtocolErrorCode.InvalidEnvelope, "Worker frame payload is not a valid WorkerEnvelope protobuf message.", exception); } } finally { ArrayPool.Shared.Return(payload); } WorkerEnvelopeValidator.Validate(envelope, _options); return envelope; } private async ValueTask ReadExactlyOrThrowAsync( Memory buffer, CancellationToken cancellationToken) { try { await _stream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false); } catch (EndOfStreamException exception) { throw new WorkerFrameProtocolException( WorkerFrameProtocolErrorCode.EndOfStream, "Worker frame ended before the expected number of bytes were read.", exception); } } }