207 lines
6.9 KiB
C#
207 lines
6.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using EonaCat.SmartRetry.Exceptions;
|
|
|
|
namespace EonaCat.SmartRetry
|
|
{
|
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
|
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
|
|
|
public class SmartRetryPolicy : ISmartRetryPolicy
|
|
{
|
|
private readonly List<Type> _handledExceptions = new();
|
|
private readonly RetryStrategy _strategy = new();
|
|
private CircuitBreaker _circuitBreaker;
|
|
private Delegate _fallback;
|
|
private Delegate _fallbackAsync;
|
|
private Func<int, Exception, Task> _onRetryAsync;
|
|
private Action<int, Exception> _onRetry;
|
|
private Func<object, bool> _retryCondition;
|
|
private readonly Random _jitterGenerator = new();
|
|
|
|
public Action OnCircuitOpenAction { get; set; }
|
|
public Action OnCircuitCloseAction { get; set; }
|
|
|
|
public static ISmartRetryPolicy Policy() => new SmartRetryPolicy();
|
|
|
|
public ISmartRetryPolicy Handle<TException>() where TException : Exception
|
|
{
|
|
_handledExceptions.Add(typeof(TException));
|
|
return this;
|
|
}
|
|
|
|
public ISmartRetryPolicy Retry(int retryCount)
|
|
{
|
|
_strategy.MaxRetries = retryCount;
|
|
return this;
|
|
}
|
|
|
|
public ISmartRetryPolicy WithExponentialBackoff(double initialDelaySeconds = 1.0, double multiplier = 2.0)
|
|
{
|
|
_strategy.BackoffType = BackoffStrategyType.Exponential;
|
|
_strategy.InitialDelaySeconds = initialDelaySeconds;
|
|
_strategy.BackoffMultiplier = multiplier;
|
|
return this;
|
|
}
|
|
|
|
public ISmartRetryPolicy WithLinearBackoff(double delaySeconds = 1.0)
|
|
{
|
|
_strategy.BackoffType = BackoffStrategyType.Linear;
|
|
_strategy.InitialDelaySeconds = delaySeconds;
|
|
return this;
|
|
}
|
|
|
|
public ISmartRetryPolicy WithCustomBackoff(Func<int, double> delayProvider)
|
|
{
|
|
_strategy.BackoffType = BackoffStrategyType.Custom;
|
|
_strategy.CustomDelayProvider = delayProvider;
|
|
return this;
|
|
}
|
|
|
|
public ISmartRetryPolicy WithJitter(double maxPercentage = 0.3)
|
|
{
|
|
_strategy.JitterMaxPercentage = maxPercentage;
|
|
return this;
|
|
}
|
|
|
|
public ISmartRetryPolicy WithCircuitBreaker(int failureThreshold = 5, TimeSpan? openDuration = null)
|
|
{
|
|
_circuitBreaker = new CircuitBreaker(failureThreshold, openDuration)
|
|
{
|
|
OnOpen = () => OnCircuitOpenAction?.Invoke(),
|
|
OnClose = () => OnCircuitCloseAction?.Invoke()
|
|
};
|
|
return this;
|
|
}
|
|
|
|
public ISmartRetryPolicy WithFallback<T>(Func<T> fallbackAction)
|
|
{
|
|
_fallback = fallbackAction;
|
|
return this;
|
|
}
|
|
|
|
public ISmartRetryPolicy WithFallbackAsync<T>(Func<Task<T>> fallbackAction)
|
|
{
|
|
_fallbackAsync = fallbackAction;
|
|
return this;
|
|
}
|
|
|
|
public ISmartRetryPolicy OnRetry(Action<int, Exception> onRetry)
|
|
{
|
|
_onRetry = onRetry;
|
|
return this;
|
|
}
|
|
|
|
public ISmartRetryPolicy OnRetryAsync(Func<int, Exception, Task> onRetryAsync)
|
|
{
|
|
_onRetryAsync = onRetryAsync;
|
|
return this;
|
|
}
|
|
|
|
public ISmartRetryPolicy OnCircuitOpen(Action onOpen)
|
|
{
|
|
OnCircuitOpenAction = onOpen;
|
|
return this;
|
|
}
|
|
|
|
public ISmartRetryPolicy OnCircuitClose(Action onClose)
|
|
{
|
|
OnCircuitCloseAction = onClose;
|
|
return this;
|
|
}
|
|
|
|
public ISmartRetryPolicy RetryIf<T>(Func<T, bool> shouldRetry)
|
|
{
|
|
_retryCondition = result => shouldRetry((T)result);
|
|
return this;
|
|
}
|
|
|
|
public ISmartRetryPolicy WithMaxRetryDuration(TimeSpan maxDuration)
|
|
{
|
|
_strategy.MaxRetryDuration = maxDuration;
|
|
return this;
|
|
}
|
|
|
|
private async Task DelayWithBackoffAndJitter(int attempt)
|
|
{
|
|
double delay = _strategy.BackoffType switch
|
|
{
|
|
BackoffStrategyType.Linear => _strategy.InitialDelaySeconds,
|
|
BackoffStrategyType.Exponential => _strategy.InitialDelaySeconds * Math.Pow(_strategy.BackoffMultiplier, attempt - 1),
|
|
BackoffStrategyType.Custom => _strategy.CustomDelayProvider?.Invoke(attempt) ?? 0,
|
|
_ => 0
|
|
};
|
|
|
|
double jitter = _strategy.JitterMaxPercentage * delay * _jitterGenerator.NextDouble();
|
|
await Task.Delay(TimeSpan.FromSeconds(delay + jitter));
|
|
}
|
|
|
|
private async Task<T> ExecuteFallbackAsync<T>()
|
|
{
|
|
if (_fallbackAsync != null)
|
|
{
|
|
return await ((Func<Task<T>>)_fallbackAsync)();
|
|
}
|
|
|
|
if (_fallback != null)
|
|
{
|
|
return ((Func<T>)_fallback)();
|
|
}
|
|
|
|
throw new RetryFailedException("All retries failed and no fallback defined.", null);
|
|
}
|
|
|
|
public async Task<T> ExecuteAsync<T>(Func<CancellationToken, Task<T>> action, CancellationToken token = default)
|
|
{
|
|
int attempt = 0;
|
|
DateTime startTime = DateTime.Now;
|
|
|
|
while (true)
|
|
{
|
|
if (_circuitBreaker != null && !_circuitBreaker.CanExecute())
|
|
return await ExecuteFallbackAsync<T>();
|
|
|
|
try
|
|
{
|
|
var result = await action(token);
|
|
|
|
if (_retryCondition != null && _retryCondition(result))
|
|
throw new RetryFailedException("Retry condition met.", null);
|
|
|
|
_circuitBreaker?.OnSuccess();
|
|
return result;
|
|
}
|
|
catch (Exception ex) when (_handledExceptions.Contains(ex.GetType()))
|
|
{
|
|
attempt++;
|
|
_circuitBreaker?.OnFailure();
|
|
|
|
_onRetry?.Invoke(attempt, ex);
|
|
if (_onRetryAsync != null)
|
|
await _onRetryAsync(attempt, ex);
|
|
|
|
if (_strategy.MaxRetryDuration.HasValue && (DateTime.Now - startTime) >= _strategy.MaxRetryDuration)
|
|
return await ExecuteFallbackAsync<T>();
|
|
|
|
if (attempt > _strategy.MaxRetries)
|
|
return await ExecuteFallbackAsync<T>();
|
|
|
|
await DelayWithBackoffAndJitter(attempt);
|
|
}
|
|
}
|
|
}
|
|
|
|
public async Task<T> ExecuteAsync<T>(Func<Task<T>> action)
|
|
{
|
|
return await ExecuteAsync<T>(ct => action(), CancellationToken.None);
|
|
}
|
|
|
|
public T Execute<T>(Func<T> action)
|
|
{
|
|
return ExecuteAsync<T>(() => Task.FromResult(action())).GetAwaiter().GetResult();
|
|
}
|
|
}
|
|
}
|