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.
}
}
}