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