| | | 1 | | namespace AsyncResponse; |
| | | 2 | | |
| | | 3 | | internal static class AsyncResponseRetry |
| | | 4 | | { |
| | | 5 | | /// <summary>Runs this background operation until cancellation is requested.</summary> |
| | | 6 | | public static async Task<T> ExecuteAsync<T>( |
| | | 7 | | Func<CancellationToken, Task<T>> action, |
| | | 8 | | Func<Exception, bool> isTransient, |
| | | 9 | | int maxAttempts, |
| | | 10 | | TimeSpan baseDelay, |
| | | 11 | | TimeSpan maxDelay, |
| | | 12 | | CancellationToken cancellationToken) |
| | | 13 | | { |
| | 3 | 14 | | ArgumentNullException.ThrowIfNull(action); |
| | 3 | 15 | | ArgumentNullException.ThrowIfNull(isTransient); |
| | 3 | 16 | | ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxAttempts); |
| | | 17 | | |
| | 3 | 18 | | var attempt = 0; |
| | | 19 | | while (true) |
| | | 20 | | { |
| | 3 | 21 | | cancellationToken.ThrowIfCancellationRequested(); |
| | 3 | 22 | | attempt++; |
| | | 23 | | |
| | | 24 | | try |
| | | 25 | | { |
| | 3 | 26 | | return await action(cancellationToken).ConfigureAwait(false); |
| | | 27 | | } |
| | 3 | 28 | | catch (Exception ex) when (attempt < maxAttempts && isTransient(ex)) |
| | | 29 | | { |
| | 3 | 30 | | await Task.Delay(Backoff(attempt, baseDelay, maxDelay), cancellationToken).ConfigureAwait(false); |
| | 3 | 31 | | } |
| | | 32 | | } |
| | 3 | 33 | | } |
| | | 34 | | |
| | | 35 | | /// <summary>Computes the retry backoff delay: exponential with half-jitter.</summary> |
| | | 36 | | public static TimeSpan Backoff(int completedAttempts, TimeSpan baseDelay, TimeSpan maxDelay) |
| | | 37 | | { |
| | 3 | 38 | | var multiplier = 1 << Math.Min(Math.Max(completedAttempts, 1) - 1, 10); |
| | 3 | 39 | | var milliseconds = Math.Min(maxDelay.TotalMilliseconds, baseDelay.TotalMilliseconds * multiplier); |
| | | 40 | | |
| | | 41 | | // Half-jitter: keep at least half the exponential step so backoff still backs off, and |
| | | 42 | | // randomize the rest — a broker blip fails many waiters across many replicas at once, and |
| | | 43 | | // un-jittered exponential delays would send them all reconnecting in lockstep waves. |
| | 3 | 44 | | milliseconds = milliseconds / 2 + Random.Shared.NextDouble() * (milliseconds / 2); |
| | 3 | 45 | | return TimeSpan.FromMilliseconds(Math.Max(1, milliseconds)); |
| | | 46 | | } |
| | | 47 | | } |