Files
EonaCat.SmartRetry/EonaCat.SmartRetry/EonaCat.SmartRetry/SmartRetryPolicy.cs
T
2025-12-15 21:05:21 +01:00

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();
}
}
}