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