namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// /// Shared capped-exponential backoff for poll-based / lazy-reconnect drivers, extracted from /// the S7 driver's bespoke poll fork (05/STAB-8, CONV-1). Provides two surfaces: /// /// — the pure schedule (base, 2×, 4×, … saturating at a /// cap) used by PollGroupEngine to stretch its retry cadence under sustained /// failure. /// The instance API ( / / /// ) — a per-device connect-attempt throttle so a dead device /// is not hammered with a full reconnect on every data call / poll tick. /// /// /// /// /// Not internally synchronized. The instance methods mutate plain fields; every /// intended call site already serializes on its per-device gate (the driver's /// ConnectGate / connect semaphore / probe lock), so adding lock-free interlocked /// state would be needless complexity. Callers MUST invoke the instance under that gate. /// /// /// is pure + thread-safe and may be called from anywhere. /// /// public sealed class ConnectionBackoff { private readonly TimeSpan _baseDelay; private readonly TimeSpan _maxDelay; private int _consecutiveFailures; private DateTime _nextAttemptUtc = DateTime.MinValue; /// Initializes a new per-device attempt throttle. /// The initial backoff window after the first failure. /// The upper bound the exponential growth saturates at. public ConnectionBackoff(TimeSpan baseDelay, TimeSpan maxDelay) { _baseDelay = baseDelay; _maxDelay = maxDelay; } /// /// Capped exponential backoff schedule. ≤ 0 returns /// ; each subsequent failure doubles the wait up to /// . Computed in ticks with an overflow guard so a large failure /// count saturates at the cap rather than wrapping negative. Ported verbatim from the S7 /// poll fork's ComputeBackoffDelay. /// /// The base interval (returned when there are no failures). /// The number of consecutive failures observed. /// The maximum delay the schedule saturates at. /// The computed backoff delay. public static TimeSpan ComputeDelay(TimeSpan baseInterval, int consecutiveFailures, TimeSpan cap) { if (consecutiveFailures <= 0) return baseInterval; // Cap the shift to avoid overflow — at 30 the result already saturates the cap for any // reasonable base interval. var shift = Math.Min(consecutiveFailures - 1, 30); var ticks = baseInterval.Ticks << shift; if (ticks <= 0 || ticks > cap.Ticks) return cap; return TimeSpan.FromTicks(ticks); } /// /// Returns true when is at or past the end of the current /// backoff window (i.e. a connect / create attempt is due). A fresh instance and a /// just-reset instance always return true. /// /// The current UTC time. /// true when an attempt is permitted. public bool ShouldAttempt(DateTime nowUtc) => nowUtc >= _nextAttemptUtc; /// /// Records a failed attempt: increments the consecutive-failure count and opens a backoff /// window ending from . /// /// The current UTC time (the window is measured from here). public void RecordFailure(DateTime nowUtc) { _consecutiveFailures++; _nextAttemptUtc = nowUtc + ComputeDelay(_baseDelay, _consecutiveFailures, _maxDelay); } /// /// Records a successful attempt: resets the failure count and clears the window so the next /// attempt is permitted immediately. Recovery is never delayed by a residual backoff window. /// public void RecordSuccess() { _consecutiveFailures = 0; _nextAttemptUtc = DateTime.MinValue; } }