diff --git a/src/Managing.Core/Exceptions/OrleansExceptionHelper.cs b/src/Managing.Core/Exceptions/OrleansExceptionHelper.cs new file mode 100644 index 00000000..1494263a --- /dev/null +++ b/src/Managing.Core/Exceptions/OrleansExceptionHelper.cs @@ -0,0 +1,119 @@ +using Microsoft.Extensions.Logging; + +namespace Managing.Core.Exceptions; + +/// +/// Helper class to detect and handle Orleans-specific exceptions +/// +public static class OrleansExceptionHelper +{ + /// + /// Checks if an exception is an Orleans-specific exception + /// + public static bool IsOrleansException(Exception ex) + { + if (ex == null) return false; + + var exceptionTypeName = ex.GetType().Name; + var exceptionMessage = ex.Message ?? string.Empty; + + // Check for Orleans exception type names + if (exceptionTypeName.Contains("Orleans", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Check for Orleans-specific exception messages + if (exceptionMessage.Contains("Orleans", StringComparison.OrdinalIgnoreCase) || + exceptionMessage.Contains("grain", StringComparison.OrdinalIgnoreCase) || + exceptionMessage.Contains("silo", StringComparison.OrdinalIgnoreCase) || + exceptionMessage.Contains("activation", StringComparison.OrdinalIgnoreCase) || + exceptionMessage.Contains("forwarding failed", StringComparison.OrdinalIgnoreCase) || + exceptionMessage.Contains("Unable to create local activation", StringComparison.OrdinalIgnoreCase) || + exceptionMessage.Contains("Request timeout", StringComparison.OrdinalIgnoreCase) || + exceptionMessage.Contains("Deadlock", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Check inner exception recursively + if (ex.InnerException != null) + { + return IsOrleansException(ex.InnerException); + } + + return false; + } + + /// + /// Converts an Orleans exception to a user-friendly message + /// + public static string GetUserFriendlyMessage(Exception ex, string operation = "operation") + { + if (!IsOrleansException(ex)) + { + return ex.Message; + } + + var exceptionTypeName = ex.GetType().Name; + var exceptionMessage = ex.Message ?? string.Empty; + + // Handle timeout exceptions + if (ex is TimeoutException || ex is TaskCanceledException || + exceptionMessage.Contains("timeout", StringComparison.OrdinalIgnoreCase) || + exceptionMessage.Contains("timed out", StringComparison.OrdinalIgnoreCase)) + { + return $"The {operation} timed out. This may occur when closing positions takes longer than expected. Please try again in a moment."; + } + + // Handle deadlock exceptions + if (exceptionMessage.Contains("deadlock", StringComparison.OrdinalIgnoreCase)) + { + return $"The {operation} could not complete due to a system conflict. Please try again in a moment."; + } + + // Handle activation/grain errors + if (exceptionMessage.Contains("activation", StringComparison.OrdinalIgnoreCase) || + exceptionMessage.Contains("grain", StringComparison.OrdinalIgnoreCase) || + exceptionMessage.Contains("forwarding failed", StringComparison.OrdinalIgnoreCase)) + { + return $"The {operation} encountered a temporary system issue. Please try again in a moment."; + } + + // Generic Orleans error + return $"The {operation} encountered a temporary system issue. Please try again in a moment. If the problem persists, contact support."; + } + + /// + /// Wraps an async operation with timeout and Orleans exception handling + /// + public static async Task ExecuteWithTimeoutAndOrleansHandling( + Func> operation, + TimeSpan timeout, + string operationName, + ILogger logger = null) + { + try + { + using var cts = new CancellationTokenSource(timeout); + var task = operation(); + var timeoutTask = Task.Delay(timeout, cts.Token); + + var completedTask = await Task.WhenAny(task, timeoutTask); + if (completedTask == timeoutTask) + { + throw new TimeoutException($"The {operationName} timed out after {timeout.TotalSeconds} seconds."); + } + + return await task; + } + catch (Exception ex) when (IsOrleansException(ex) || ex is TimeoutException) + { + logger?.LogError(ex, "Orleans exception or timeout during {OperationName}: {ExceptionType} - {Message}", + operationName, ex.GetType().Name, ex.Message); + var userMessage = GetUserFriendlyMessage(ex, operationName); + throw new ServiceUnavailableException(userMessage); + } + } +} +