/// <summary> /// Executes an operation with retries. /// </summary> /// <param name="operationToAttempt">The operation, cannot be null.</param> /// <param name="errorDetectionStrategy">The error detection strategy to use instead of the default one.</param> /// <param name="notificationPolicy">The notification policy to use.</param> /// <exception cref="ArgumentNullException">If some of the non-nullable arguments are null.</exception> /// <exception cref="Exception">Any exception thrown by the <paramref name="operationToAttempt"/> after all the attempts have been exhausted.</exception> public void ExecuteWithRetries(Action operationToAttempt, ITransientErrorDetectionStrategy errorDetectionStrategy = null, IRetryNotificationPolicy notificationPolicy = null) { // Check the pre-conditions Guard.ArgumentNotNull(operationToAttempt, nameof(operationToAttempt)); errorDetectionStrategy = errorDetectionStrategy ?? DefaultErrorDetectionStrategy; for (var attemptCount = 0; attemptCount < MaxAttempts; ++attemptCount) { try { // Execute the operation operationToAttempt(); // We are good to go return; } catch (Exception ex) { // Check if it is transient exception, we always treat timeout exceptions as transient bool isTransient = IsTimeoutException(ex) || errorDetectionStrategy.IsTransientException(ex); bool shouldRetry = ShouldRetry(attemptCount, isTransient); // Notify the caller if we are requested to do so if (notificationPolicy != null) { notificationPolicy.OnException(shouldRetry, ex); } if (!shouldRetry) { throw; } } if (!UseFastRetriesForTesting) { // Compute the delay and wait var delay = WaitingPolicy.ComputeWaitTime(attemptCount); Thread.Sleep(delay); } } // for throw new InvalidOperationException("This cannot be reached"); }
/// <summary> /// Executes an async operation (action with no result) with retries. /// </summary> /// <param name="operationToAttempt">The operation, cannot be null.</param> /// <param name="timeout">Optional timeout for the operation for single retry iteration, can be TimeSpan.Zero or Timeout.Infinite which means no timeout</param> /// <param name="cancellationToken">Optional token to cancel the waiting policy, operation and notification.</param> /// <param name="errorDetectionStrategy">Optional error detection strategy to use instead of the default one.</param> /// <param name="notificationPolicy">Optional notification policy to use.</param> /// <returns>The future for the result of the operation.</returns> /// <exception cref="ArgumentNullException">If some of the non-nullable arguments are null.</exception> /// <exception cref="TimeoutException">If the operation was timeout in all retries.</exception> /// <exception cref="OperationCanceledException">If the operation was cancelled.</exception> /// <exception cref="Exception">Any exception thrown by the <paramref name="operationToAttempt"/> after all the attempts have been exhausted.</exception> public async Task ExecuteWithRetriesAsync( Func <CancellationToken, Task> operationToAttempt, TimeSpan timeout = default(TimeSpan), CancellationToken cancellationToken = default(CancellationToken), ITransientErrorDetectionStrategy errorDetectionStrategy = null, IRetryNotificationPolicyAsync notificationPolicy = null) { // Check the pre-conditions Guard.ArgumentNotNull(operationToAttempt, nameof(operationToAttempt)); Guard.ArgumentNotNegativeValue(timeout.Ticks, nameof(timeout)); errorDetectionStrategy = errorDetectionStrategy ?? DefaultErrorDetectionStrategy; for (var attemptCount = 0; attemptCount < MaxAttempts; ++attemptCount) { cancellationToken.ThrowIfCancellationRequested(); // Construct timeout and cancellation linked token - it's important to timely Dispose // the "timeout" cancellation source (as well), because internally, it captures the // (current) thread's (CLR) execution context (synchronization context, logical call // context, etc.), so everything "tied" to these structures (like logging context, for // example, "chained" to a CallContext slot) would have a strong reference and will not // be eligible for garbage collection (note that these are managed resources!). These // would be released eventually when the timeout expires, but we should not rely on that // (if timeout passed in is considerable and the service is under pressure, we may easily // run out of memory). using (var timeoutCts = !IsNoTimeout(timeout) ? new CancellationTokenSource(timeout) : null) { using (var timeoutAndCancellationSource = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken, timeoutCts == null ? CancellationToken.None : timeoutCts.Token)) { var timeoutAndCancellationToken = timeoutAndCancellationSource.Token; try { // Execute the operation await operationToAttempt(timeoutAndCancellationToken).ConfigureAwait(false); // We are good to go return; } catch (Exception ex) { // cancellationToken is set should bail out cancellationToken.ThrowIfCancellationRequested(); // Check if it is transient exception, we always treat timeout exceptions as transient bool isTransient = IsTimeoutException(ex) || errorDetectionStrategy.IsTransientException(ex); bool shouldRetry = ShouldRetry(attemptCount, isTransient); // Notify the caller if we are requested to do so if (notificationPolicy != null) { await notificationPolicy.OnExceptionAsync(shouldRetry, ex, cancellationToken); } if (!shouldRetry) { if (ex is OperationCanceledException) // We wrap OperationCanceledException/TaskCanceledException in TimeoutException to indicate that operation was timed out { throw new TimeoutException($"The operation has timed out after all {MaxAttempts} attempts. Each attempt took more than ${timeout.Milliseconds}ms.", ex); } else { throw; } } } } // using } // using if (!UseFastRetriesForTesting) { // Compute the delay and wait var delay = WaitingPolicy.ComputeWaitTime(attemptCount); await Task.Delay(delay, cancellationToken).ConfigureAwait(false); } } // for throw new InvalidOperationException("This cannot be reached"); }