using System; namespace LaunchDarkly.EventSource { internal sealed class ExponentialBackoffWithDecorrelation { private readonly object _lock = new object(); private readonly Random _randomSource; private TimeSpan _minimumDelay; private TimeSpan _maximumDelay; private int _backoffN; private TimeSpan? _serverDirectedMinDelay; public ExponentialBackoffWithDecorrelation(TimeSpan minimumDelay, TimeSpan maximumDelay) : this(minimumDelay, maximumDelay, new Random()) { } /// /// Constructs an instance with a caller-supplied , so that tests can /// make jitter deterministic. /// internal ExponentialBackoffWithDecorrelation(TimeSpan minimumDelay, TimeSpan maximumDelay, Random randomSource) { _minimumDelay = NonNegative(minimumDelay); _maximumDelay = NonNegative(maximumDelay); _randomSource = randomSource ?? new Random(); } /// /// Gets the next backoff duration, advancing n. /// public TimeSpan GetNextBackOff() { lock (_lock) { long unjittered = GetUnjitteredMillisecondsForN(_backoffN); // Random.Next takes an int bound. 2^31 milliseconds is far longer than any // reconnect delay we would use, so saturating here cannot affect a realistic // delay. int jitterBound = (int)Math.Min(unjittered, int.MaxValue); long delayMillis = unjittered / 2 + (jitterBound > 0 ? _randomSource.Next(jitterBound) / 2 : 0); _backoffN++; return TimeSpan.FromMilliseconds(delayMillis); } } /// /// Returns the delay in whole milliseconds for a given backoff n, before jitter is /// applied: minimumDelay * 2^n, limited to the maximum. /// /// /// /// The caller must hold the lock. This reads the bounds without taking it because it is the /// inner core of , which already holds it; taking it here would /// only be a re-entrant acquisition on the hot path. Anything needing a synchronized read of /// the bounds on its own should use the accessors below, which take the lock themselves. /// /// /// n is compared against the ceiling rather than used to compute a value that is /// then clamped, so no intermediate ever exceeds the maximum. That matters because /// n is unbounded: a long outage keeps incrementing it, and computing /// minimumDelay * 2^n directly would overflow. /// /// internal long GetUnjitteredMillisecondsForN(int n) { long min = (_serverDirectedMinDelay ?? _minimumDelay).Ticks / TimeSpan.TicksPerMillisecond; long max = _maximumDelay.Ticks / TimeSpan.TicksPerMillisecond; if (min <= 0 || max <= 0) { return 0; } if (n <= 0) { return min > max ? max : min; } // Shifting the ceiling down cannot overflow, whereas shifting the minimum up can. // The n >= 63 test is required rather than defensive: C# masks a 64-bit shift count // to its low 6 bits, so `max >> 64` would silently mean `max >> 0`. if (n >= 63 || min > (max >> n)) { return max; } return min << n; } /// /// Applies a server-directed reconnection time received via the SSE retry: field, /// replacing the minimum delay used to compute subsequent backoffs. /// /// /// /// The override takes precedence over both the configured minimum and any minimum /// installed by . It is sticky: it survives bounds changes and /// , and is replaced only by a later server-directed value. /// /// /// The maximum is never affected, so the ceiling that bounds reconnection keeps applying. /// /// /// n resets, so the next attempt uses the directed value itself rather than continuing to /// double from wherever the progression had reached. /// /// /// Negative values become zero. No upper bound is applied here; a caller taking a value /// from an untrusted source is responsible for capping it first. /// /// /// the server-directed reconnection time public void SetServerDirectedMinDelay(TimeSpan minDelay) { var value = NonNegative(minDelay); lock (_lock) { _serverDirectedMinDelay = value; _backoffN = 0; } } /// /// Replaces the delay bounds. /// /// /// If the new bounds differ from the current ones, n resets so that the /// next delay starts from the new minimum. If they are the same, this does nothing, which /// makes repeated calls with identical values idempotent -- a caller that reapplies the /// same bounds on every failure will not pin the delay at the minimum. /// /// the new minimum delay; negative values become zero /// the new maximum delay; negative values become zero public void SetBounds(TimeSpan minimumDelay, TimeSpan maximumDelay) { var min = NonNegative(minimumDelay); var max = NonNegative(maximumDelay); lock (_lock) { if (min == _minimumDelay && max == _maximumDelay) { return; } _minimumDelay = min; _maximumDelay = max; _backoffN = 0; } } /// /// Resets n so the next delay starts from the current minimum, without /// changing the bounds. /// public void ResetBackoffN() { lock (_lock) { _backoffN = 0; } } // Accessors for tests. internal int GetBackoffN() { lock (_lock) { return _backoffN; } } internal TimeSpan? GetServerDirectedMinDelay() { lock (_lock) { return _serverDirectedMinDelay; } } internal TimeSpan GetMinimumDelay() { lock (_lock) { return _minimumDelay; } } internal TimeSpan GetMaximumDelay() { lock (_lock) { return _maximumDelay; } } // Matches the clamping behavior of ConfigurationBuilder's retry-delay setters: a negative // duration becomes zero rather than throwing. private static TimeSpan NonNegative(TimeSpan t) => t < TimeSpan.Zero ? TimeSpan.Zero : t; } }