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