using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.Stability; /// /// Demand-aware driver-wedge detector per docs/v2/plan.md decision #147. /// Flips a driver to only when BOTH of the following hold: /// (a) there is pending work outstanding, AND (b) no progress has been observed for longer /// than . Idle drivers, write-only burst drivers, and subscription-only /// drivers whose signals don't arrive regularly all stay Healthy. /// /// /// Pending work signal is supplied by the caller via : /// non-zero Polly bulkhead depth, ≥1 active MonitoredItem, or ≥1 queued historian read /// each qualifies. The detector itself is state-light: all it remembers is the last /// LastProgressUtc it saw and the last wedge verdict. No history buffer. /// /// Default threshold per plan: 5 × PublishingInterval, with a minimum of 60 s. /// Concrete values are driver-agnostic and configured per-instance by the caller. /// public sealed class WedgeDetector { /// Wedge-detection threshold; pass < 60 s and the detector clamps to 60 s. public TimeSpan Threshold { get; } /// Whether the driver reported itself at construction. public WedgeDetector(TimeSpan threshold) { Threshold = threshold < TimeSpan.FromSeconds(60) ? TimeSpan.FromSeconds(60) : threshold; } /// /// Classify the current state against the demand signal. Does not retain state across /// calls — each call is self-contained; the caller owns the LastProgressUtc clock. /// public WedgeVerdict Classify(DriverState state, DemandSignal demand, DateTime utcNow) { if (state != DriverState.Healthy) return WedgeVerdict.NotApplicable; if (!demand.HasPendingWork) return WedgeVerdict.Idle; var sinceProgress = utcNow - demand.LastProgressUtc; return sinceProgress > Threshold ? WedgeVerdict.Faulted : WedgeVerdict.Healthy; } } /// /// Caller-supplied demand snapshot. All three counters are OR'd — any non-zero means work /// is outstanding, which is the trigger for checking the clock. /// /// Polly bulkhead depth (in-flight capability calls). /// Number of live OPC UA MonitoredItems bound to this driver. /// Pending historian-read requests the driver owes the server. /// Last time the driver reported a successful unit of work (read, subscribe-ack, publish). public readonly record struct DemandSignal( int BulkheadDepth, int ActiveMonitoredItems, int QueuedHistoryReads, DateTime LastProgressUtc) { /// True when any of the three counters is > 0. public bool HasPendingWork => BulkheadDepth > 0 || ActiveMonitoredItems > 0 || QueuedHistoryReads > 0; } /// Outcome of a single call. public enum WedgeVerdict { /// Driver wasn't Healthy to begin with — wedge detection doesn't apply. NotApplicable, /// Driver claims Healthy + no pending work → stays Healthy. Idle, /// Driver claims Healthy + has pending work + has made progress within the threshold → stays Healthy. Healthy, /// Driver claims Healthy + has pending work + has NOT made progress within the threshold → wedged. Faulted, }