MarketAlly.AIPlugin.Extensions/MarketAlly.AIPlugin.Analysis/Infrastructure/ErrorHandling.cs

354 lines
14 KiB
C#
Executable File

using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace MarketAlly.AIPlugin.Analysis.Infrastructure
{
/// <summary>
/// Enhanced error handling utilities for analysis operations
/// </summary>
public static class ErrorHandling
{
/// <summary>
/// Executes an operation with retry logic and comprehensive error handling
/// </summary>
public static async Task<T> ExecuteWithRetryAsync<T>(
Func<Task<T>> operation,
int maxRetries = 3,
TimeSpan? delay = null,
ILogger? logger = null,
CancellationToken cancellationToken = default,
[CallerMemberName] string callerName = "",
[CallerFilePath] string callerFilePath = "",
[CallerLineNumber] int callerLineNumber = 0)
{
var actualDelay = delay ?? TimeSpan.FromSeconds(1);
var exceptions = new List<Exception>();
var stopwatch = Stopwatch.StartNew();
for (int attempt = 0; attempt <= maxRetries; attempt++)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
logger?.LogDebug("Executing operation {OperationName}, attempt {Attempt}/{MaxRetries}",
callerName, attempt + 1, maxRetries + 1);
var result = await operation();
if (attempt > 0)
{
logger?.LogInformation("Operation {OperationName} succeeded after {Attempts} attempts in {Duration}ms",
callerName, attempt + 1, stopwatch.ElapsedMilliseconds);
}
return result;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
logger?.LogWarning("Operation {OperationName} was cancelled after {Attempts} attempts",
callerName, attempt + 1);
throw;
}
catch (Exception ex)
{
exceptions.Add(ex);
logger?.LogWarning(ex, "Operation {OperationName} failed on attempt {Attempt}/{MaxRetries} at {Location}",
callerName, attempt + 1, maxRetries + 1, $"{callerFilePath}:{callerLineNumber}");
if (attempt == maxRetries)
{
logger?.LogError("Operation {OperationName} failed after {Attempts} attempts in {Duration}ms",
callerName, attempt + 1, stopwatch.ElapsedMilliseconds);
throw new AggregateException($"Operation {callerName} failed after {maxRetries + 1} attempts", exceptions);
}
if (ShouldRetry(ex))
{
var nextDelay = CalculateDelay(actualDelay, attempt);
logger?.LogDebug("Retrying operation {OperationName} in {Delay}ms", callerName, nextDelay.TotalMilliseconds);
await Task.Delay(nextDelay, cancellationToken);
}
else
{
logger?.LogError(ex, "Operation {OperationName} failed with non-retryable exception", callerName);
throw;
}
}
}
throw new InvalidOperationException("This should never be reached");
}
/// <summary>
/// Executes an operation with comprehensive error handling (non-generic version)
/// </summary>
public static async Task ExecuteWithRetryAsync(
Func<Task> operation,
int maxRetries = 3,
TimeSpan? delay = null,
ILogger? logger = null,
CancellationToken cancellationToken = default,
[CallerMemberName] string callerName = "",
[CallerFilePath] string callerFilePath = "",
[CallerLineNumber] int callerLineNumber = 0)
{
await ExecuteWithRetryAsync(async () =>
{
await operation();
return true; // Dummy return value
}, maxRetries, delay, logger, cancellationToken, callerName, callerFilePath, callerLineNumber);
}
/// <summary>
/// Safely executes an operation and returns a result with error information
/// </summary>
public static async Task<OperationResult<T>> SafeExecuteAsync<T>(
Func<Task<T>> operation,
ILogger? logger = null,
[CallerMemberName] string callerName = "",
[CallerFilePath] string callerFilePath = "",
[CallerLineNumber] int callerLineNumber = 0)
{
var stopwatch = Stopwatch.StartNew();
try
{
logger?.LogDebug("Starting safe execution of {OperationName}", callerName);
var result = await operation();
logger?.LogDebug("Safe execution of {OperationName} completed successfully in {Duration}ms",
callerName, stopwatch.ElapsedMilliseconds);
return OperationResult<T>.Success(result, stopwatch.Elapsed);
}
catch (Exception ex)
{
logger?.LogError(ex, "Safe execution of {OperationName} failed at {Location} after {Duration}ms",
callerName, $"{callerFilePath}:{callerLineNumber}", stopwatch.ElapsedMilliseconds);
return OperationResult<T>.Failure(ex, stopwatch.Elapsed);
}
}
/// <summary>
/// Creates a timeout wrapper for operations
/// </summary>
public static async Task<T> WithTimeoutAsync<T>(
Func<CancellationToken, Task<T>> operation,
TimeSpan timeout,
ILogger? logger = null,
[CallerMemberName] string callerName = "")
{
using var cts = new CancellationTokenSource(timeout);
try
{
logger?.LogDebug("Starting operation {OperationName} with timeout {Timeout}ms",
callerName, timeout.TotalMilliseconds);
return await operation(cts.Token);
}
catch (OperationCanceledException) when (cts.Token.IsCancellationRequested)
{
logger?.LogWarning("Operation {OperationName} timed out after {Timeout}ms",
callerName, timeout.TotalMilliseconds);
throw new TimeoutException($"Operation {callerName} timed out after {timeout.TotalMilliseconds}ms");
}
}
/// <summary>
/// Handles exceptions from plugin operations with detailed logging
/// </summary>
public static PluginErrorInfo HandlePluginException(
Exception exception,
string pluginName,
string operationName,
ILogger? logger = null)
{
var errorInfo = new PluginErrorInfo
{
PluginName = pluginName,
OperationName = operationName,
Exception = exception,
Timestamp = DateTime.UtcNow,
ErrorType = ClassifyError(exception),
Severity = DetermineSeverity(exception),
Recoverable = IsRecoverable(exception)
};
logger?.Log(GetLogLevel(errorInfo.Severity), exception,
"Plugin {PluginName} failed during {OperationName}: {ErrorType} - {ErrorMessage}",
pluginName, operationName, errorInfo.ErrorType, exception.Message);
return errorInfo;
}
/// <summary>
/// Determines if an exception should trigger a retry
/// </summary>
private static bool ShouldRetry(Exception exception)
{
return exception switch
{
OperationCanceledException => false,
ArgumentNullException => false,
ArgumentException => false,
InvalidOperationException => false,
NotSupportedException => false,
UnauthorizedAccessException => false,
System.IO.FileNotFoundException => false,
System.IO.DirectoryNotFoundException => false,
System.IO.IOException => true,
TimeoutException => true,
_ => true
};
}
/// <summary>
/// Calculates exponential backoff delay
/// </summary>
private static TimeSpan CalculateDelay(TimeSpan baseDelay, int attempt)
{
var exponentialDelay = TimeSpan.FromTicks(baseDelay.Ticks * (long)Math.Pow(2, attempt));
var jitter = TimeSpan.FromMilliseconds(Random.Shared.Next(0, 100));
return exponentialDelay + jitter;
}
/// <summary>
/// Classifies the type of error for better handling
/// </summary>
private static string ClassifyError(Exception exception)
{
return exception switch
{
ArgumentException => "Configuration",
UnauthorizedAccessException => "Security",
System.IO.IOException => "IO",
TimeoutException => "Timeout",
OutOfMemoryException => "Memory",
StackOverflowException => "Stack",
OperationCanceledException => "Cancellation",
_ => "General"
};
}
/// <summary>
/// Determines the severity of an error
/// </summary>
private static ErrorSeverity DetermineSeverity(Exception exception)
{
return exception switch
{
OutOfMemoryException => ErrorSeverity.Critical,
StackOverflowException => ErrorSeverity.Critical,
UnauthorizedAccessException => ErrorSeverity.High,
System.IO.FileNotFoundException => ErrorSeverity.Medium,
System.IO.DirectoryNotFoundException => ErrorSeverity.Medium,
ArgumentException => ErrorSeverity.Medium,
TimeoutException => ErrorSeverity.Low,
OperationCanceledException => ErrorSeverity.Low,
_ => ErrorSeverity.Medium
};
}
/// <summary>
/// Determines if an error is recoverable
/// </summary>
private static bool IsRecoverable(Exception exception)
{
return exception switch
{
OutOfMemoryException => false,
StackOverflowException => false,
UnauthorizedAccessException => false,
System.IO.FileNotFoundException => false,
System.IO.DirectoryNotFoundException => false,
ArgumentException => false,
TimeoutException => true,
System.IO.IOException => true,
_ => true
};
}
/// <summary>
/// Gets appropriate log level for error severity
/// </summary>
private static LogLevel GetLogLevel(ErrorSeverity severity)
{
return severity switch
{
ErrorSeverity.Critical => LogLevel.Critical,
ErrorSeverity.High => LogLevel.Error,
ErrorSeverity.Medium => LogLevel.Warning,
ErrorSeverity.Low => LogLevel.Information,
_ => LogLevel.Warning
};
}
}
/// <summary>
/// Result of an operation with error handling
/// </summary>
public class OperationResult<T>
{
public bool IsSuccess { get; private set; }
public T? Value { get; private set; }
public Exception? Exception { get; private set; }
public TimeSpan Duration { get; private set; }
public string? ErrorMessage => Exception?.Message;
private OperationResult(bool isSuccess, T? value, Exception? exception, TimeSpan duration)
{
IsSuccess = isSuccess;
Value = value;
Exception = exception;
Duration = duration;
}
public static OperationResult<T> Success(T value, TimeSpan duration)
{
return new OperationResult<T>(true, value, null, duration);
}
public static OperationResult<T> Failure(Exception exception, TimeSpan duration)
{
return new OperationResult<T>(false, default, exception, duration);
}
}
/// <summary>
/// Information about plugin errors
/// </summary>
public class PluginErrorInfo
{
public string PluginName { get; set; } = string.Empty;
public string OperationName { get; set; } = string.Empty;
public Exception? Exception { get; set; }
public DateTime Timestamp { get; set; }
public string ErrorType { get; set; } = string.Empty;
public ErrorSeverity Severity { get; set; }
public bool Recoverable { get; set; }
}
/// <summary>
/// Error severity levels
/// </summary>
public enum ErrorSeverity
{
Low,
Medium,
High,
Critical
}
}