Files
lmxopcua/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ConnectionBackoff.cs
T

94 lines
4.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// 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:
/// <list type="bullet">
/// <item><see cref="ComputeDelay"/> — the pure schedule (base, 2×, 4×, … saturating at a
/// cap) used by <c>PollGroupEngine</c> to stretch its retry cadence under sustained
/// failure.</item>
/// <item>The instance API (<see cref="ShouldAttempt"/> / <see cref="RecordFailure"/> /
/// <see cref="RecordSuccess"/>) — a per-device connect-attempt throttle so a dead device
/// is not hammered with a full reconnect on every data call / poll tick.</item>
/// </list>
/// </summary>
/// <remarks>
/// <para>
/// <b>Not internally synchronized.</b> The instance methods mutate plain fields; every
/// intended call site already serializes on its per-device gate (the driver's
/// <c>ConnectGate</c> / connect semaphore / probe lock), so adding lock-free interlocked
/// state would be needless complexity. Callers MUST invoke the instance under that gate.
/// </para>
/// <para>
/// <see cref="ComputeDelay"/> is pure + thread-safe and may be called from anywhere.
/// </para>
/// </remarks>
public sealed class ConnectionBackoff
{
private readonly TimeSpan _baseDelay;
private readonly TimeSpan _maxDelay;
private int _consecutiveFailures;
private DateTime _nextAttemptUtc = DateTime.MinValue;
/// <summary>Initializes a new per-device attempt throttle.</summary>
/// <param name="baseDelay">The initial backoff window after the first failure.</param>
/// <param name="maxDelay">The upper bound the exponential growth saturates at.</param>
public ConnectionBackoff(TimeSpan baseDelay, TimeSpan maxDelay)
{
_baseDelay = baseDelay;
_maxDelay = maxDelay;
}
/// <summary>
/// Capped exponential backoff schedule. <paramref name="consecutiveFailures"/> ≤ 0 returns
/// <paramref name="baseInterval"/>; each subsequent failure doubles the wait up to
/// <paramref name="cap"/>. 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 <c>ComputeBackoffDelay</c>.
/// </summary>
/// <param name="baseInterval">The base interval (returned when there are no failures).</param>
/// <param name="consecutiveFailures">The number of consecutive failures observed.</param>
/// <param name="cap">The maximum delay the schedule saturates at.</param>
/// <returns>The computed backoff delay.</returns>
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);
}
/// <summary>
/// Returns <c>true</c> when <paramref name="nowUtc"/> 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 <c>true</c>.
/// </summary>
/// <param name="nowUtc">The current UTC time.</param>
/// <returns><c>true</c> when an attempt is permitted.</returns>
public bool ShouldAttempt(DateTime nowUtc) => nowUtc >= _nextAttemptUtc;
/// <summary>
/// Records a failed attempt: increments the consecutive-failure count and opens a backoff
/// window ending <see cref="ComputeDelay"/> from <paramref name="nowUtc"/>.
/// </summary>
/// <param name="nowUtc">The current UTC time (the window is measured from here).</param>
public void RecordFailure(DateTime nowUtc)
{
_consecutiveFailures++;
_nextAttemptUtc = nowUtc + ComputeDelay(_baseDelay, _consecutiveFailures, _maxDelay);
}
/// <summary>
/// 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.
/// </summary>
public void RecordSuccess()
{
_consecutiveFailures = 0;
_nextAttemptUtc = DateTime.MinValue;
}
}