using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming; using ZB.MOM.WW.ScadaBridge.Communication.Grpc; using ZB.MOM.WW.ScadaBridge.LoadHarness.Metrics; namespace ZB.MOM.WW.ScadaBridge.LoadHarness.Probes; /// /// The central half of the wire-path stream probe: one instance subscription opened /// through the PRODUCTION — the exact /// SubscribeAsync path central's DebugStreamBridgeActor uses, including /// the R2 batch negotiation (BatchingSupported = true) and the client-side /// batch unpack + proto→domain conversion. /// /// /// Latency is recorded in onEvent, i.e. AFTER wire deserialization, batch /// unpacking and domain mapping — the same "DCL boundary → subscriber" measurement as /// , extended across the real socket. The emit /// instant travels verbatim on the proto Timestamp, so the subtraction stays a /// genuine end-to-end figure (same process, same clock). /// /// public sealed class WireStreamSubscriber { private Task _streamTask = Task.CompletedTask; private volatile LatencyHistogram? _latency; private long _received; private long _faults; private long _completions; /// Probe name (also the stream's correlation id). public string Name { get; } /// Domain events delivered by the client callback. public long ReceivedEvents => Interlocked.Read(ref _received); /// Stream faults reported via onError. Expected 0 for a healthy run. public long Faults => Interlocked.Read(ref _faults); /// Graceful server-side completions via onCompleted. Expected 0 inside a run shorter than the 4 h stream lifetime. public long GracefulCompletions => Interlocked.Read(ref _completions); private WireStreamSubscriber(string name, LatencyHistogram? latency) { Name = name; _latency = latency; } /// /// Opens one instance stream on the shared per-site client. The subscription task /// runs until cancels — the harness deliberately does NOT /// reconnect on fault (a fault inside a 25-minute loopback run is a finding, not /// an operational event to paper over), it counts it. /// /// The site's shared production gRPC client (one channel per site, streams multiplexed). /// Instance whose events this probe receives. /// Probe/correlation name (must be a valid Akka path element). /// Histogram fed with end-to-end event latency. /// Progress sink for fault reporting. /// Stops the subscription. /// The attached subscriber. public static WireStreamSubscriber Attach( SiteStreamGrpcClient client, string instanceUniqueName, string name, LatencyHistogram? latency, Action log, CancellationToken ct) { var self = new WireStreamSubscriber(name, latency); self._streamTask = client.SubscribeAsync( correlationId: name, instanceUniqueName: instanceUniqueName, onEvent: evt => { Interlocked.Increment(ref self._received); if (self._latency is { } histogram && evt is AttributeValueChanged changed) histogram.Record(DateTimeOffset.UtcNow - changed.Timestamp); }, onError: ex => { Interlocked.Increment(ref self._faults); log($" wire probe {name} stream fault: {ex.GetType().Name}: {ex.Message}"); }, onCompleted: () => Interlocked.Increment(ref self._completions), ct); return self; } /// Repoints the latency histogram (steady-state window separation) — mirrors . /// The histogram to record into from now on, or null to stop recording. public void RetargetLatency(LatencyHistogram? latency) => _latency = latency; /// Awaits the subscription task after its token has been cancelled. /// A task that completes when the stream loop has exited. public async Task WaitForShutdownAsync() { try { await _streamTask; } catch (OperationCanceledException) { // Normal teardown. } } }