Added Retry Policy and Launcher Error.
Included ErrorHelper for mapping.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
namespace AlayaCore.Abstractions.Interfaces.Policies
|
||||
{
|
||||
public class IRetryPolicy
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
58
AlayaCore/Clients/DefaultHttpClient.cs
Normal file
58
AlayaCore/Clients/DefaultHttpClient.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AlayaCore.Abstractions.Interfaces.Clients;
|
||||
|
||||
namespace AlayaCore.Clients
|
||||
{
|
||||
public sealed class DefaultHttpClient : IHttpClient, IDisposable
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private bool _disposed;
|
||||
|
||||
public DefaultHttpClient(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> GetAsync(
|
||||
Uri requestUri,
|
||||
HttpCompletionOption completionOption,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (requestUri == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(requestUri));
|
||||
}
|
||||
|
||||
if (!requestUri.IsAbsoluteUri)
|
||||
{
|
||||
throw new ArgumentException("Request URI must be absolute.", nameof(requestUri));
|
||||
}
|
||||
|
||||
ThrowIfDisposed();
|
||||
|
||||
return _httpClient.GetAsync(requestUri, completionOption, cancellationToken);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_httpClient.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(DefaultHttpClient));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
AlayaCore/Errors/LauncherError.cs
Normal file
7
AlayaCore/Errors/LauncherError.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace AlayaCore.Errors
|
||||
{
|
||||
public class LauncherError
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
7
AlayaCore/Models/Configuration/RetryPolicyOptions.cs
Normal file
7
AlayaCore/Models/Configuration/RetryPolicyOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace AlayaCore.Models.Configuration
|
||||
{
|
||||
public class RetryPolicyOptions
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
167
AlayaCore/Policies/RetryPolicy.cs
Normal file
167
AlayaCore/Policies/RetryPolicy.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AlayaCore.Abstractions.Interfaces.Policies;
|
||||
using AlayaCore.Models.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AlayaCore.Models.Policies
|
||||
{
|
||||
public sealed class RetryPolicy : IRetryPolicy
|
||||
{
|
||||
private readonly RetryPolicyOptions _options;
|
||||
private readonly ILogger<RetryPolicy> _logger;
|
||||
private readonly Random _random;
|
||||
|
||||
public RetryPolicy(
|
||||
RetryPolicyOptions options,
|
||||
ILogger<RetryPolicy> logger)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_random = new Random();
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(
|
||||
Func<CancellationToken, Task> operation,
|
||||
string operationName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (operation == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(operation));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(operationName))
|
||||
{
|
||||
throw new ArgumentException("Operation name cannot be null, empty, or whitespace.", nameof(operationName));
|
||||
}
|
||||
|
||||
await ExecuteAsync<object?>(
|
||||
async token =>
|
||||
{
|
||||
await operation(token).ConfigureAwait(false);
|
||||
return null;
|
||||
},
|
||||
operationName,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<T> ExecuteAsync<T>(
|
||||
Func<CancellationToken, Task<T>> operation,
|
||||
string operationName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (operation == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(operation));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(operationName))
|
||||
{
|
||||
throw new ArgumentException("Operation name cannot be null, empty, or whitespace.", nameof(operationName));
|
||||
}
|
||||
|
||||
if (_options.MaxAttempts <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("RetryPolicyOptions.MaxAttempts must be greater than zero.");
|
||||
}
|
||||
|
||||
Exception? lastException = null;
|
||||
|
||||
for (int attempt = 1; attempt <= _options.MaxAttempts; attempt++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
if (attempt > 1)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Retrying operation {OperationName}. Attempt {Attempt} of {MaxAttempts}.",
|
||||
operationName,
|
||||
attempt,
|
||||
_options.MaxAttempts);
|
||||
}
|
||||
|
||||
return await operation(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Operation {OperationName} was cancelled during attempt {Attempt}.",
|
||||
operationName,
|
||||
attempt);
|
||||
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex) when (IsRetryable(ex))
|
||||
{
|
||||
lastException = ex;
|
||||
|
||||
if (attempt == _options.MaxAttempts)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Operation {OperationName} failed after {MaxAttempts} attempts.",
|
||||
operationName,
|
||||
_options.MaxAttempts);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
TimeSpan delay = CalculateDelay(attempt);
|
||||
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Operation {OperationName} failed with a transient error on attempt {Attempt} of {MaxAttempts}. Retrying after {DelayMs}ms.",
|
||||
operationName,
|
||||
attempt,
|
||||
_options.MaxAttempts,
|
||||
(int)delay.TotalMilliseconds);
|
||||
|
||||
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Operation {OperationName} failed with a non-retryable error on attempt {Attempt}.",
|
||||
operationName,
|
||||
attempt);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Retry policy exited unexpectedly for operation '{operationName}'.",
|
||||
lastException);
|
||||
}
|
||||
|
||||
private bool IsRetryable(Exception exception)
|
||||
{
|
||||
return exception switch
|
||||
{
|
||||
HttpRequestException => true,
|
||||
IOException => true,
|
||||
TaskCanceledException => true,
|
||||
InvalidDataException => false,
|
||||
ArgumentException => false,
|
||||
InvalidOperationException => false,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private TimeSpan CalculateDelay(int attempt)
|
||||
{
|
||||
double exponentialDelay = _options.BaseDelayMilliseconds * Math.Pow(_options.BackoffMultiplier, attempt - 1);
|
||||
double cappedDelay = Math.Min(exponentialDelay, _options.MaxDelayMilliseconds);
|
||||
|
||||
int jitter = _random.Next(0, 150);
|
||||
return TimeSpan.FromMilliseconds(cappedDelay + jitter);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
AlayaCore/Utilities/Enums/LauncherErrorType.cs
Normal file
7
AlayaCore/Utilities/Enums/LauncherErrorType.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace AlayaCore.Utilities.Enums
|
||||
{
|
||||
public enum LauncherErrorType
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
7
AlayaCore/Utilities/Helpers/ErrorHelper.cs
Normal file
7
AlayaCore/Utilities/Helpers/ErrorHelper.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace AlayaCore.Utilities.Helpers
|
||||
{
|
||||
public class ErrorHelper
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
7
AlayaCore/Utilities/Helpers/OptionsHelper.cs
Normal file
7
AlayaCore/Utilities/Helpers/OptionsHelper.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace AlayaCore.Utilities.Helpers
|
||||
{
|
||||
public class OptionsHelper
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user