using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.CBDDC.Core.Exceptions;
using ZB.MOM.WW.CBDDC.Core.Network;
using TimeoutException = ZB.MOM.WW.CBDDC.Core.Exceptions.TimeoutException;
namespace ZB.MOM.WW.CBDDC.Core.Resilience;
///
/// Provides retry logic for transient failures.
///
public class RetryPolicy : IRetryPolicy
{
private readonly ILogger _logger;
private readonly IPeerNodeConfigurationProvider _peerNodeConfigurationProvider;
///
/// Initializes a new instance of the class.
///
/// The provider for retry configuration values.
/// The logger instance.
public RetryPolicy(IPeerNodeConfigurationProvider peerNodeConfigurationProvider,
ILogger? logger = null)
{
_logger = logger ?? NullLogger.Instance;
_peerNodeConfigurationProvider = peerNodeConfigurationProvider
?? throw new ArgumentNullException(nameof(peerNodeConfigurationProvider));
}
///
/// Executes an operation with retry logic.
///
/// The result type returned by the operation.
/// The asynchronous operation to execute.
/// The operation name used for logging.
/// A token used to cancel retry delays.
public async Task ExecuteAsync(
Func> operation,
string operationName,
CancellationToken cancellationToken = default)
{
var config = await _peerNodeConfigurationProvider.GetConfiguration();
Exception? lastException = null;
for (var attempt = 1; attempt <= config.RetryAttempts; attempt++)
try
{
_logger.LogDebug("Executing {Operation} (attempt {Attempt}/{Max})",
operationName, attempt, config.RetryAttempts);
return await operation();
}
catch (Exception ex) when (IsTransient(ex))
{
lastException = ex;
if (attempt >= config.RetryAttempts) break;
int delay = config.RetryDelayMs * attempt; // Exponential backoff
_logger.LogWarning(ex,
"Operation {Operation} failed (attempt {Attempt}/{Max}). Retrying in {Delay}ms...",
operationName, attempt, config.RetryAttempts, delay);
await Task.Delay(delay, cancellationToken);
}
if (lastException != null)
_logger.LogError(lastException,
"Operation {Operation} failed after {Attempts} attempts",
operationName, config.RetryAttempts);
else
_logger.LogError(
"Operation {Operation} failed after {Attempts} attempts",
operationName, config.RetryAttempts);
throw new CBDDCException("RETRY_EXHAUSTED",
$"Operation '{operationName}' failed after {config.RetryAttempts} attempts",
lastException!);
}
///
/// Executes an operation with retry logic (void return).
///
/// The asynchronous operation to execute.
/// The operation name used for logging.
/// A token used to cancel retry delays.
public async Task ExecuteAsync(
Func operation,
string operationName,
CancellationToken cancellationToken = default)
{
await ExecuteAsync(async () =>
{
await operation();
return true;
}, operationName, cancellationToken);
}
private static bool IsTransient(Exception ex)
{
// Network errors are typically transient
if (ex is NetworkException or SocketException or IOException)
return true;
// Timeout errors are transient
if (ex is TimeoutException or OperationCanceledException)
return true;
return false;
}
}