5e2e40a927
Behavior-preserving allocation cuts on the per-event/per-command path: - GWC-06: StreamEvents send-timing uses Stopwatch.GetTimestamp() + GetElapsedTime() instead of a per-event Stopwatch allocation (same measured span). - GWC-07/IPC-05 (event): MapEvent transfers ownership of the inner MxEvent instead of .Clone()-ing it. Safe: WorkerEvent is parsed fresh per pipe frame with the distributor pump as its single consumer (GWC-01), MapEvent runs once before fan-out, and every downstream consumer (subscribers, replay ring) only READS the event (WorkerSequence is stamped upstream; verified no post-mapping mutation). Comment documents the invariant + restore-clone caveat if a second consumer is added. - IPC-05 (command): CreateCommandEnvelope no longer re-clones; MapCommand already isolated the graph from the caller-owned gRPC message. - GWC-15: grpc_stream_queue.depth converts from a per-event push counter to an ObservableGauge summing registered channel sources at scrape time only (name/semantics unchanged); removes all per-event .Count/lock work. Kept every load-bearing isolation clone (MapCommand, Invoke, bulk filters, MapCommandReply). Server build clean (0 warnings); EventStream/Metrics/ Distributor/Mapper tests 62/62 (incl. formerly-flaky queue-depth tests, now green under the lazy gauge, + 2 new MapEvent ownership tests). Docs: Metrics.md, Grpc.md. Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
196 lines
7.0 KiB
C#
196 lines
7.0 KiB
C#
using Google.Protobuf.WellKnownTypes;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Server.Grpc;
|
|
|
|
/// <summary>
|
|
/// Maps between worker IPC types and gRPC contract types.
|
|
/// </summary>
|
|
public sealed class MxAccessGrpcMapper
|
|
{
|
|
private readonly TimeProvider _timeProvider;
|
|
|
|
/// <summary>
|
|
/// Initializes the mapper with an optional time provider.
|
|
/// </summary>
|
|
/// <param name="timeProvider">Time provider for timestamps; defaults to system time if null.</param>
|
|
public MxAccessGrpcMapper(TimeProvider? timeProvider = null)
|
|
{
|
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a gRPC MX command request to a worker command.
|
|
/// </summary>
|
|
/// <param name="request">Request payload.</param>
|
|
/// <returns>The mapped <see cref="WorkerCommand"/> ready for worker dispatch.</returns>
|
|
public WorkerCommand MapCommand(MxCommandRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
ArgumentNullException.ThrowIfNull(request.Command);
|
|
|
|
return new WorkerCommand
|
|
{
|
|
Command = request.Command.Clone(),
|
|
EnqueueTimestamp = Timestamp.FromDateTimeOffset(_timeProvider.GetUtcNow()),
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a worker command reply to a gRPC MX command reply.
|
|
/// </summary>
|
|
/// <param name="reply">Worker command reply.</param>
|
|
/// <returns>The mapped <see cref="MxCommandReply"/>, or a protocol-violation reply if the worker reply carried no public payload.</returns>
|
|
public MxCommandReply MapCommandReply(WorkerCommandReply reply)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(reply);
|
|
|
|
if (reply.Reply is null)
|
|
{
|
|
return new MxCommandReply
|
|
{
|
|
ProtocolStatus = ProtocolViolation("Worker command reply did not contain a public reply payload."),
|
|
};
|
|
}
|
|
|
|
return reply.Reply.Clone();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a worker event to a gRPC MX event.
|
|
/// </summary>
|
|
/// <param name="workerEvent">Worker event to map.</param>
|
|
/// <returns>The mapped <see cref="MxEvent"/>, or an unspecified-family event if the worker event carried no public payload.</returns>
|
|
public MxEvent MapEvent(WorkerEvent workerEvent)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(workerEvent);
|
|
|
|
// GWC-07 / IPC-05: ownership transfer, not a deep clone. The enclosing WorkerEvent is
|
|
// parsed fresh from a single pipe frame in WorkerClient's read loop and is discarded
|
|
// immediately after this mapping — the SessionEventDistributor pump is its single
|
|
// consumer (GWC-01 claims the worker event channel as single-reader), so nothing else
|
|
// aliases or mutates workerEvent.Event. We therefore move the inner MxEvent into the
|
|
// outbound graph instead of cloning it. Downstream the pump fans this one MxEvent to
|
|
// every subscriber and retains it in the replay ring, but that sharing is READ-ONLY
|
|
// (subscribers only yield/filter it), so a single shared instance is safe. If a second
|
|
// consumer of WorkerEvent is ever added, restore a .Clone() here to re-isolate.
|
|
return workerEvent.Event ?? new MxEvent
|
|
{
|
|
Family = MxEventFamily.Unspecified,
|
|
RawStatus = "Worker event did not contain a public event payload.",
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an OK protocol status.
|
|
/// </summary>
|
|
/// <param name="message">Status message.</param>
|
|
/// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.Ok"/>.</returns>
|
|
public static ProtocolStatus Ok(string message = "OK")
|
|
{
|
|
return new ProtocolStatus
|
|
{
|
|
Code = ProtocolStatusCode.Ok,
|
|
Message = message,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an InvalidRequest protocol status.
|
|
/// </summary>
|
|
/// <param name="message">Status message.</param>
|
|
/// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.InvalidRequest"/>.</returns>
|
|
public static ProtocolStatus InvalidRequest(string message)
|
|
{
|
|
return new ProtocolStatus
|
|
{
|
|
Code = ProtocolStatusCode.InvalidRequest,
|
|
Message = message,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a SessionNotFound protocol status.
|
|
/// </summary>
|
|
/// <param name="message">Status message.</param>
|
|
/// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.SessionNotFound"/>.</returns>
|
|
public static ProtocolStatus SessionNotFound(string message)
|
|
{
|
|
return new ProtocolStatus
|
|
{
|
|
Code = ProtocolStatusCode.SessionNotFound,
|
|
Message = message,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a SessionNotReady protocol status.
|
|
/// </summary>
|
|
/// <param name="message">Status message.</param>
|
|
/// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.SessionNotReady"/>.</returns>
|
|
public static ProtocolStatus SessionNotReady(string message)
|
|
{
|
|
return new ProtocolStatus
|
|
{
|
|
Code = ProtocolStatusCode.SessionNotReady,
|
|
Message = message,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a WorkerUnavailable protocol status.
|
|
/// </summary>
|
|
/// <param name="message">Status message.</param>
|
|
/// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.WorkerUnavailable"/>.</returns>
|
|
public static ProtocolStatus WorkerUnavailable(string message)
|
|
{
|
|
return new ProtocolStatus
|
|
{
|
|
Code = ProtocolStatusCode.WorkerUnavailable,
|
|
Message = message,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a Timeout protocol status.
|
|
/// </summary>
|
|
/// <param name="message">Status message.</param>
|
|
/// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.Timeout"/>.</returns>
|
|
public static ProtocolStatus Timeout(string message)
|
|
{
|
|
return new ProtocolStatus
|
|
{
|
|
Code = ProtocolStatusCode.Timeout,
|
|
Message = message,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a Canceled protocol status.
|
|
/// </summary>
|
|
/// <param name="message">Status message.</param>
|
|
/// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.Canceled"/>.</returns>
|
|
public static ProtocolStatus Canceled(string message)
|
|
{
|
|
return new ProtocolStatus
|
|
{
|
|
Code = ProtocolStatusCode.Canceled,
|
|
Message = message,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a ProtocolViolation protocol status.
|
|
/// </summary>
|
|
/// <param name="message">Status message.</param>
|
|
/// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.ProtocolViolation"/>.</returns>
|
|
public static ProtocolStatus ProtocolViolation(string message)
|
|
{
|
|
return new ProtocolStatus
|
|
{
|
|
Code = ProtocolStatusCode.ProtocolViolation,
|
|
Message = message,
|
|
};
|
|
}
|
|
}
|