Files
CBDDC/src/ZB.MOM.WW.CBDDC.Core/Resilience/RetryPolicy.cs
Joseph Doherty bd10914828
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m17s
Harden Surreal migration with retry/coverage fixes and XML docs cleanup
2026-02-22 05:39:00 -05:00

117 lines
4.5 KiB
C#
Executable File

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;
/// <summary>
/// Provides retry logic for transient failures.
/// </summary>
public class RetryPolicy : IRetryPolicy
{
private readonly ILogger<RetryPolicy> _logger;
private readonly IPeerNodeConfigurationProvider _peerNodeConfigurationProvider;
/// <summary>
/// Initializes a new instance of the <see cref="RetryPolicy" /> class.
/// </summary>
/// <param name="peerNodeConfigurationProvider">The provider for retry configuration values.</param>
/// <param name="logger">The logger instance.</param>
public RetryPolicy(IPeerNodeConfigurationProvider peerNodeConfigurationProvider,
ILogger<RetryPolicy>? logger = null)
{
_logger = logger ?? NullLogger<RetryPolicy>.Instance;
_peerNodeConfigurationProvider = peerNodeConfigurationProvider
?? throw new ArgumentNullException(nameof(peerNodeConfigurationProvider));
}
/// <summary>
/// Executes an operation with retry logic.
/// </summary>
/// <typeparam name="T">The result type returned by the operation.</typeparam>
/// <param name="operation">The asynchronous operation to execute.</param>
/// <param name="operationName">The operation name used for logging.</param>
/// <param name="cancellationToken">A token used to cancel retry delays.</param>
public async Task<T> ExecuteAsync<T>(
Func<Task<T>> 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!);
}
/// <summary>
/// Executes an operation with retry logic (void return).
/// </summary>
/// <param name="operation">The asynchronous operation to execute.</param>
/// <param name="operationName">The operation name used for logging.</param>
/// <param name="cancellationToken">A token used to cancel retry delays.</param>
public async Task ExecuteAsync(
Func<Task> 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;
}
}