Added Retry Policy and Launcher Error.

Included ErrorHelper for mapping.
This commit is contained in:
2026-04-06 20:47:00 +01:00
parent 11863088e4
commit 491a3d420d
8 changed files with 267 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
namespace AlayaCore.Abstractions.Interfaces.Policies
{
public class IRetryPolicy
{
}
}

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

View File

@@ -0,0 +1,7 @@
namespace AlayaCore.Errors
{
public class LauncherError
{
}
}

View File

@@ -0,0 +1,7 @@
namespace AlayaCore.Models.Configuration
{
public class RetryPolicyOptions
{
}
}

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

View File

@@ -0,0 +1,7 @@
namespace AlayaCore.Utilities.Enums
{
public enum LauncherErrorType
{
}
}

View File

@@ -0,0 +1,7 @@
namespace AlayaCore.Utilities.Helpers
{
public class ErrorHelper
{
}
}

View File

@@ -0,0 +1,7 @@
namespace AlayaCore.Utilities.Helpers
{
public class OptionsHelper
{
}
}