From a88d3b8ccb33a8866397998cdee632d453284334 Mon Sep 17 00:00:00 2001 From: Jeff-Xu23 Date: Tue, 29 Apr 2025 10:01:22 +0800 Subject: [PATCH 1/5] feat:exception handler --- docs/exception-handling.md | 126 +++++++++++ .../ExceptionHandlingSample.cs | 209 ++++++++++++++++++ .../AevatarOptions.cs | 2 + .../Events/ExceptionEvent.cs | 14 ++ .../ExceptionPublisherExtensions.cs | 165 ++++++++++++++ .../Extensions/GAgentExceptionExtensions.cs | 98 ++++++++ src/Aevatar.Core/GAgentBase.cs | 60 +++++ 7 files changed, 674 insertions(+) create mode 100644 docs/exception-handling.md create mode 100644 samples/ExceptionHandling/ExceptionHandlingSample.cs create mode 100644 src/Aevatar.Core.Abstractions/Events/ExceptionEvent.cs create mode 100644 src/Aevatar.Core/Extensions/ExceptionPublisherExtensions.cs create mode 100644 src/Aevatar.Core/Extensions/GAgentExceptionExtensions.cs diff --git a/docs/exception-handling.md b/docs/exception-handling.md new file mode 100644 index 00000000..d1245b3c --- /dev/null +++ b/docs/exception-handling.md @@ -0,0 +1,126 @@ +# Exception Capturing and Publishing + +This document describes how to use the exception capturing and publishing features of the Aevatar framework. This functionality allows exception information to be written to Orleans Stream for centralized processing, monitoring, and analysis. + +## Feature Overview + +The exception capturing and publishing functionality sends exception information to a dedicated exception handling channel via Orleans Stream. Key features include: + +- Publishing detailed exception information to a dedicated Orleans Stream +- Supporting the recording of exception context information +- Automatically capturing calling method and class name +- Providing easy-to-use helper methods to simplify the exception handling process +- Using a separate Kafka Topic, isolated from business Topics + +## Configuration + +Add the following configuration to the `appsettings.json` file: + +```json +{ + "Aevatar": { + "ExceptionStreamNamespace": "AevatarException" + } +} +``` + +Where `ExceptionStreamNamespace` specifies the Stream namespace for exception events, with a default value of "AevatarException". + +## Usage + +### Direct Exception Publishing + +```csharp +public class MyGAgent : GAgentBase +{ + public async Task DoSomethingAsync() + { + try + { + // Business logic + await ProcessDataAsync(); + } + catch (Exception ex) + { + // Publish exception + var contextData = new { UserId = "user123", Action = "ProcessData" }; + await this.PublishExceptionAsync(ex, contextData); + + // Can choose to rethrow or handle the exception + throw; + } + } +} +``` + +### Using Helper Methods to Automatically Capture and Publish Exceptions + +```csharp +public class MyGAgent : GAgentBase +{ + public async Task DoSomethingAsync() + { + var contextData = new { UserId = "user123", Action = "ProcessData" }; + + // Automatically capture and publish exceptions, rethrown by default + await this.CatchAndPublishExceptionAsync(async () => + { + await ProcessDataAsync(); + }, contextData); + } + + public async Task GetDataAsync() + { + var contextData = new { UserId = "user123", Action = "GetData" }; + + // For cases with return values, without rethrowing the exception + var (result, exceptionId) = await this.CatchAndPublishExceptionAsync( + async () => await FetchDataAsync(), + new Result { Success = false }, // Default value + contextData, + rethrowException: false); + + if (exceptionId != Guid.Empty) + { + // Exception occurred, using default value + Logger.LogWarning("Exception occurred, using default value. ExceptionId: {ExceptionId}", exceptionId); + } + + return result; + } +} +``` + +## Exception Event Format + +The published exception event contains the following information: + +```csharp +public class ExceptionEvent : EventBase +{ + public GrainId GrainId { get; set; } // Grain ID where the exception occurred + public string ExceptionMessage { get; set; } // Exception message + public string ExceptionType { get; set; } // Exception type + public string StackTrace { get; set; } // Stack trace + public string ContextData { get; set; } // Context data (JSON format) + public DateTime Timestamp { get; set; } // Exception timestamp (UTC) + public string? MethodName { get; set; } // Method name where the exception occurred + public string? ClassName { get; set; } // Class name where the exception occurred +} +``` + +## Exception Handling Process + +1. Exception is captured in the GAgent +2. Exception information is encapsulated as an ExceptionEvent +3. ExceptionEvent is published to a dedicated Orleans Stream +4. Via Kafka, exception events are routed to consumers for processing +5. Exception handling services can aggregate, analyze, and alert on exceptions + +## Best Practices + +- Add exception capturing and publishing for important or complex operations +- Include sufficient information in the context data to facilitate troubleshooting +- When handling sensitive data, be careful not to include personal privacy information in the context +- For high-frequency operations, consider setting an exception sampling rate to avoid too many exception events affecting performance +- Implement exception consumer services for real-time monitoring and analysis of exceptions \ No newline at end of file diff --git a/samples/ExceptionHandling/ExceptionHandlingSample.cs b/samples/ExceptionHandling/ExceptionHandlingSample.cs new file mode 100644 index 00000000..05dffb74 --- /dev/null +++ b/samples/ExceptionHandling/ExceptionHandlingSample.cs @@ -0,0 +1,209 @@ +using System; +using System.Threading.Tasks; +using Aevatar.Core; +using Aevatar.Core.Abstractions; +using Aevatar.Core.Extensions; +using Microsoft.Extensions.Logging; + +namespace Aevatar.Samples.ExceptionHandling; + +/// +/// Sample State Class +/// +[GenerateSerializer] +public class SampleState : StateBase +{ + [Id(0)] public int Counter { get; set; } +} + +/// +/// Sample State Log Event Class +/// +[GenerateSerializer] +public class SampleStateLogEvent : StateLogEventBase +{ +} + +/// +/// Sample GAgent demonstrating how to use exception capturing and publishing features +/// +[GAgent] +public class ExceptionHandlingSampleGAgent : GAgentBase +{ + private readonly ILogger _logger; + + public ExceptionHandlingSampleGAgent(ILogger logger) : base(logger) + { + _logger = logger; + } + + /// + /// Demonstrates how to use exception publishing feature directly + /// + public async Task DemoDirectExceptionPublishingAsync() + { + _logger.LogInformation("Starting direct exception publishing demo"); + + try + { + // Simulate an exception + throw new InvalidOperationException("This is a test exception"); + } + catch (Exception ex) + { + // Create context data + var contextData = new + { + Operation = "DemoDirectExceptionPublishing", + Timestamp = DateTime.UtcNow, + GrainId = this.GetGrainId().ToString() + }; + + // Publish exception directly + var exceptionId = await this.PublishExceptionAsync(ex, contextData); + + _logger.LogInformation("Published exception with ID: {ExceptionId}", exceptionId); + + // In real applications, you might choose to rethrow or handle the exception + // throw; + } + + _logger.LogInformation("Completed direct exception publishing demo"); + } + + /// + /// Demonstrates how to use helper method to catch and publish exceptions (without return value) + /// + public async Task DemoExceptionHandlingWithoutResultAsync() + { + _logger.LogInformation("Starting exception handling demo without result"); + + var contextData = new + { + Operation = "DemoExceptionHandlingWithoutResult", + Timestamp = DateTime.UtcNow, + GrainId = this.GetGrainId().ToString() + }; + + // Use helper method to catch and publish exceptions, without rethrowing the exception + var exceptionId = await this.CatchAndPublishExceptionAsync( + async () => + { + // Simulate an exception + await Task.Delay(100); + throw new ArgumentException("Invalid argument in operation"); + }, + contextData, + rethrowException: false); + + if (exceptionId != Guid.Empty) + { + _logger.LogInformation("Exception occurred and published with ID: {ExceptionId}", exceptionId); + } + + _logger.LogInformation("Completed exception handling demo without result"); + } + + /// + /// Demonstrates how to use helper method to catch and publish exceptions (with return value) + /// + public async Task<(bool Success, string Message)> DemoExceptionHandlingWithResultAsync() + { + _logger.LogInformation("Starting exception handling demo with result"); + + var contextData = new + { + Operation = "DemoExceptionHandlingWithResult", + Timestamp = DateTime.UtcNow, + Parameters = new { Id = "sample-id", RequestType = "GET" } + }; + + // Use helper method to catch and publish exceptions, with return value, without rethrowing the exception + var (result, exceptionId) = await this.CatchAndPublishExceptionAsync( + async () => + { + // Simulate a successful operation + await Task.Delay(100); + + // May throw an exception based on conditions + if (DateTime.UtcNow.Millisecond % 2 == 0) + { + throw new TimeoutException("Operation timed out"); + } + + return (Success: true, Message: "Operation completed successfully"); + }, + (Success: false, Message: "Operation failed due to an exception"), // Default value + contextData, + rethrowException: false); + + if (exceptionId != Guid.Empty) + { + _logger.LogInformation("Exception occurred and published with ID: {ExceptionId}", exceptionId); + _logger.LogInformation("Using default result: {Result}", result); + } + else + { + _logger.LogInformation("Operation completed successfully: {Result}", result); + } + + _logger.LogInformation("Completed exception handling demo with result"); + + return result; + } + + /// + /// Demonstrates how to use exception capturing and publishing in actual business logic + /// + public async Task PerformBusinessOperationAsync(int value) + { + _logger.LogInformation("Performing business operation with value: {Value}", value); + + // Catch and publish exceptions with business context data + var contextData = new + { + OperationName = "PerformBusinessOperation", + InputValue = value + }; + + var (result, exceptionId) = await this.CatchAndPublishExceptionAsync( + async () => + { + // Simulate business logic + await Task.Delay(100); + + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "Value cannot be negative"); + } + + // Update state + RaiseEvent(new SampleStateLogEvent()); + State.Counter += value; + + return State.Counter; + }, + -1, // Default value, indicating operation failure + contextData, + rethrowException: false); + + if (exceptionId != Guid.Empty) + { + _logger.LogWarning("Business operation failed with exception ID: {ExceptionId}", exceptionId); + } + else + { + _logger.LogInformation("Business operation completed successfully, new counter value: {Counter}", result); + } + + return result; + } + + /// + /// GAgent description + /// + public override Task GetDescriptionAsync() + { + return Task.FromResult("Exception Handling Sample GAgent"); + } +} \ No newline at end of file diff --git a/src/Aevatar.Core.Abstractions/AevatarOptions.cs b/src/Aevatar.Core.Abstractions/AevatarOptions.cs index 4da8d914..44ad3357 100644 --- a/src/Aevatar.Core.Abstractions/AevatarOptions.cs +++ b/src/Aevatar.Core.Abstractions/AevatarOptions.cs @@ -6,5 +6,7 @@ public class AevatarOptions public string StateProjectionStreamNamespace { get; set; } = "AevatarStateProjection"; public string BroadCastStreamNamespace { get; set; } = "AevatarBroadCast"; + public string ExceptionStreamNamespace { get; set; } = "AevatarException"; + public string ExceptionStreamKey { get; set; } = "global-exceptions"; //public int ElasticSearchProcessors { get; set; } = 10; } \ No newline at end of file diff --git a/src/Aevatar.Core.Abstractions/Events/ExceptionEvent.cs b/src/Aevatar.Core.Abstractions/Events/ExceptionEvent.cs new file mode 100644 index 00000000..17764e9b --- /dev/null +++ b/src/Aevatar.Core.Abstractions/Events/ExceptionEvent.cs @@ -0,0 +1,14 @@ +namespace Aevatar.Core.Abstractions; + +[GenerateSerializer] +public class ExceptionEvent : EventBase +{ + [Id(0)] public required GrainId GrainId { get; set; } + [Id(1)] public required string ExceptionMessage { get; set; } + [Id(2)] public required string ExceptionType { get; set; } + [Id(3)] public required string StackTrace { get; set; } + [Id(4)] public required string ContextData { get; set; } + [Id(5)] public required DateTime Timestamp { get; set; } = DateTime.UtcNow; + [Id(6)] public string? MethodName { get; set; } + [Id(7)] public string? ClassName { get; set; } +} \ No newline at end of file diff --git a/src/Aevatar.Core/Extensions/ExceptionPublisherExtensions.cs b/src/Aevatar.Core/Extensions/ExceptionPublisherExtensions.cs new file mode 100644 index 00000000..ec72940a --- /dev/null +++ b/src/Aevatar.Core/Extensions/ExceptionPublisherExtensions.cs @@ -0,0 +1,165 @@ +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using Aevatar.Core.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Orleans.Runtime; +using Orleans.Streams; + +namespace Aevatar.Core.Extensions; + +/// +/// Provides extension methods for publishing exceptions to Orleans Stream +/// +public static class ExceptionPublisherExtensions +{ + /// + /// Publishes an exception to Orleans Stream + /// + /// Orleans Grain instance + /// Exception to publish + /// Context data, can be any serializable object + /// Caller method name, auto-populated + /// Caller class name, auto-populated + /// Exception event ID + public static async Task PublishExceptionAsync( + this Grain grain, + Exception exception, + object? contextData = null, + [CallerMemberName] string? callerMemberName = null, + [CallerFilePath] string? callerClassName = null) + { + try + { + var grainId = grain.GetGrainId(); + var context = grain.GrainContext; + var options = context.ActivationServices.GetRequiredService>().Value; + var streamProvider = grain.GetStreamProvider(AevatarCoreConstants.StreamProvider); + + var contextDataJson = "{}"; + if (contextData != null) + { + try + { + contextDataJson = JsonSerializer.Serialize(contextData); + } + catch (Exception serializationEx) + { + contextDataJson = $"{{\"error\":\"Failed to serialize context data: {serializationEx.Message}\"}}"; + } + } + + // Extract class name + string? className = null; + if (!string.IsNullOrEmpty(callerClassName)) + { + className = Path.GetFileNameWithoutExtension(callerClassName); + } + + var exceptionEvent = new ExceptionEvent + { + GrainId = grainId, + ExceptionMessage = exception.Message, + ExceptionType = exception.GetType().FullName ?? "Unknown", + StackTrace = exception.StackTrace ?? string.Empty, + ContextData = contextDataJson, + Timestamp = DateTime.UtcNow, + MethodName = callerMemberName, + ClassName = className + }; + + var streamNamespace = options.ExceptionStreamNamespace; + var stream = streamProvider.GetStream(StreamId.Create(streamNamespace, options.ExceptionStreamKey)); + + var eventId = Guid.NewGuid(); + await stream.OnNextAsync(exceptionEvent); + + return eventId; + } + catch (Exception ex) + { + // If an error occurs while publishing the exception, log it but don't try to publish again (to avoid infinite loops) + var context = grain.GrainContext; + var logger = context.ActivationServices.GetService>(); + logger?.LogError(ex, "Failed to publish exception: {Message}", ex.Message); + return Guid.Empty; + } + } + + /// + /// Executes an operation, catches any exception and publishes it to Orleans Stream + /// + /// Orleans Grain instance + /// Operation to execute + /// Context data, can be any serializable object + /// Whether to rethrow the exception, defaults to true + /// Caller method name, auto-populated + /// Caller class name, auto-populated + /// If an exception occurs, returns the exception event ID; otherwise returns Guid.Empty + public static async Task CatchAndPublishExceptionAsync( + this Grain grain, + Func action, + object? contextData = null, + bool rethrowException = true, + [CallerMemberName] string? callerMemberName = null, + [CallerFilePath] string? callerClassName = null) + { + try + { + await action(); + return Guid.Empty; + } + catch (Exception ex) + { + var eventId = await grain.PublishExceptionAsync(ex, contextData, callerMemberName, callerClassName); + + if (rethrowException) + { + throw; + } + + return eventId; + } + } + + /// + /// Executes an operation, catches any exception and publishes it to Orleans Stream, returns the operation result + /// + /// Operation result type + /// Orleans Grain instance + /// Operation to execute + /// Default value to return if an exception occurs and is not rethrown + /// Context data, can be any serializable object + /// Whether to rethrow the exception, defaults to true + /// Caller method name, auto-populated + /// Caller class name, auto-populated + /// If the operation succeeds, returns the operation result; if an exception occurs and is not rethrown, returns the default value + public static async Task<(T Result, Guid ExceptionId)> CatchAndPublishExceptionAsync( + this Grain grain, + Func> func, + T defaultValue = default!, + object? contextData = null, + bool rethrowException = true, + [CallerMemberName] string? callerMemberName = null, + [CallerFilePath] string? callerClassName = null) + { + try + { + var result = await func(); + return (result, Guid.Empty); + } + catch (Exception ex) + { + var eventId = await grain.PublishExceptionAsync(ex, contextData, callerMemberName, callerClassName); + + if (rethrowException) + { + throw; + } + + return (defaultValue, eventId); + } + } +} \ No newline at end of file diff --git a/src/Aevatar.Core/Extensions/GAgentExceptionExtensions.cs b/src/Aevatar.Core/Extensions/GAgentExceptionExtensions.cs new file mode 100644 index 00000000..f0d32858 --- /dev/null +++ b/src/Aevatar.Core/Extensions/GAgentExceptionExtensions.cs @@ -0,0 +1,98 @@ +using System.Runtime.CompilerServices; +using Aevatar.Core.Abstractions; + +namespace Aevatar.Core.Extensions; + +/// +/// Provides exception handling extension methods for GAgentBase +/// +public static class GAgentExceptionExtensions +{ + /// + /// Publishes an exception to Orleans Stream + /// + /// State type + /// StateLogEvent type + /// Event type + /// Configuration type + /// GAgentBase instance + /// Exception to publish + /// Context data, can be any serializable object + /// Caller method name, auto-populated + /// Caller class name, auto-populated + /// Exception event ID + public static Task PublishExceptionAsync( + this GAgentBase gAgent, + Exception exception, + object? contextData = null, + [CallerMemberName] string? callerMemberName = null, + [CallerFilePath] string? callerClassName = null) + where TState : StateBase, new() + where TStateLogEvent : StateLogEventBase + where TEvent : EventBase + where TConfiguration : ConfigurationBase + { + return ((Grain)gAgent).PublishExceptionAsync(exception, contextData, callerMemberName, callerClassName); + } + + /// + /// Executes an operation, catches any exception and publishes it to Orleans Stream + /// + /// State type + /// StateLogEvent type + /// Event type + /// Configuration type + /// GAgentBase instance + /// Operation to execute + /// Context data, can be any serializable object + /// Whether to rethrow the exception, defaults to true + /// Caller method name, auto-populated + /// Caller class name, auto-populated + /// If an exception occurs, returns the exception event ID; otherwise returns Guid.Empty + public static Task CatchAndPublishExceptionAsync( + this GAgentBase gAgent, + Func action, + object? contextData = null, + bool rethrowException = true, + [CallerMemberName] string? callerMemberName = null, + [CallerFilePath] string? callerClassName = null) + where TState : StateBase, new() + where TStateLogEvent : StateLogEventBase + where TEvent : EventBase + where TConfiguration : ConfigurationBase + { + return ((Grain)gAgent).CatchAndPublishExceptionAsync(action, contextData, rethrowException, callerMemberName, callerClassName); + } + + /// + /// Executes an operation, catches any exception and publishes it to Orleans Stream, returns the operation result + /// + /// State type + /// StateLogEvent type + /// Event type + /// Configuration type + /// Operation result type + /// GAgentBase instance + /// Operation to execute + /// Default value to return if an exception occurs and is not rethrown + /// Context data, can be any serializable object + /// Whether to rethrow the exception, defaults to true + /// Caller method name, auto-populated + /// Caller class name, auto-populated + /// If the operation succeeds, returns the operation result; if an exception occurs and is not rethrown, returns the default value + public static Task<(TResult Result, Guid ExceptionId)> CatchAndPublishExceptionAsync( + this GAgentBase gAgent, + Func> func, + TResult defaultValue = default!, + object? contextData = null, + bool rethrowException = true, + [CallerMemberName] string? callerMemberName = null, + [CallerFilePath] string? callerClassName = null) + where TState : StateBase, new() + where TStateLogEvent : StateLogEventBase + where TEvent : EventBase + where TConfiguration : ConfigurationBase + { + return ((Grain)gAgent).CatchAndPublishExceptionAsync(func, defaultValue, contextData, rethrowException, callerMemberName, callerClassName); + } +} \ No newline at end of file diff --git a/src/Aevatar.Core/GAgentBase.cs b/src/Aevatar.Core/GAgentBase.cs index 4dc5c3d1..27ae1029 100644 --- a/src/Aevatar.Core/GAgentBase.cs +++ b/src/Aevatar.Core/GAgentBase.cs @@ -1,5 +1,7 @@ using System.Collections.Concurrent; +using System.Runtime.CompilerServices; using Aevatar.Core.Abstractions; +using Aevatar.Core.Extensions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -54,6 +56,64 @@ public abstract partial class private IStateDispatcher? StateDispatcher { get; set; } protected AevatarOptions? AevatarOptions; + /// + /// Publishes an exception to Orleans Stream + /// + /// Exception to publish + /// Context data, can be any serializable object + /// Caller method name, auto-populated + /// Caller class name, auto-populated + /// Exception event ID + public Task PublishExceptionAsync( + Exception exception, + object? contextData = null, + [CallerMemberName] string? callerMemberName = null, + [CallerFilePath] string? callerClassName = null) + { + return ((Grain)this).PublishExceptionAsync(exception, contextData, callerMemberName, callerClassName); + } + + /// + /// Executes an operation, catches any exception and publishes it to Orleans Stream + /// + /// Operation to execute + /// Context data, can be any serializable object + /// Whether to rethrow the exception, defaults to true + /// Caller method name, auto-populated + /// Caller class name, auto-populated + /// If an exception occurs, returns the exception event ID; otherwise returns Guid.Empty + public Task CatchAndPublishExceptionAsync( + Func action, + object? contextData = null, + bool rethrowException = true, + [CallerMemberName] string? callerMemberName = null, + [CallerFilePath] string? callerClassName = null) + { + return ((Grain)this).CatchAndPublishExceptionAsync(action, contextData, rethrowException, callerMemberName, callerClassName); + } + + /// + /// Executes an operation, catches any exception and publishes it to Orleans Stream, returns the operation result + /// + /// Operation result type + /// Operation to execute + /// Default value to return if an exception occurs and is not rethrown + /// Context data, can be any serializable object + /// Whether to rethrow the exception, defaults to true + /// Caller method name, auto-populated + /// Caller class name, auto-populated + /// If the operation succeeds, returns the operation result; if an exception occurs and is not rethrown, returns the default value + public Task<(TResult Result, Guid ExceptionId)> CatchAndPublishExceptionAsync( + Func> func, + TResult defaultValue = default!, + object? contextData = null, + bool rethrowException = true, + [CallerMemberName] string? callerMemberName = null, + [CallerFilePath] string? callerClassName = null) + { + return ((Grain)this).CatchAndPublishExceptionAsync(func, defaultValue, contextData, rethrowException, callerMemberName, callerClassName); + } + public async Task ActivateAsync() { await Task.Yield(); From 233d6dbfef092f10a78c341ee4e0344407fb05a4 Mon Sep 17 00:00:00 2001 From: Jeff-Xu23 Date: Tue, 29 Apr 2025 13:45:12 +0800 Subject: [PATCH 2/5] feat:adjust code --- .../ExceptionPublisherExtensions.cs | 1 + .../Extensions/GAgentExceptionExtensions.cs | 98 ------------------- src/Aevatar.Core/GAgentBase.cs | 6 +- 3 files changed, 4 insertions(+), 101 deletions(-) delete mode 100644 src/Aevatar.Core/Extensions/GAgentExceptionExtensions.cs diff --git a/src/Aevatar.Core/Extensions/ExceptionPublisherExtensions.cs b/src/Aevatar.Core/Extensions/ExceptionPublisherExtensions.cs index ec72940a..054c1288 100644 --- a/src/Aevatar.Core/Extensions/ExceptionPublisherExtensions.cs +++ b/src/Aevatar.Core/Extensions/ExceptionPublisherExtensions.cs @@ -74,6 +74,7 @@ public static async Task PublishExceptionAsync( var stream = streamProvider.GetStream(StreamId.Create(streamNamespace, options.ExceptionStreamKey)); var eventId = Guid.NewGuid(); + exceptionEvent.CorrelationId = eventId; await stream.OnNextAsync(exceptionEvent); return eventId; diff --git a/src/Aevatar.Core/Extensions/GAgentExceptionExtensions.cs b/src/Aevatar.Core/Extensions/GAgentExceptionExtensions.cs deleted file mode 100644 index f0d32858..00000000 --- a/src/Aevatar.Core/Extensions/GAgentExceptionExtensions.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System.Runtime.CompilerServices; -using Aevatar.Core.Abstractions; - -namespace Aevatar.Core.Extensions; - -/// -/// Provides exception handling extension methods for GAgentBase -/// -public static class GAgentExceptionExtensions -{ - /// - /// Publishes an exception to Orleans Stream - /// - /// State type - /// StateLogEvent type - /// Event type - /// Configuration type - /// GAgentBase instance - /// Exception to publish - /// Context data, can be any serializable object - /// Caller method name, auto-populated - /// Caller class name, auto-populated - /// Exception event ID - public static Task PublishExceptionAsync( - this GAgentBase gAgent, - Exception exception, - object? contextData = null, - [CallerMemberName] string? callerMemberName = null, - [CallerFilePath] string? callerClassName = null) - where TState : StateBase, new() - where TStateLogEvent : StateLogEventBase - where TEvent : EventBase - where TConfiguration : ConfigurationBase - { - return ((Grain)gAgent).PublishExceptionAsync(exception, contextData, callerMemberName, callerClassName); - } - - /// - /// Executes an operation, catches any exception and publishes it to Orleans Stream - /// - /// State type - /// StateLogEvent type - /// Event type - /// Configuration type - /// GAgentBase instance - /// Operation to execute - /// Context data, can be any serializable object - /// Whether to rethrow the exception, defaults to true - /// Caller method name, auto-populated - /// Caller class name, auto-populated - /// If an exception occurs, returns the exception event ID; otherwise returns Guid.Empty - public static Task CatchAndPublishExceptionAsync( - this GAgentBase gAgent, - Func action, - object? contextData = null, - bool rethrowException = true, - [CallerMemberName] string? callerMemberName = null, - [CallerFilePath] string? callerClassName = null) - where TState : StateBase, new() - where TStateLogEvent : StateLogEventBase - where TEvent : EventBase - where TConfiguration : ConfigurationBase - { - return ((Grain)gAgent).CatchAndPublishExceptionAsync(action, contextData, rethrowException, callerMemberName, callerClassName); - } - - /// - /// Executes an operation, catches any exception and publishes it to Orleans Stream, returns the operation result - /// - /// State type - /// StateLogEvent type - /// Event type - /// Configuration type - /// Operation result type - /// GAgentBase instance - /// Operation to execute - /// Default value to return if an exception occurs and is not rethrown - /// Context data, can be any serializable object - /// Whether to rethrow the exception, defaults to true - /// Caller method name, auto-populated - /// Caller class name, auto-populated - /// If the operation succeeds, returns the operation result; if an exception occurs and is not rethrown, returns the default value - public static Task<(TResult Result, Guid ExceptionId)> CatchAndPublishExceptionAsync( - this GAgentBase gAgent, - Func> func, - TResult defaultValue = default!, - object? contextData = null, - bool rethrowException = true, - [CallerMemberName] string? callerMemberName = null, - [CallerFilePath] string? callerClassName = null) - where TState : StateBase, new() - where TStateLogEvent : StateLogEventBase - where TEvent : EventBase - where TConfiguration : ConfigurationBase - { - return ((Grain)gAgent).CatchAndPublishExceptionAsync(func, defaultValue, contextData, rethrowException, callerMemberName, callerClassName); - } -} \ No newline at end of file diff --git a/src/Aevatar.Core/GAgentBase.cs b/src/Aevatar.Core/GAgentBase.cs index 27ae1029..2d0b7754 100644 --- a/src/Aevatar.Core/GAgentBase.cs +++ b/src/Aevatar.Core/GAgentBase.cs @@ -64,7 +64,7 @@ public abstract partial class /// Caller method name, auto-populated /// Caller class name, auto-populated /// Exception event ID - public Task PublishExceptionAsync( + protected Task PublishExceptionAsync( Exception exception, object? contextData = null, [CallerMemberName] string? callerMemberName = null, @@ -82,7 +82,7 @@ public Task PublishExceptionAsync( /// Caller method name, auto-populated /// Caller class name, auto-populated /// If an exception occurs, returns the exception event ID; otherwise returns Guid.Empty - public Task CatchAndPublishExceptionAsync( + protected Task CatchAndPublishExceptionAsync( Func action, object? contextData = null, bool rethrowException = true, @@ -103,7 +103,7 @@ public Task CatchAndPublishExceptionAsync( /// Caller method name, auto-populated /// Caller class name, auto-populated /// If the operation succeeds, returns the operation result; if an exception occurs and is not rethrown, returns the default value - public Task<(TResult Result, Guid ExceptionId)> CatchAndPublishExceptionAsync( + protected Task<(TResult Result, Guid ExceptionId)> CatchAndPublishExceptionAsync( Func> func, TResult defaultValue = default!, object? contextData = null, From 1bb5712f8359c9ec733e36992bd0c8a4cdaa20f7 Mon Sep 17 00:00:00 2001 From: Jeff-Xu23 Date: Tue, 29 Apr 2025 14:48:32 +0800 Subject: [PATCH 3/5] feat:add unit test --- .../ExceptionPublisherTests.cs | 169 ++++++++++++++++++ .../ExceptionHandlingTestGAgent.cs | 53 ++++++ 2 files changed, 222 insertions(+) create mode 100644 test/Aevatar.Core.Tests/ExceptionPublisherTests.cs diff --git a/test/Aevatar.Core.Tests/ExceptionPublisherTests.cs b/test/Aevatar.Core.Tests/ExceptionPublisherTests.cs new file mode 100644 index 00000000..a292f5f8 --- /dev/null +++ b/test/Aevatar.Core.Tests/ExceptionPublisherTests.cs @@ -0,0 +1,169 @@ +using System.Text.Json; +using Aevatar.Core.Abstractions; +using Aevatar.Core.Tests.TestGAgents; +using Microsoft.Extensions.Options; +using Orleans.TestKit; +using Orleans.TestKit.Streams; +using Shouldly; + +namespace Aevatar.Core.Tests; + +public class ExceptionPublisherTests : GAgentTestKitBase +{ + private ExceptionHandlingTestGAgent _agent; + private TestStream _exceptionStream; + + public ExceptionPublisherTests () + { + var options = new AevatarOptions + { + ExceptionStreamNamespace = "AevatarException", + ExceptionStreamKey = "global-exceptions" + }; + Silo.ServiceProvider.AddService>(Options.Create(options)); + + // Create the test exception stream + _exceptionStream = Silo.AddStreamProbe( + StreamId.Create(options.ExceptionStreamNamespace, options.ExceptionStreamKey)); + + } + + [Fact] + public async Task PublishExceptionAsync_ShouldPublishExceptionToStream() + { + // Create test agent + _agent = await Silo.CreateGrainAsync(Guid.NewGuid()); + + // Arrange + var exception = new InvalidOperationException("Test exception"); + var contextData = new { TestId = 1, Message = "Test context" }; + + // Act + var exceptionId = await _agent.TestPublishExceptionAsync(exception, contextData); + + // Assert + exceptionId.ShouldNotBe(Guid.Empty); + _exceptionStream.Sends.ShouldBe(1); + + _exceptionStream.VerifySend(e => + e.ExceptionMessage == exception.Message && + e.ExceptionType == exception.GetType().FullName && + e.CorrelationId == exceptionId && + e.ContextData.Contains("TestId") && + e.ContextData.Contains("Test context") + ); + } + + [Fact] + public async Task CatchAndPublishExceptionAsync_WithoutException_ShouldReturnEmptyGuid() + { + // Create test agent + _agent = await Silo.CreateGrainAsync(Guid.NewGuid()); + + // Arrange & Act + var exceptionId = await _agent.TestCatchAndPublishWithoutResultAsync(false); + + // Assert + exceptionId.ShouldBe(Guid.Empty); + _exceptionStream.Sends.ShouldBe(0); + } + + [Fact] + public async Task CatchAndPublishExceptionAsync_WithException_ShouldPublishAndNotRethrow() + { + // Create test agent + _agent = await Silo.CreateGrainAsync(Guid.NewGuid()); + + // Arrange & Act + var exceptionId = await _agent.TestCatchAndPublishWithoutResultAsync(true, false); + + // Assert + exceptionId.ShouldNotBe(Guid.Empty); + _exceptionStream.Sends.ShouldBe(1); + _exceptionStream.VerifySend(e => + e.ExceptionMessage == "Test exception" && + e.ExceptionType == typeof(InvalidOperationException).FullName && + e.CorrelationId == exceptionId + ); + } + + [Fact] + public async Task CatchAndPublishExceptionAsync_WithException_ShouldPublishAndRethrow() + { + // Create test agent + _agent = await Silo.CreateGrainAsync(Guid.NewGuid()); + + // Arrange & Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await _agent.TestCatchAndPublishWithoutResultAsync(true, true)); + + exception.Message.ShouldBe("Test exception"); + _exceptionStream.Sends.ShouldBe(1); + _exceptionStream.VerifySend(e => e.ExceptionMessage == "Test exception"); + } + + [Fact] + public async Task CatchAndPublishExceptionWithResult_WithoutException_ShouldReturnResultAndEmptyGuid() + { + // Create test agent + _agent = await Silo.CreateGrainAsync(Guid.NewGuid()); + + // Arrange & Act + var (result, exceptionId) = await _agent.TestCatchAndPublishWithResultAsync(false); + + // Assert + result.ShouldBe(42); + exceptionId.ShouldBe(Guid.Empty); + _exceptionStream.Sends.ShouldBe(0); + } + + [Fact] + public async Task CatchAndPublishExceptionWithResult_WithException_ShouldReturnDefaultAndPublish() + { + // Create test agent + _agent = await Silo.CreateGrainAsync(Guid.NewGuid()); + + // Arrange & Act + var (result, exceptionId) = await _agent.TestCatchAndPublishWithResultAsync(true, false); + + // Assert + result.ShouldBe(0); // Default value for int + exceptionId.ShouldNotBe(Guid.Empty); + _exceptionStream.Sends.ShouldBe(1); + _exceptionStream.VerifySend(e => + e.ExceptionMessage == "Test exception" && + e.ExceptionType == typeof(InvalidOperationException).FullName && + e.CorrelationId == exceptionId + ); + } + + [Fact] + public async Task CatchAndPublishExceptionWithResult_WithExceptionAndRethrow_ShouldPublishAndRethrow() + { + // Create test agent + _agent = await Silo.CreateGrainAsync(Guid.NewGuid()); + + // Arrange & Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await _agent.TestCatchAndPublishWithResultAsync(true, true)); + + exception.Message.ShouldBe("Test exception"); + _exceptionStream.Sends.ShouldBe(1); + _exceptionStream.VerifySend(e => e.ExceptionMessage == "Test exception"); + } + + [Fact] + public async Task CatchAndPublishExceptionWithResult_WithCustomDefault_ShouldReturnCustomDefault() + { + // Create test agent + _agent = await Silo.CreateGrainAsync(Guid.NewGuid()); + + // Arrange & Act + var (result, exceptionId) = await _agent.TestCatchAndPublishWithCustomDefaultAsync(); + + // Assert + result.ShouldBe(999); + exceptionId.ShouldNotBe(Guid.Empty); + _exceptionStream.Sends.ShouldBe(1); + } +} \ No newline at end of file diff --git a/test/Aevatar.Core.Tests/TestGAgents/ExceptionHandlingTestGAgent.cs b/test/Aevatar.Core.Tests/TestGAgents/ExceptionHandlingTestGAgent.cs index 26315375..37ba3cd1 100644 --- a/test/Aevatar.Core.Tests/TestGAgents/ExceptionHandlingTestGAgent.cs +++ b/test/Aevatar.Core.Tests/TestGAgents/ExceptionHandlingTestGAgent.cs @@ -1,4 +1,5 @@ using Aevatar.Core.Abstractions; +using System.Runtime.CompilerServices; namespace Aevatar.Core.Tests.TestGAgents; @@ -27,4 +28,56 @@ public async Task HandleEventHandlingExceptionAsync(EventHandlerExceptionEvent @ { State.ErrorMessages.Add(@event.ExceptionMessage); } + + // Implementation of test methods + public Task TestPublishExceptionAsync(Exception exception, object? contextData = null) + { + return PublishExceptionAsync(exception, contextData); + } + + public Task TestCatchAndPublishWithoutResultAsync(bool throwException, bool rethrowException = true) + { + return CatchAndPublishExceptionAsync( + async () => + { + await Task.Delay(10); + if (throwException) + { + throw new InvalidOperationException("Test exception"); + } + }, + new { TestSource = "TestCatchAndPublishWithoutResultAsync" }, + rethrowException); + } + + public Task<(int Result, Guid ExceptionId)> TestCatchAndPublishWithResultAsync( + bool throwException, bool rethrowException = true) + { + return CatchAndPublishExceptionAsync( + async () => + { + await Task.Delay(10); + if (throwException) + { + throw new InvalidOperationException("Test exception"); + } + return 42; + }, + 0, + new { TestSource = "TestCatchAndPublishWithResultAsync" }, + rethrowException); + } + + public Task<(int Result, Guid ExceptionId)> TestCatchAndPublishWithCustomDefaultAsync() + { + return CatchAndPublishExceptionAsync( + async () => + { + await Task.Delay(10); + throw new InvalidOperationException("Test exception with custom default"); + }, + 999, + new { TestSource = "TestCatchAndPublishWithCustomDefaultAsync" }, + false); + } } \ No newline at end of file From 96796825b0fe431b2e1571ec443b039337f0a216 Mon Sep 17 00:00:00 2001 From: Jeff-Xu23 Date: Tue, 29 Apr 2025 16:17:49 +0800 Subject: [PATCH 4/5] feat:modification for PR --- .../AevatarOptions.cs | 6 +- .../ExceptionPublisherExtensions.cs | 77 ++++++++++--------- src/Aevatar.Core/GAgentBase.cs | 30 ++++---- 3 files changed, 58 insertions(+), 55 deletions(-) diff --git a/src/Aevatar.Core.Abstractions/AevatarOptions.cs b/src/Aevatar.Core.Abstractions/AevatarOptions.cs index 44ad3357..876eeea7 100644 --- a/src/Aevatar.Core.Abstractions/AevatarOptions.cs +++ b/src/Aevatar.Core.Abstractions/AevatarOptions.cs @@ -6,7 +6,9 @@ public class AevatarOptions public string StateProjectionStreamNamespace { get; set; } = "AevatarStateProjection"; public string BroadCastStreamNamespace { get; set; } = "AevatarBroadCast"; - public string ExceptionStreamNamespace { get; set; } = "AevatarException"; - public string ExceptionStreamKey { get; set; } = "global-exceptions"; + public string ExceptionStreamNamespace { get; set; } = "AevatarDLQ"; + public string ExceptionStreamKey { get; set; } = "Global"; + + public int ExceptionStackMaxLength { get; set; } = 1024; //public int ElasticSearchProcessors { get; set; } = 10; } \ No newline at end of file diff --git a/src/Aevatar.Core/Extensions/ExceptionPublisherExtensions.cs b/src/Aevatar.Core/Extensions/ExceptionPublisherExtensions.cs index 054c1288..46e43ddb 100644 --- a/src/Aevatar.Core/Extensions/ExceptionPublisherExtensions.cs +++ b/src/Aevatar.Core/Extensions/ExceptionPublisherExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; @@ -21,23 +22,26 @@ public static class ExceptionPublisherExtensions /// Orleans Grain instance /// Exception to publish /// Context data, can be any serializable object - /// Caller method name, auto-populated - /// Caller class name, auto-populated + /// Caller method name, auto-populated + /// Caller class name, auto-populated /// Exception event ID public static async Task PublishExceptionAsync( this Grain grain, Exception exception, object? contextData = null, - [CallerMemberName] string? callerMemberName = null, - [CallerFilePath] string? callerClassName = null) + [CallerMemberName] string? methodName = null, + [CallerFilePath] string? className = null) { + var grainId = grain.GetGrainId(); + var context = grain.GrainContext; + var logger = context.ActivationServices.GetService>(); + logger?.LogDebug( + $"class:{className} method:{methodName}, ready to publish exception event:{exception.Message}"); + try { - var grainId = grain.GetGrainId(); - var context = grain.GrainContext; var options = context.ActivationServices.GetRequiredService>().Value; var streamProvider = grain.GetStreamProvider(AevatarCoreConstants.StreamProvider); - var contextDataJson = "{}"; if (contextData != null) { @@ -50,45 +54,42 @@ public static async Task PublishExceptionAsync( contextDataJson = $"{{\"error\":\"Failed to serialize context data: {serializationEx.Message}\"}}"; } } - - // Extract class name - string? className = null; - if (!string.IsNullOrEmpty(callerClassName)) - { - className = Path.GetFileNameWithoutExtension(callerClassName); - } + var exceptionEvent = new ExceptionEvent { GrainId = grainId, ExceptionMessage = exception.Message, ExceptionType = exception.GetType().FullName ?? "Unknown", - StackTrace = exception.StackTrace ?? string.Empty, + StackTrace = exception.StackTrace == null + ? string.Empty + : exception.StackTrace.Length >= options.ExceptionStackMaxLength + ? exception.StackTrace.Substring(0, options.ExceptionStackMaxLength) + : exception.StackTrace, ContextData = contextDataJson, Timestamp = DateTime.UtcNow, - MethodName = callerMemberName, + MethodName = methodName, ClassName = className }; var streamNamespace = options.ExceptionStreamNamespace; - var stream = streamProvider.GetStream(StreamId.Create(streamNamespace, options.ExceptionStreamKey)); - + var stream = + streamProvider.GetStream(StreamId.Create(streamNamespace, options.ExceptionStreamKey)); + var eventId = Guid.NewGuid(); exceptionEvent.CorrelationId = eventId; await stream.OnNextAsync(exceptionEvent); - + return eventId; } catch (Exception ex) { // If an error occurs while publishing the exception, log it but don't try to publish again (to avoid infinite loops) - var context = grain.GrainContext; - var logger = context.ActivationServices.GetService>(); logger?.LogError(ex, "Failed to publish exception: {Message}", ex.Message); return Guid.Empty; } } - + /// /// Executes an operation, catches any exception and publishes it to Orleans Stream /// @@ -96,16 +97,16 @@ public static async Task PublishExceptionAsync( /// Operation to execute /// Context data, can be any serializable object /// Whether to rethrow the exception, defaults to true - /// Caller method name, auto-populated - /// Caller class name, auto-populated + /// Caller method name, auto-populated + /// Caller class name, auto-populated /// If an exception occurs, returns the exception event ID; otherwise returns Guid.Empty public static async Task CatchAndPublishExceptionAsync( this Grain grain, Func action, object? contextData = null, bool rethrowException = true, - [CallerMemberName] string? callerMemberName = null, - [CallerFilePath] string? callerClassName = null) + [CallerMemberName] string? methodName = null, + [CallerFilePath] string? className = null) { try { @@ -114,17 +115,17 @@ public static async Task CatchAndPublishExceptionAsync( } catch (Exception ex) { - var eventId = await grain.PublishExceptionAsync(ex, contextData, callerMemberName, callerClassName); - + var eventId = await grain.PublishExceptionAsync(ex, contextData, methodName, className); + if (rethrowException) { throw; } - + return eventId; } } - + /// /// Executes an operation, catches any exception and publishes it to Orleans Stream, returns the operation result /// @@ -134,8 +135,8 @@ public static async Task CatchAndPublishExceptionAsync( /// Default value to return if an exception occurs and is not rethrown /// Context data, can be any serializable object /// Whether to rethrow the exception, defaults to true - /// Caller method name, auto-populated - /// Caller class name, auto-populated + /// Caller method name, auto-populated + /// Caller class name, auto-populated /// If the operation succeeds, returns the operation result; if an exception occurs and is not rethrown, returns the default value public static async Task<(T Result, Guid ExceptionId)> CatchAndPublishExceptionAsync( this Grain grain, @@ -143,8 +144,8 @@ public static async Task CatchAndPublishExceptionAsync( T defaultValue = default!, object? contextData = null, bool rethrowException = true, - [CallerMemberName] string? callerMemberName = null, - [CallerFilePath] string? callerClassName = null) + [CallerMemberName] string? methodName = null, + [CallerFilePath] string? className = null) { try { @@ -153,14 +154,14 @@ public static async Task CatchAndPublishExceptionAsync( } catch (Exception ex) { - var eventId = await grain.PublishExceptionAsync(ex, contextData, callerMemberName, callerClassName); - + var eventId = await grain.PublishExceptionAsync(ex, contextData, methodName, className); + if (rethrowException) { throw; } - + return (defaultValue, eventId); } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/Aevatar.Core/GAgentBase.cs b/src/Aevatar.Core/GAgentBase.cs index 2d0b7754..fb7213f9 100644 --- a/src/Aevatar.Core/GAgentBase.cs +++ b/src/Aevatar.Core/GAgentBase.cs @@ -61,16 +61,16 @@ public abstract partial class /// /// Exception to publish /// Context data, can be any serializable object - /// Caller method name, auto-populated - /// Caller class name, auto-populated + /// Caller method name, auto-populated + /// Caller class name, auto-populated /// Exception event ID protected Task PublishExceptionAsync( Exception exception, object? contextData = null, - [CallerMemberName] string? callerMemberName = null, - [CallerFilePath] string? callerClassName = null) + string? methodName = null, + string? agentType = null) { - return ((Grain)this).PublishExceptionAsync(exception, contextData, callerMemberName, callerClassName); + return ((Grain)this).PublishExceptionAsync(exception, contextData, methodName, agentType); } /// @@ -79,17 +79,17 @@ protected Task PublishExceptionAsync( /// Operation to execute /// Context data, can be any serializable object /// Whether to rethrow the exception, defaults to true - /// Caller method name, auto-populated - /// Caller class name, auto-populated + /// Caller method name, auto-populated + /// Caller class name, auto-populated /// If an exception occurs, returns the exception event ID; otherwise returns Guid.Empty protected Task CatchAndPublishExceptionAsync( Func action, object? contextData = null, bool rethrowException = true, - [CallerMemberName] string? callerMemberName = null, - [CallerFilePath] string? callerClassName = null) + string? methodName = null, + string? agentType = null) { - return ((Grain)this).CatchAndPublishExceptionAsync(action, contextData, rethrowException, callerMemberName, callerClassName); + return ((Grain)this).CatchAndPublishExceptionAsync(action, contextData, rethrowException, methodName, agentType); } /// @@ -100,18 +100,18 @@ protected Task CatchAndPublishExceptionAsync( /// Default value to return if an exception occurs and is not rethrown /// Context data, can be any serializable object /// Whether to rethrow the exception, defaults to true - /// Caller method name, auto-populated - /// Caller class name, auto-populated + /// Caller method name, auto-populated + /// Caller class name, auto-populated /// If the operation succeeds, returns the operation result; if an exception occurs and is not rethrown, returns the default value protected Task<(TResult Result, Guid ExceptionId)> CatchAndPublishExceptionAsync( Func> func, TResult defaultValue = default!, object? contextData = null, bool rethrowException = true, - [CallerMemberName] string? callerMemberName = null, - [CallerFilePath] string? callerClassName = null) + string? methodName = null, + string? agentType = null) { - return ((Grain)this).CatchAndPublishExceptionAsync(func, defaultValue, contextData, rethrowException, callerMemberName, callerClassName); + return ((Grain)this).CatchAndPublishExceptionAsync(func, defaultValue, contextData, rethrowException, methodName, agentType); } public async Task ActivateAsync() From a8d69155ece5ddbcdf75d0feebc3275867bbe763 Mon Sep 17 00:00:00 2001 From: Jeff-Xu23 Date: Tue, 29 Apr 2025 17:20:52 +0800 Subject: [PATCH 5/5] feat:add feature md and aevatar-core.mdc --- .cursor/rules/aevatar-core.mdc | 17 +++++++++++++++++ features/catch-and-publish-exception.md | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 .cursor/rules/aevatar-core.mdc create mode 100644 features/catch-and-publish-exception.md diff --git a/.cursor/rules/aevatar-core.mdc b/.cursor/rules/aevatar-core.mdc new file mode 100644 index 00000000..4eff5306 --- /dev/null +++ b/.cursor/rules/aevatar-core.mdc @@ -0,0 +1,17 @@ +--- +description: +globs: +alwaysApply: true +--- +# Background + (1) The project is developed using C# and uses the Orleans technology stack. GAgent is a wrapper for Orleans Grain. + +# Reference Files + • README.md([README.md](mdc:README.md)): + The aevatar Framework is a distributed actor-based framework built on Microsoft Orleans for creating scalable event-sourced applications. Its core component, GAgentBase, provides event sourcing, pub/sub messaging, state management, and hierarchical agent relationship capabilities. Key features include automatic event forwarding, type-safe state containers, dynamic agent registration, and built-in stream processing. The README demonstrates usage examples for creating agents, handling events, and managing registrations, along with best practices. + + • DIRECTORY_STRUCTURE.md([DIRECTORY_STRUCTURE.md](mdc:docs/DIRECTORY_STRUCTURE.md)): + The DIRECTORY_STRUCTURE.md document provides a comprehensive overview of the Aevatar Framework's organization, highlighting its modular architecture. The framework is structured with clear separation between source code, samples, and tests. The source code (src/) is organized into several key components including Aevatar.Core (containing the GAgentBase implementation), Core.Abstractions (defining interfaces), EventSourcing modules, and plugin systems. The samples directory showcases various implementation examples like ArtifactGAgent, MessagingGAgent, and SimpleAIGAgent. The test directory provides extensive test coverage for all framework components. Built on .NET, the framework leverages event sourcing with MongoDB integration, Orleans for distributed computing, a plugin-based extensibility system, and Generative Agents (GAgents) for AI integration. Key components include GAgents, event sourcing, plugin systems, Orleans integration, and state projections through event streams. + + • Aevatar.Core.md([Aevatar.Core.md](mdc:docs/Aevatar.Core.md)) + The Aevatar.Core.md document illustrates the foundation of the Aevatar Framework through sequence and relationship diagrams. This core module implements the Generative Agent (GAgent) concept - intelligent, stateful, event-driven entities that can communicate through events. Key components include GAgentBase (providing event publishing, subscription, and handling capabilities), GAgentFactory (creating agent instances), GAgentManager (managing agent lifecycle), StateProjection (handling state updates based on events), and EventDispatcher (routing events between agents). The module implements a lightweight event-sourcing pattern where agent state changes are driven by events, creating a network of communicating agents. It supports specialized agent types such as ArtifactGAgent and StateProjectionGAgentBase for different operational needs and advanced state management. The diagrams effectively illustrate the data flow from client interactions through event processing and the relationships between the various components. \ No newline at end of file diff --git a/features/catch-and-publish-exception.md b/features/catch-and-publish-exception.md new file mode 100644 index 00000000..78ed8a67 --- /dev/null +++ b/features/catch-and-publish-exception.md @@ -0,0 +1,17 @@ +# Background + (1) The project is developed using C# and uses the Orleans technology stack. GAgent is a wrapper for Orleans Grain. + (2) It is necessary to capture exceptions in implemented GAgent EventHandlers and notify subscribers of the exception information and corresponding context information through Orleans Stream. + (3) Subscribers do not need to handle exceptions through GAgent's EventHandle, but instead assemble their own StreamProvider and subscribe to the stream. + +# Reference Files + • README.md + • DIRECTORY_STRUCTURE.md + • Aevatar.Core.md + +# Features + Implement a method in the Aevatar.Core project to write exceptions to Orleans Stream, with the following capabilities: + a. Must include exception information, context of the exception, and information such as the time the exception was thrown + b. Requires a separate Kafka Topic to handle exceptions, isolated from business topics, with the exception topic name read from configuration + +# Note + a. Be sure not to modify existing designs and implementations. \ No newline at end of file