From 2d172eb10de664dc199759ddd1cce54754ed4769 Mon Sep 17 00:00:00 2001 From: Petar Marinov Date: Sat, 29 Jun 2024 15:48:19 +0300 Subject: [PATCH] Handle Events With Delegate (#9) * Register a delegate for event handling * Delegate handler load and exeption handling tests * Delegate handler registration unit tests * Delegate execution unit tests * unit tests * Stabilize unit tests * Stabilize unit tests * Stabilize unit tests * Stabilize unit tests * Stabilize unit tests * Stabilize unit tests * Stabilize unit tests * Stabilize unit tests * Stabilize unit tests * Stabilize unit tests * Typos, unused fields, spacing * Update README * Update README * Update build ignore files * Updare readme --- .editorconfig | 8 +- .github/workflows/build.yml | 12 +- README.md | 238 ++++++++++--- docs/event_broker.drawio | 75 ----- docs/event_broker.png | Bin 29820 -> 0 bytes package-readme.md | 28 +- .../DelegateHandlerRegistryBuilder.cs | 113 +++++++ .../DependencyInjection/EventBrokerBuilder.cs | 2 +- .../EventHandlerRegistryBuilder.cs | 2 +- .../ServiceCollectionExtensions.cs | 8 + src/M.EventBrokerSlim/IEventHandler.cs | 3 +- src/M.EventBrokerSlim/INextHandler.cs | 15 + .../Internal/DelegateHandlerDescriptor.cs | 15 + .../Internal/DelegateHandlerRegistry.cs | 35 ++ .../DelegateHandlerRetryDescriptor.cs | 12 + .../Internal/DelegateHelper.cs | 114 +++++++ ...elegateParameterArrayPooledObjectPolicy.cs | 18 + .../Internal/EventHandlerRegistry.cs | 2 +- .../Internal/EventHandlerRetryDescriptor.cs | 12 + .../Internal/ExecutorPooledObjectPolicy.cs | 17 + .../Internal/HandlerExecutionContext.cs | 51 ++- ...ndlerExecutionContextPooledObjectPolicy.cs | 12 +- src/M.EventBrokerSlim/Internal/LogMessages.cs | 5 + .../Internal/RetryDescriptor.cs | 13 +- src/M.EventBrokerSlim/Internal/RetryQueue.cs | 2 +- .../Internal/ThreadPoolEventHandlerRunner.cs | 153 ++++++++- .../DelegateHandlerTests/Events.cs | 9 + .../ExceptionHandlingTests.cs | 113 +++++++ .../HandlerExecutionTests.cs | 318 ++++++++++++++++++ .../DelegateHandlerTests/HandlerSettings.cs | 3 + .../DelegateHandlerTests/LoadTests.cs | 119 +++++++ .../DelegateHandlerTests/RegistrationTests.cs | 259 ++++++++++++++ .../ServiceCollectionExtensions.cs | 42 +++ .../EventBrokerTests.cs | 24 +- test/M.EventBrokerSlim.Tests/EventRecorder.cs | 8 +- test/M.EventBrokerSlim.Tests/EventsTracker.cs | 44 ++- .../ExceptionHandlingTests.cs | 3 +- .../HandlerExecutionTests.cs | 14 +- .../HandlerRegistrationTests.cs | 12 +- .../HandlerScopeAndInstanceTests.cs | 6 +- test/M.EventBrokerSlim.Tests/LoadTests.cs | 10 +- .../MultipleHandlersTests.cs | 2 +- .../OrderOfRetriesTests.cs | 4 +- .../RetryFromHandleTests.cs | 32 +- .../RetryFromHandleUsingDelayDelegateTests.cs | 20 +- .../RetryFromOnErrorTests.cs | 38 ++- ...RetryFromOnErrorUsingDelayDelegateTests.cs | 27 +- .../RetryOverrideFromOnErrorTests.cs | 7 +- .../RetryPolicyTests.cs | 7 +- .../ServiceProviderHelper.cs | 7 + 50 files changed, 1822 insertions(+), 271 deletions(-) delete mode 100644 docs/event_broker.drawio delete mode 100644 docs/event_broker.png create mode 100644 src/M.EventBrokerSlim/DependencyInjection/DelegateHandlerRegistryBuilder.cs create mode 100644 src/M.EventBrokerSlim/INextHandler.cs create mode 100644 src/M.EventBrokerSlim/Internal/DelegateHandlerDescriptor.cs create mode 100644 src/M.EventBrokerSlim/Internal/DelegateHandlerRegistry.cs create mode 100644 src/M.EventBrokerSlim/Internal/DelegateHandlerRetryDescriptor.cs create mode 100644 src/M.EventBrokerSlim/Internal/DelegateHelper.cs create mode 100644 src/M.EventBrokerSlim/Internal/DelegateParameterArrayPooledObjectPolicy.cs create mode 100644 src/M.EventBrokerSlim/Internal/EventHandlerRetryDescriptor.cs create mode 100644 src/M.EventBrokerSlim/Internal/ExecutorPooledObjectPolicy.cs create mode 100644 test/M.EventBrokerSlim.Tests/DelegateHandlerTests/Events.cs create mode 100644 test/M.EventBrokerSlim.Tests/DelegateHandlerTests/ExceptionHandlingTests.cs create mode 100644 test/M.EventBrokerSlim.Tests/DelegateHandlerTests/HandlerExecutionTests.cs create mode 100644 test/M.EventBrokerSlim.Tests/DelegateHandlerTests/HandlerSettings.cs create mode 100644 test/M.EventBrokerSlim.Tests/DelegateHandlerTests/LoadTests.cs create mode 100644 test/M.EventBrokerSlim.Tests/DelegateHandlerTests/RegistrationTests.cs create mode 100644 test/M.EventBrokerSlim.Tests/DelegateHandlerTests/ServiceCollectionExtensions.cs diff --git a/.editorconfig b/.editorconfig index 785844f..be65caa 100644 --- a/.editorconfig +++ b/.editorconfig @@ -48,7 +48,7 @@ csharp_style_expression_bodied_local_functions = false:silent csharp_space_around_binary_operators = before_and_after csharp_style_inlined_variable_declaration = true:suggestion csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent -csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:warning csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent @@ -119,6 +119,8 @@ dotnet_style_prefer_inferred_tuple_names = true:suggestion dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion dotnet_style_prefer_compound_assignment = true:suggestion dotnet_style_namespace_match_folder = true:suggestion -dotnet_style_allow_multiple_blank_lines_experimental = true:silent -dotnet_style_allow_statement_immediately_after_block_experimental = true:silent +dotnet_style_allow_multiple_blank_lines_experimental = false:warning +dotnet_style_allow_statement_immediately_after_block_experimental = false:warning insert_final_newline = true +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f0b8ee2..6a3d497 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,18 +4,18 @@ on: push: branches: [ main ] paths-ignore: - - "*.md" - - "*.png" - - "*.drawio" + - "README.md" + - "package-readme.md" + - "package-icon.png" - ".gitignore" tags: - '[0-9]+.[0-9]+.[0-9]+*' pull_request: branches: [ main ] paths-ignore: - - "*.md" - - "*.png" - - "*.drawio" + - "README.md" + - "package-readme.md" + - "package-icon.png" - ".gitignore" jobs: diff --git a/README.md b/README.md index 8f6544f..efa8a51 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,16 @@ Features: - in-memory, in-process - publishing is *Fire and Forget* style - events don't have to implement specific interface -- event handlers are runned on a `ThreadPool` threads +- event handlers are executed on a `ThreadPool` threads - the number of concurrent handlers running can be limited - built-in retry option - tightly integrated with Microsoft.Extensions.DependencyInjection -- each handler is resolved and runned in a new DI container scope +- each handler is resolved and executed in a new DI container scope +- **NEW** event handlers can be delegates # How does it work -Implement an event handler by implementing `IEventHadler` interface: +Implement an event handler by implementing `IEventHandler` interface: ```csharp public record SomeEvent(string Message); @@ -37,17 +38,28 @@ public class SomeEventHandler : IEventHandler public async Task OnError(Exception exception, SomeEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) { - // called on unhandled exeption from Handle + // called on unhandled exception from Handle // optionally use retryPolicy.RetryAfter(TimeSpan) } } -``` +``` + +or use `DelegateHandlerRegistryBuilder` to register delegate as handler: + +```csharp +DelegateHandlerRegistryBuilder builder = new(); +builder.RegisterHandler( + static async (SomeEvent someEvent, ISomeService service, CancellationToken cancellationToken) => + { + await service.DoSomething(someEvent, cancellationToken); + }); +``` -Add event broker impelementation to DI container using `AddEventBroker` extension method and register handlers: +Add event broker implementation to DI container using `AddEventBroker` extension method and register handlers, optionally add delegate handler registries: ```csharp -serviceCollection.AddEventBroker( - x => x.AddTransient()); +serviceCollection.AddEventBroker(x => x.AddTransient()) + .AddSingleton(builder); ``` Inject `IEventBroker` and publish events: @@ -70,110 +82,232 @@ class MyClass } ``` -# Design +# Overview -`EventBroker` uses `System.Threading.Channels.Channel` to decouple procucers from consumers. +`EventBroker` uses `System.Threading.Channels.Channel` to decouple producers from consumers. There are no limits for publishers. Publishing is as fast as writing an event to a channel. -Event handlers are resolved by event type in a new DI scope which is disposed after handler comletes. Each handler execution is scheduled on the `ThreadPool` without blocking the producer. No more than configured maximum handlers run concurrently. +Event handlers are resolved by event type in a new DI scope which is disposed after handler completes. Each handler execution is scheduled on the `ThreadPool` without blocking the producer. No more than configured maximum handlers run concurrently. -![](docs/event_broker.png) +```mermaid +graph LR; + +subgraph "unlimited producers" + event1["event"] + event2["event"] + event3["event"] +end + +subgraph "event broker" + publish["publish"] + + subgraph "channel" + events(["events"]) + end + + event1 --> publish + event2 --> publish + event3 --> publish + + publish --> events + + subgraph "single consumer" + consumer["resolve\nhandlers"] + end + + events --> consumer + + subgraph "limited concurrent handlers" + handler1["handle(event)"] + handler2["handle(event)"] + end + + consumer --> handler1 + consumer --> handler2 +end + +``` # Details ## Events -Events can be of any type. A best pracice for event is to be immutable - may be processed by multiple handlers in different threads. +Events can be of any type. A best practice for event is to be immutable - may be processed by multiple handlers in different threads. ## Event Handlers -Event handlers have to implement `IEventHandler` interface and to be registered in the DI container. -For each event handler a new DI container scope is created and the event handler is resolved from it. This way it can safely use injected services. -Every event handler is scheduled for execution on the `ThreadPool` without blocking the producer. +Event handlers can be specified in two ways: +- By implementing `IEventHandler` interface and registering the implementation in the DI container. +- By registering a handler delegate using `DelegateHandlerRegistryBuilder` and adding the `DelegateHandlerRegistryBuilder` instance to the DI container. -## Configuration +Both approaches can be used side by side, even for the same event. No matter how handlers are specified, a new DI container scope is created for each event handler. Every event handler is scheduled for execution on the `ThreadPool` without blocking the producer. -`EventBroker` is depending on `Microsoft.Extentions.DependencyInjection` container for resolving event handlers. -It guarantees that each handler is resolved in a new scope which is disposed after the handler completes. +### Event Handlers Implementing `IEventHandler` -`EventBroker` is configured with `AddEventBroker` and `AddEventHandlers` extension methods of `IServiceCollection` using a confiuration delegate. -Event handlers are registered by the event type and a corresponding `IEventHandler` implementation as transient, scoped, or singleton. +When event of type `TEvent` is published, `EventBroker` will resolve each `IEventHandler` implementation from a dedicated scope. This means that additional dependencies can be injected via the handler constructor, also resolved from the same scope. -*Example:* +The parameters of `IEventHandler` methods are managed by `EventBroker`. ```csharp -services.AddEventBroker( - x => x.WithMaxConcurrentHandlers(3) - .DisableMissingHandlerWarningLog() - .AddTransient() - .AddScoped() - .AddSingleton()) +Task Handle(TEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken); + +Task OnError(Exception exception, TEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken); +``` +- `TEvent` - the instance of the published event. +- `IRetryPolicy` - the instance of the retry policy for the handler (see [Retries](#retries) section). +- `CancellationToken` - the `EventBroker` cancellation token. +- `Exception` - exception thrown from `Handle`. + +Since event handlers are executed on the `ThreadPool`, there is nowhere to propagate unhandled exceptions. +An exception thrown from `Handle` method is caught and passed to `OnError` method of the same handler instance (may be on another thread however). +An exception thrown from `OnError` is handled and swallowed and potentially logged (see [Logging](#logging) section). + +### Delegate Event Handlers + +Delegate handlers are registered using `DelegateHandlerRegistryBuilder` against event type. + +```csharp +DelegateHandlerRegistryBuilder builder = new(); +builder.RegisterHandler( + static async (SomeEvent someEvent, ISomeService someService) => + { + await someService.DoSomething(); + }); +``` + +Registered delegate must return a `Task`. Delegate can have 0 to 16 parameters. All of them will be resolved from DI container scope and passed when the delegate is invoked. +There are few special cases of optional parameters managed by `EventBroker` (without being registered in DI container): +- `TEvent` - an instance of the event being handled. Should match the type of the event the delegate was registered for. +- `IRetryPolicy` - the instance of the retry policy for the handler (see [Retries](#retries) section). +- `CancellationToken` - the `EventBroker` cancellation token. +- `INextHandler` - used to call the next wrapper in the chain or the handler if no more wrappers available (see below). + +Delegate handlers do not provide special exception handling. Exception caused by resolving services or unhandled exception during execution will be handled and swallowed and potentially logged (see [Logging](#logging) section). + +Delegate handlers registration has a decorator-like feature allowing to pipeline multiple delegates. The `INextHandler` instance is used to call the next in the pipeline. + +```csharp +builder.RegisterHandler( + static async (SomeEvent someEvent, ISomeService someService) => await someService.DoSomething()) + .WrapWith( + static async (INextHandler next, ILogger logger) + { + try + { + await next.Execute(); + } + catch(Exception ex) + { + logger.LogError(ex); + } + }) + .WrapWith( + static async (SomeEvent someEvent, ILogger logger) + { + Stopwatch timer = new(); + await next.Execute(); + timer.Stop(); + logger.LogInformation("{event} handling duration {elapsed}", someEvent, timer.Elapsed); + }); +``` + +Delegate wrappers are executed from the last registered moving "inwards" toward the handler. + +## DI Configuration + +`EventBroker` is depending on `Microsoft.Extensions.DependencyInjection` container for resolving event handlers and their dependencies. It guarantees that each handler is resolved in a new scope, disposed after the handler completes. There can be multiple handlers for the same event. + +`EventBroker` is configured with `AddEventBroker` extension method of `IServiceCollection` using a configuration delegate. + +```csharp +services.AddEventBroker(x => x.WithMaxConcurrentHandlers(3) + .DisableMissingHandlerWarningLog()); ``` `WithMaxConcurrentHandlers` defines how many handlers can run at the same time. Default is 2. `DisableMissingHandlerWarningLog` suppresses logging warning when there is no handler found for event. -`EventBroker` behavior and event handlers can be configured with separate extension methods. The order of calls to `AddEventBroker` and `AddEventHandlers` does not matter. +### Handlers Implementing `IEventHandler` -*Example:* -```csharp -services.AddEventBroker( - x => x.WithMaxConcurrentHandlers(3) - .DisableMissingHandlerWarningLog()); +Event handlers are registered by the event type and a corresponding `IEventHandler` implementation as transient, scoped, or singleton. `AddEventHandlers` extension method of `IServiceCollection` provides a configuration delegate. +```csharp services.AddEventHandlers( x => x.AddTransient() .AddScoped() .AddSingleton()) ``` -There can be multiple handlers for the same event. +Handler implementations can also be registered in the `AddEventBroker` method. -Note that handlers **not** registered using `AddEventBroker` or `AddEventHandlers` methods will be **ignored** by `EventBroker`. +```csharp +services.AddEventBroker(x => x.WithMaxConcurrentHandlers(3) + .DisableMissingHandlerWarningLog() + .AddTransient() + .AddScoped() + .AddSingleton()); +``` +The order of calls to `AddEventBroker` and `AddEventHandlers` does not matter. `AddEventHandlers` can be called multiple times. -## Publishing Events +> [!WARNING] +> Handlers **not** registered using `AddEventBroker` or `AddEventHandlers` methods will be **ignored** by `EventBroker`. -`IEventBroker` and its implementation are registered in the DI container by the `AddEventBroker` method. +### Delegate Handlers -Events are published using `IEventBroker.Publish` method. +Delegate event handlers are registered using `DelegateHandlerRegistryBuilder` instance. -Events can be published after given time interval with `IEventBroker.PublishDeferred` method. +```csharp +DelegateHandlerRegistryBuilder builder = new(); +builder.RegisterHandler( + static async (SomeEvent someEvent, ISomeService someService) => + { + await someService.DoSomething(); + }); -**Caution**: `PublishDeferred` may not be accurate and may perform badly if large amount of deferred messages are scheduled. It runs a new task that in turn uses `Task.Delay` and then publishes the event. -A lot of `Task.Delay` means a lot of timers waiting in a queue. +services.AddSingleton(builder); +``` -## Exception Handling +Multiple `DelegateHandlerRegistryBuilder` instances can be registered. Delegate handlers can be registered after the service collection has been built. -Since event handlers are executed on the `ThreadPool`, there is nowhere to propagate unhandled ecxeptions. +Registrations after `IEventBroker` instance is resolved are not allowed. -An exception thrown from `Handle` method is caught and passed to `OnError` method of the same handler instance (may be on another thread however). +## Publishing Events -An exception thrown from `OnError` is handled and swallowed and potentially logged. +Events are published using `IEventBroker.Publish` method. + +Events can be published after given time interval with `IEventBroker.PublishDeferred` method. + +**Caution**: `PublishDeferred` may not be accurate and may perform badly if large amount of deferred messages are scheduled. It runs a new task that in turn uses `Task.Delay` and then publishes the event. +A lot of `Task.Delay` means a lot of timers waiting in a queue. ## Logging -If there is logging configured in the DI container, `EventBroker` will use it to log when: +If there is `ILogger` configured in the DI container, `EventBroker` will use it to log when: - There is no event handler found for published event (warning). Can be disabled with `DisableMissingHandlerWarningLog()` during configuration. - Exception is thrown during event handler resolving (error). - Exception is thrown from handlers `OnError()` method (error). +- Exception is thrown from delegate handler (error). If there is no logger configured, these exceptions will be handled and swallowed. ## Retries -Retrying within event hadler can become a bottleneck. Imagine `EventBroker` is restricted to one concurrent handler. An exception is caught in `Handle` and retry is attempted after given time interval. Since `Handle` is not completed, there is no available "slot" to run other handlers while `Handle` is waiting. +Retrying within event handler can become a bottleneck. Imagine `EventBroker` is restricted to one concurrent handler. An exception is caught in `Handle` and retry is attempted after given time interval. Since `Handle` is not completed, there is no available "slot" to run other handlers while `Handle` is waiting. -Another option will be to use `IEventBroker.PublishDeferred`. This will eliminate the bottleneck but will itroduce different problems. The same event will be handled again by all handlers, meaning specaial care should be taken to make all handlers idempotent. Any additional information (e.g. number of retries) needs to be known, it should be carried with the event, introducing accidential complexity. +Another option will be to use `IEventBroker.PublishDeferred`. This will eliminate the bottleneck but will introduce different problems. The same event will be handled again by all handlers, meaning special care should be taken to make all handlers idempotent. Any additional information (e.g. number of retries) needs to be known, it should be carried with the event, introducing accidental complexity. -To avoid these problems, both `IEventBroker` `Handle` and `OnError` methods have `IRetryPolicy` parameter. +To avoid these problems, both `IEventBroker` `Handle` and `OnError` methods have `IRetryPolicy` parameter. It is also available for delegate handlers. `IRetryPolicy.RetryAfter()` will schedule a retry only for the handler it is called from, without blocking. After the given time interval an instance of the handler will be resolved from the DI container (from a new scope) and executed with the same event instance. `IRetryPolicy.Attempt` is the current retry attempt for a given handler and event. `IRetryPolicy.LastDelay` is the time interval before the retry. -`IRetryPolicy.RetryRequested` is used to coordinate retry request between `Handle` and `OnError`. `IRetryPolicy` is passed to both methods to enable error handling and retry request entirely in `Handle` method. `OnError` can check `IRetryPolicy.RetryRequested` to know whether `Hanlde` had called `IRetryPolicy.RetryAfter()`. +`IRetryPolicy.RetryRequested` is used to coordinate retry request between `Handle` and `OnError`. `IRetryPolicy` is passed to both methods to enable error handling and retry request entirely in `Handle` method. `OnError` can check `IRetryPolicy.RetryRequested` to know whether `Handle` had called `IRetryPolicy.RetryAfter()`. + +If added as a parameter, the `IRetryPolicy` will be passed to delegate wrappers and handler. It has the same behavior, allowing delegate handlers to be retired too. -**Caution:** the retry will not be exactly after the specified time interval in `IRetryPolicy.RetryAfter()`. Take into account a tolerance of around 50 milliseconds. Additionally, retry executions respect maximum concurrent handlers setting, meaning a high load can cause additional delay. +> [!WARNING] +> Retry will not be exactly after the specified time interval in `IRetryPolicy.RetryAfter()`. Take into account a tolerance of around 50 milliseconds. Additionally, retry executions respect maximum concurrent handlers setting, meaning a high load can cause additional delay. diff --git a/docs/event_broker.drawio b/docs/event_broker.drawio deleted file mode 100644 index bcdf432..0000000 --- a/docs/event_broker.drawio +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/event_broker.png b/docs/event_broker.png deleted file mode 100644 index 62f8ca48d87eae388c1cd9a3a87f1be40ffccda5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29820 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 diff --git a/package-readme.md b/package-readme.md index 6568959..bfefe57 100644 --- a/package-readme.md +++ b/package-readme.md @@ -6,15 +6,16 @@ Features: - in-memory, in-process - publishing is *Fire and Forget* style - events don't have to implement specific interface -- event handlers are runned on a `ThreadPool` threads +- event handlers are executed on a `ThreadPool` threads - the number of concurrent handlers running can be limited - built-in retry option - tightly integrated with Microsoft.Extensions.DependencyInjection -- each handler is resolved and runned in a new DI container scopee +- each handler is resolved and executed in a new DI container scope +- **NEW** event handlers can be delegates # How does it work -Define an event handler by implementing `IEventHadler` interface: +Implement an event handler by implementing `IEventHandler` interface: ```csharp public record SomeEvent(string Message); @@ -33,18 +34,28 @@ public class SomeEventHandler : IEventHandler public async Task OnError(Exception exception, SomeEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) { - // called on unhandled exeption from Handle + // called on unhandled exception from Handle // optionally use retryPolicy.RetryAfter(TimeSpan) } } -``` +``` +or use `DelegateHandlerRegistryBuilder` to register delegate as handler: -Use `AddEventBroker` extension method to register `IEventBroker` and handlers: +```csharp +DelegateHandlerRegistryBuilder builder = new(); +builder.RegisterHandler( + static async (SomeEvent someEvent, ISomeService service, CancellationToken cancellationToken) => + { + await service.DoSomething(someEvent, cancellationToken); + }); +``` + +Add event broker implementation to DI container using `AddEventBroker` extension method and register handlers, optionally add delegate handler registries: ```csharp -serviceCollection.AddEventBroker( - x => x.AddTransient()); +serviceCollection.AddEventBroker(x => x.AddTransient()) + .AddSingleton(builder); ``` Inject `IEventBroker` and publish events: @@ -66,4 +77,3 @@ class MyClass } } ``` - diff --git a/src/M.EventBrokerSlim/DependencyInjection/DelegateHandlerRegistryBuilder.cs b/src/M.EventBrokerSlim/DependencyInjection/DelegateHandlerRegistryBuilder.cs new file mode 100644 index 0000000..2f277ab --- /dev/null +++ b/src/M.EventBrokerSlim/DependencyInjection/DelegateHandlerRegistryBuilder.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using M.EventBrokerSlim.Internal; + +namespace M.EventBrokerSlim.DependencyInjection; + +/// +/// Used to define event handlers as delegates. +/// +public class DelegateHandlerRegistryBuilder +{ + /// + /// Registers an event handler delegate returning . + /// All of the parameters will be resolved from new DI container scope and injected. The scope will be disposed after delegate execution. + ///
Tip: Use keyword for the anonymous delegate to avoid accidential closure. + ///
+ /// + /// Special objects available (without being registered in DI container): + /// + /// - an instance of the event being handled. + /// - a cancellation token that should be used to cancel the work. + /// - provides ability to request a retry for the same event by the handler. Do not keep a reference to this instance, it may be pooled and reused + /// + /// + /// The type of the event being handled. + /// A delegate returning that will be executed when event of type is published. + /// Object allowing to fluently continue registering handlers or build handler pipeline. + /// Thrown when registry is closed for new registrations. Usually after an instance of has been resolved. + public DelegateHandlerWrapperBuilder RegisterHandler(Delegate handler) + { + if(IsClosed) + { + throw new InvalidOperationException("Registry is closed. Please complete registrations before IEventBroker is resolved."); + } + + var descriptor = DelegateHelper.BuildDelegateHandlerDescriptor(handler, typeof(TEvent)); + HandlerDescriptors.Add(descriptor); + return new DelegateHandlerWrapperBuilder(this, descriptor); + } + + /// + /// Indicates whether the registry can still be used to register event handlers. + /// + public bool IsClosed { get; private set; } + + internal List HandlerDescriptors { get; } = new(); + + internal static DelegateHandlerRegistry Build(IEnumerable builders) + { + return new DelegateHandlerRegistry( + builders.SelectMany(x => + { + x.IsClosed = true; + return x.HandlerDescriptors; + })); + } + + /// + /// Used to define pipeline of delegates wrapping the event handler. + /// + /// + /// Order of execution is in reverse of registration order. The last registered is executed first, moving "inwards" toward the handler. + /// + public class DelegateHandlerWrapperBuilder + { + private readonly DelegateHandlerRegistryBuilder _builder; + private readonly DelegateHandlerDescriptor _handlerDescriptor; + + internal DelegateHandlerWrapperBuilder(DelegateHandlerRegistryBuilder builder, DelegateHandlerDescriptor handlerDescriptor) + { + _builder = builder; + _handlerDescriptor = handlerDescriptor; + } + + /// + /// Returns the current instance, allowing to continue registering event handlers. + /// + /// Current instance. + public DelegateHandlerRegistryBuilder Builder() => _builder; + + /// + /// Registers a delegate returning executed before the event handler delegate. Use to call the next wrapper in the chain. + /// All of the parameters will be resolved from new DI container scope and injected. All wrappers and the handler share the same DI container scope. Order of execution is in reverse of registration order. The last registered is executed first, moving "inwards" toward the handler. + ///
Tip: Use keyword for the anonymous delegate to avoid accidential closure. + ///
+ /// + /// Special objects available (without being registered in DI container): + /// + /// - used to call the next wrapper in the chain or the handler if no more wrappers available. Do not keep a reference to this instance, it may be pooled and reused + /// TEvent - an instance of the event being handled. + /// - a cancellation token that should be used to cancel the work. + /// - provides ability to request a retry for the same event by the handler. Do not keep a reference to this instance, it may be pooled and reused + /// + /// + /// A delegate returning that will be executed when event of type TEvent is published, before the handler delegate. + /// Object allowing to fluently continue registering handlers or build handler pipeline. + /// Thrown when registry is closed for new registrations. Usually after an instance of has been resolved. + public DelegateHandlerWrapperBuilder WrapWith(Delegate wrapper) + { + if(_builder.IsClosed) + { + throw new InvalidOperationException("Registry is closed. Please complete registrations before IEventBroker is resolved."); + } + + var descriptor = DelegateHelper.BuildDelegateHandlerDescriptor(wrapper, _handlerDescriptor.EventType); + _handlerDescriptor.Pipeline.Add(descriptor); + return this; + } + } +} diff --git a/src/M.EventBrokerSlim/DependencyInjection/EventBrokerBuilder.cs b/src/M.EventBrokerSlim/DependencyInjection/EventBrokerBuilder.cs index 66b7558..a94c77a 100644 --- a/src/M.EventBrokerSlim/DependencyInjection/EventBrokerBuilder.cs +++ b/src/M.EventBrokerSlim/DependencyInjection/EventBrokerBuilder.cs @@ -26,7 +26,7 @@ public EventBrokerBuilder WithMaxConcurrentHandlers(int maxConcurrentHandlers) { if(maxConcurrentHandlers <= 0) { - throw new ArgumentOutOfRangeException(nameof(maxConcurrentHandlers), "MaxConcurrentHandlers should be greater than zero"); + throw new ArgumentOutOfRangeException(nameof(maxConcurrentHandlers), "MaxConcurrentHandlers should be greater than zero."); } _maxConcurrentHandlers = maxConcurrentHandlers; diff --git a/src/M.EventBrokerSlim/DependencyInjection/EventHandlerRegistryBuilder.cs b/src/M.EventBrokerSlim/DependencyInjection/EventHandlerRegistryBuilder.cs index a756725..d609b72 100644 --- a/src/M.EventBrokerSlim/DependencyInjection/EventHandlerRegistryBuilder.cs +++ b/src/M.EventBrokerSlim/DependencyInjection/EventHandlerRegistryBuilder.cs @@ -6,7 +6,7 @@ namespace M.EventBrokerSlim.DependencyInjection; /// -/// Registers EventBorker event handlers in DI container. +/// Registers EventBroker event handlers in DI container. /// public class EventHandlerRegistryBuilder { diff --git a/src/M.EventBrokerSlim/DependencyInjection/ServiceCollectionExtensions.cs b/src/M.EventBrokerSlim/DependencyInjection/ServiceCollectionExtensions.cs index 5dc4216..2384f4b 100644 --- a/src/M.EventBrokerSlim/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/M.EventBrokerSlim/DependencyInjection/ServiceCollectionExtensions.cs @@ -57,6 +57,7 @@ public static IServiceCollection AddEventBroker( x.GetRequiredKeyedService>(eventBrokerKey), x.GetRequiredService(), x.GetRequiredService(), + x.GetRequiredService(), x.GetRequiredKeyedService(eventBrokerKey), x.GetService>())); @@ -67,6 +68,13 @@ public static IServiceCollection AddEventBroker( return EventHandlerRegistryBuilder.Build(builders); }); + serviceCollection.AddSingleton( + x => + { + var builders = x.GetServices(); + return DelegateHandlerRegistryBuilder.Build(builders); + }); + return serviceCollection; } diff --git a/src/M.EventBrokerSlim/IEventHandler.cs b/src/M.EventBrokerSlim/IEventHandler.cs index 76c48f9..05bdcbd 100644 --- a/src/M.EventBrokerSlim/IEventHandler.cs +++ b/src/M.EventBrokerSlim/IEventHandler.cs @@ -1,7 +1,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using M.EventBrokerSlim.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -22,7 +21,7 @@ public interface IEventHandler Task Handle(TEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken); /// - /// Called when an unhadled exception is caught during execution. + /// Called when an unhandled exception is caught during execution. /// Exceptions thrown from this method are swallowed. /// If there is configured in the an Error will be logged. /// diff --git a/src/M.EventBrokerSlim/INextHandler.cs b/src/M.EventBrokerSlim/INextHandler.cs new file mode 100644 index 0000000..265bd9a --- /dev/null +++ b/src/M.EventBrokerSlim/INextHandler.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; + +namespace M.EventBrokerSlim; + +/// +/// Inject as a delegate event handler parameter and use to call next delegate in event handling pipeline. +/// +public interface INextHandler +{ + /// + /// Executes the next delegate in the event handling pipeline. + /// + /// The task object representing the asynchronous operation. + Task Execute(); +} diff --git a/src/M.EventBrokerSlim/Internal/DelegateHandlerDescriptor.cs b/src/M.EventBrokerSlim/Internal/DelegateHandlerDescriptor.cs new file mode 100644 index 0000000..62bc59a --- /dev/null +++ b/src/M.EventBrokerSlim/Internal/DelegateHandlerDescriptor.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace M.EventBrokerSlim.Internal; + +internal sealed class DelegateHandlerDescriptor +{ + public required Type EventType { get; init; } + + public required Type[] ParamTypes { get; init; } + + public required object Handler { get; init; } + + public List Pipeline { get; } = new(); +} diff --git a/src/M.EventBrokerSlim/Internal/DelegateHandlerRegistry.cs b/src/M.EventBrokerSlim/Internal/DelegateHandlerRegistry.cs new file mode 100644 index 0000000..2dfadfd --- /dev/null +++ b/src/M.EventBrokerSlim/Internal/DelegateHandlerRegistry.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace M.EventBrokerSlim.Internal; + +internal sealed class DelegateHandlerRegistry +{ + private readonly FrozenDictionary> _handlers; + + public DelegateHandlerRegistry(IEnumerable delegateHandlerDescriptors) + { + _handlers = delegateHandlerDescriptors + .GroupBy(x => x.EventType) + .ToFrozenDictionary(x => x.Key, x => x.ToImmutableArray()); + } + + public ImmutableArray GetHandlers(Type eventType) + { + _handlers.TryGetValue(eventType, out ImmutableArray handlers); + return handlers.IsDefaultOrEmpty ? ImmutableArray.Empty : handlers; + } + + internal int MaxPipelineLength() + { + if(_handlers.Count == 0) + { + return 0; + } + + return _handlers.SelectMany(x => x.Value.Select(p => p.Pipeline.Count)).Max() + 1; + } +} diff --git a/src/M.EventBrokerSlim/Internal/DelegateHandlerRetryDescriptor.cs b/src/M.EventBrokerSlim/Internal/DelegateHandlerRetryDescriptor.cs new file mode 100644 index 0000000..c8bf9d7 --- /dev/null +++ b/src/M.EventBrokerSlim/Internal/DelegateHandlerRetryDescriptor.cs @@ -0,0 +1,12 @@ +namespace M.EventBrokerSlim.Internal; + +internal sealed class DelegateHandlerRetryDescriptor : RetryDescriptor +{ + public DelegateHandlerRetryDescriptor(object @event, DelegateHandlerDescriptor delegateHandlerDescriptor, RetryPolicy retryPolicy) : + base(@event, retryPolicy) + { + DelegateHandlerDescriptor = delegateHandlerDescriptor; + } + + public DelegateHandlerDescriptor DelegateHandlerDescriptor { get; } +} diff --git a/src/M.EventBrokerSlim/Internal/DelegateHelper.cs b/src/M.EventBrokerSlim/Internal/DelegateHelper.cs new file mode 100644 index 0000000..ae6db99 --- /dev/null +++ b/src/M.EventBrokerSlim/Internal/DelegateHelper.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; + +namespace M.EventBrokerSlim.Internal; + +internal static class DelegateHelper +{ + public static DelegateHandlerDescriptor BuildDelegateHandlerDescriptor(Delegate delegateHandler, Type eventType) + { + if(delegateHandler.Method.ReturnType != typeof(Task)) + { + throw new ArgumentException("Delegate must return a Task."); + } + + ParameterInfo[] delegateParameters = delegateHandler.Method.GetParameters(); + if(delegateParameters.Length > 16) + { + throw new ArgumentException("Delegate can't have more than 16 arguments."); + } + + return new() + { + EventType = eventType, + ParamTypes = delegateParameters.Select(x => x.ParameterType).ToArray(), + Handler = CompileDelegateToFunc(delegateHandler, delegateParameters) + }; + } + + private static object CompileDelegateToFunc(Delegate delegateHandler, ParameterInfo[] delegateParameters) + { + List parametersAsObject = new(); + List parametersCastToOriginalType = new(); + foreach(ParameterInfo parameterInfo in delegateParameters) + { + parametersAsObject.Add(Expression.Parameter(typeof(object))); + parametersCastToOriginalType.Add(Expression.Convert(parametersAsObject.Last(), parameterInfo.ParameterType)); + } + + var call = Expression.Call( + instance: delegateHandler.Target is null ? null : Expression.Constant(delegateHandler.Target), + method: delegateHandler.Method, + arguments: parametersCastToOriginalType); + + return delegateParameters.Length switch + { + 0 => Expression.Lambda>(call) + .Compile(), + 1 => Expression.Lambda>(call, parametersAsObject[0]) + .Compile(), + 2 => Expression.Lambda>(call, parametersAsObject) + .Compile(), + 3 => Expression.Lambda>(call, parametersAsObject) + .Compile(), + 4 => Expression.Lambda>(call, parametersAsObject) + .Compile(), + 5 => Expression.Lambda>(call, parametersAsObject) + .Compile(), + 6 => Expression.Lambda>(call, parametersAsObject) + .Compile(), + 7 => Expression.Lambda>(call, parametersAsObject) + .Compile(), + 8 => Expression.Lambda>(call, parametersAsObject) + .Compile(), + 9 => Expression.Lambda>(call, parametersAsObject) + .Compile(), + 10 => Expression.Lambda>(call, parametersAsObject) + .Compile(), + 11 => Expression.Lambda>(call, parametersAsObject) + .Compile(), + 12 => Expression.Lambda>(call, parametersAsObject) + .Compile(), + 13 => Expression.Lambda>(call, parametersAsObject) + .Compile(), + 14 => Expression.Lambda>(call, parametersAsObject) + .Compile(), + 15 => Expression.Lambda>(call, parametersAsObject) + .Compile(), + 16 => Expression.Lambda>(call, parametersAsObject) + .Compile(), + _ => throw new InvalidOperationException("Can't convert Delegate with more than 16 arguments to Func."), + }; + } + + public static async Task ExecuteDelegateHandler(object handler, object[] parameters, int parametersCount) + { + var call = parametersCount switch + { + 0 => ((Func)handler)(), + 1 => ((Func)handler)(parameters[0]), + 2 => ((Func)handler)(parameters[0], parameters[1]), + 3 => ((Func)handler)(parameters[0], parameters[1], parameters[2]), + 4 => ((Func)handler)(parameters[0], parameters[1], parameters[2], parameters[3]), + 5 => ((Func)handler)(parameters[0], parameters[1], parameters[2], parameters[3], parameters[4]), + 6 => ((Func)handler)(parameters[0], parameters[1], parameters[2], parameters[3], parameters[4], parameters[5]), + 7 => ((Func)handler)(parameters[0], parameters[1], parameters[2], parameters[3], parameters[4], parameters[5], parameters[6]), + 8 => ((Func)handler)(parameters[0], parameters[1], parameters[2], parameters[3], parameters[4], parameters[5], parameters[6], parameters[7]), + 9 => ((Func)handler)(parameters[0], parameters[1], parameters[2], parameters[3], parameters[4], parameters[5], parameters[6], parameters[7], parameters[8]), + 10 => ((Func)handler)(parameters[0], parameters[1], parameters[2], parameters[3], parameters[4], parameters[5], parameters[6], parameters[7], parameters[8], parameters[9]), + 11 => ((Func)handler)(parameters[0], parameters[1], parameters[2], parameters[3], parameters[4], parameters[5], parameters[6], parameters[7], parameters[8], parameters[9], parameters[10]), + 12 => ((Func)handler)(parameters[0], parameters[1], parameters[2], parameters[3], parameters[4], parameters[5], parameters[6], parameters[7], parameters[8], parameters[9], parameters[10], parameters[11]), + 13 => ((Func)handler)(parameters[0], parameters[1], parameters[2], parameters[3], parameters[4], parameters[5], parameters[6], parameters[7], parameters[8], parameters[9], parameters[10], parameters[11], parameters[12]), + 14 => ((Func)handler)(parameters[0], parameters[1], parameters[2], parameters[3], parameters[4], parameters[5], parameters[6], parameters[7], parameters[8], parameters[9], parameters[10], parameters[11], parameters[12], parameters[13]), + 15 => ((Func)handler)(parameters[0], parameters[1], parameters[2], parameters[3], parameters[4], parameters[5], parameters[6], parameters[7], parameters[8], parameters[9], parameters[10], parameters[11], parameters[12], parameters[13], parameters[14]), + 16 => ((Func)handler)(parameters[0], parameters[1], parameters[2], parameters[3], parameters[4], parameters[5], parameters[6], parameters[7], parameters[8], parameters[9], parameters[10], parameters[11], parameters[12], parameters[13], parameters[14], parameters[15]), + _ => throw new InvalidOperationException("Can't execute Func with more than 16 arguments."), + }; + + await call; + } +} diff --git a/src/M.EventBrokerSlim/Internal/DelegateParameterArrayPooledObjectPolicy.cs b/src/M.EventBrokerSlim/Internal/DelegateParameterArrayPooledObjectPolicy.cs new file mode 100644 index 0000000..4628f04 --- /dev/null +++ b/src/M.EventBrokerSlim/Internal/DelegateParameterArrayPooledObjectPolicy.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.Extensions.ObjectPool; + +namespace M.EventBrokerSlim.Internal; + +internal sealed class DelegateParameterArrayPooledObjectPolicy : IPooledObjectPolicy +{ + public object[] Create() + { + return new object[16]; + } + + public bool Return(object[] obj) + { + Array.Clear(obj); + return true; + } +} diff --git a/src/M.EventBrokerSlim/Internal/EventHandlerRegistry.cs b/src/M.EventBrokerSlim/Internal/EventHandlerRegistry.cs index a28ed38..7a20772 100644 --- a/src/M.EventBrokerSlim/Internal/EventHandlerRegistry.cs +++ b/src/M.EventBrokerSlim/Internal/EventHandlerRegistry.cs @@ -25,6 +25,6 @@ public EventHandlerRegistry(List descriptors, int maxCon internal ImmutableArray GetEventHandlers(Type eventType) { _eventHandlerDescriptors.TryGetValue(eventType, out var handlers); - return handlers; + return handlers.IsDefault ? ImmutableArray.Empty : handlers; } } diff --git a/src/M.EventBrokerSlim/Internal/EventHandlerRetryDescriptor.cs b/src/M.EventBrokerSlim/Internal/EventHandlerRetryDescriptor.cs new file mode 100644 index 0000000..a0f959f --- /dev/null +++ b/src/M.EventBrokerSlim/Internal/EventHandlerRetryDescriptor.cs @@ -0,0 +1,12 @@ +namespace M.EventBrokerSlim.Internal; + +internal sealed class EventHandlerRetryDescriptor : RetryDescriptor +{ + public EventHandlerRetryDescriptor(object @event, EventHandlerDescriptor eventHandlerDescriptor, RetryPolicy retryPolicy) : + base(@event, retryPolicy) + { + EventHandlerDescriptor = eventHandlerDescriptor; + } + + public EventHandlerDescriptor EventHandlerDescriptor { get; } +} diff --git a/src/M.EventBrokerSlim/Internal/ExecutorPooledObjectPolicy.cs b/src/M.EventBrokerSlim/Internal/ExecutorPooledObjectPolicy.cs new file mode 100644 index 0000000..929a716 --- /dev/null +++ b/src/M.EventBrokerSlim/Internal/ExecutorPooledObjectPolicy.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.ObjectPool; + +namespace M.EventBrokerSlim.Internal; + +internal sealed class ExecutorPooledObjectPolicy : IPooledObjectPolicy +{ + public ThreadPoolEventHandlerRunner.Executor Create() + { + return new ThreadPoolEventHandlerRunner.Executor(); + } + + public bool Return(ThreadPoolEventHandlerRunner.Executor obj) + { + obj.Clear(); + return true; + } +} diff --git a/src/M.EventBrokerSlim/Internal/HandlerExecutionContext.cs b/src/M.EventBrokerSlim/Internal/HandlerExecutionContext.cs index df4a239..c992822 100644 --- a/src/M.EventBrokerSlim/Internal/HandlerExecutionContext.cs +++ b/src/M.EventBrokerSlim/Internal/HandlerExecutionContext.cs @@ -7,7 +7,7 @@ namespace M.EventBrokerSlim.Internal; -internal class HandlerExecutionContext +internal sealed class HandlerExecutionContext { private readonly DefaultObjectPool _retryPolicyPool; private readonly SemaphoreSlim _semaphore; @@ -15,8 +15,18 @@ internal class HandlerExecutionContext private readonly ILogger _logger; private readonly DefaultObjectPool _contextObjectPool; private readonly RetryQueue _retryQueue; - - public HandlerExecutionContext(DefaultObjectPool retryPolicyPool, SemaphoreSlim semaphore, IServiceScopeFactory serviceScopeFactory, ILogger logger, DefaultObjectPool contextObjectPool, RetryQueue retryQueue) + private readonly DefaultObjectPool _delegateParametersArrayObjectPool; + private readonly DefaultObjectPool _executorPool; + + public HandlerExecutionContext( + DefaultObjectPool retryPolicyPool, + SemaphoreSlim semaphore, + IServiceScopeFactory serviceScopeFactory, + ILogger logger, + DefaultObjectPool contextObjectPool, + RetryQueue retryQueue, + DefaultObjectPool delegateParametersArrayObjectPool, + DefaultObjectPool executorPool) { _retryPolicyPool = retryPolicyPool; _semaphore = semaphore; @@ -24,12 +34,15 @@ public HandlerExecutionContext(DefaultObjectPool retryPolicyPool, S _logger = logger; _contextObjectPool = contextObjectPool; _retryQueue = retryQueue; + _delegateParametersArrayObjectPool = delegateParametersArrayObjectPool; + _executorPool = executorPool; } - public HandlerExecutionContext Initialize(object @event, EventHandlerDescriptor eventHandlerDescriptor, RetryDescriptor? retryDescriptor, CancellationToken cancellationToken) + public HandlerExecutionContext Initialize(object @event, EventHandlerDescriptor? eventHandlerDescriptor, DelegateHandlerDescriptor? delegateHandlerDescriptor, RetryDescriptor? retryDescriptor, CancellationToken cancellationToken) { Event = @event; EventHandlerDescriptor = eventHandlerDescriptor; + DelegateHandlerDescriptor = delegateHandlerDescriptor; RetryDescriptor = retryDescriptor; CancellationToken = cancellationToken; @@ -41,6 +54,7 @@ public void Clear() { Event = null; EventHandlerDescriptor = null; + DelegateHandlerDescriptor = null; RetryDescriptor = null; CancellationToken = default; @@ -51,6 +65,8 @@ public void Clear() public EventHandlerDescriptor? EventHandlerDescriptor { get; private set; } + public DelegateHandlerDescriptor? DelegateHandlerDescriptor { get; private set; } + public RetryDescriptor? RetryDescriptor { get; private set; } public CancellationToken CancellationToken { get; private set; } @@ -65,10 +81,18 @@ public async Task CompleteAsync() // first retry if(RetryDescriptor is null) { - RetryDescriptor = new RetryDescriptor(Event!, EventHandlerDescriptor!, RetryPolicy); + if(EventHandlerDescriptor is not null) + { + RetryDescriptor = new EventHandlerRetryDescriptor(Event!, EventHandlerDescriptor!, RetryPolicy); + } + + if(DelegateHandlerDescriptor is not null) + { + RetryDescriptor = new DelegateHandlerRetryDescriptor(Event!, DelegateHandlerDescriptor!, RetryPolicy); + } } - await _retryQueue.Enqueue(RetryDescriptor).ConfigureAwait(false); + await _retryQueue.Enqueue(RetryDescriptor!).ConfigureAwait(false); } else { @@ -85,9 +109,20 @@ public object GetService(IServiceProvider serviceProvider) public IServiceScope CreateScope() => _serviceScopeFactory.CreateScope(); - public void LogEventHandlerResolvingError(Exception exception) + public object[] BorrowDelegateParametersArray() => _delegateParametersArrayObjectPool.Get(); + + public void ReturnDelegateParametersArray(object[] parametersArray) => _delegateParametersArrayObjectPool.Return(parametersArray); + + public ThreadPoolEventHandlerRunner.Executor BorrowExecutor() => _executorPool.Get(); + + public void ReturnExecutor(ThreadPoolEventHandlerRunner.Executor executor) => _executorPool.Return(executor); + + internal void LogEventHandlerResolvingError(Exception exception) => _logger.LogEventHandlerResolvingError(Event!.GetType(), exception); - public void LogUnhandledExceptionFromOnError(Type serviceType, Exception exception) + internal void LogUnhandledExceptionFromOnError(Type serviceType, Exception exception) => _logger.LogUnhandledExceptionFromOnError(serviceType, exception); + + internal void LogDelegateEventHandlerError(Type eventType, Exception exception) + => _logger.LogDelegateEventHandlerError(eventType, exception); } diff --git a/src/M.EventBrokerSlim/Internal/HandlerExecutionContextPooledObjectPolicy.cs b/src/M.EventBrokerSlim/Internal/HandlerExecutionContextPooledObjectPolicy.cs index d5b8f20..dc91cf7 100644 --- a/src/M.EventBrokerSlim/Internal/HandlerExecutionContextPooledObjectPolicy.cs +++ b/src/M.EventBrokerSlim/Internal/HandlerExecutionContextPooledObjectPolicy.cs @@ -5,32 +5,38 @@ namespace M.EventBrokerSlim.Internal; -internal class HandlerExecutionContextPooledObjectPolicy : IPooledObjectPolicy +internal sealed class HandlerExecutionContextPooledObjectPolicy : IPooledObjectPolicy { private readonly DefaultObjectPool _retryPolicyPool; private readonly SemaphoreSlim _semaphore; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly ILogger _logger; private readonly RetryQueue _retryQueue; + private readonly DefaultObjectPool _delegateParametersArrayObjectPool; + private readonly DefaultObjectPool _executorPool; internal HandlerExecutionContextPooledObjectPolicy( DefaultObjectPool retryPolicyPool, SemaphoreSlim semaphore, IServiceScopeFactory serviceScopeFactory, ILogger logger, - RetryQueue retryQueue) + RetryQueue retryQueue, + DefaultObjectPool delegateParametersArrayObjectPool, + DefaultObjectPool executorPool) { _retryPolicyPool = retryPolicyPool; _semaphore = semaphore; _serviceScopeFactory = serviceScopeFactory; _logger = logger; _retryQueue = retryQueue; + _delegateParametersArrayObjectPool = delegateParametersArrayObjectPool; + _executorPool = executorPool; } internal DefaultObjectPool? ContextObjectPool { get; set; } public HandlerExecutionContext Create() - => new HandlerExecutionContext(_retryPolicyPool, _semaphore, _serviceScopeFactory, _logger, ContextObjectPool!, _retryQueue); + => new HandlerExecutionContext(_retryPolicyPool, _semaphore, _serviceScopeFactory, _logger, ContextObjectPool!, _retryQueue, _delegateParametersArrayObjectPool, _executorPool); public bool Return(HandlerExecutionContext obj) { diff --git a/src/M.EventBrokerSlim/Internal/LogMessages.cs b/src/M.EventBrokerSlim/Internal/LogMessages.cs index 6fae520..cf3aeeb 100644 --- a/src/M.EventBrokerSlim/Internal/LogMessages.cs +++ b/src/M.EventBrokerSlim/Internal/LogMessages.cs @@ -21,4 +21,9 @@ internal static partial void LogUnhandledExceptionFromOnError( Message = "No event handler found for event {eventType}", Level = LogLevel.Warning)] internal static partial void LogNoEventHandlerForEvent(this ILogger logger, Type eventType); + + [LoggerMessage( + Message = "Unhandled exception executing delegate handler for event {eventType}", + Level = LogLevel.Error)] + internal static partial void LogDelegateEventHandlerError(this ILogger logger, Type eventType, Exception exception); } diff --git a/src/M.EventBrokerSlim/Internal/RetryDescriptor.cs b/src/M.EventBrokerSlim/Internal/RetryDescriptor.cs index 2f65147..77ed715 100644 --- a/src/M.EventBrokerSlim/Internal/RetryDescriptor.cs +++ b/src/M.EventBrokerSlim/Internal/RetryDescriptor.cs @@ -1,3 +1,14 @@ namespace M.EventBrokerSlim.Internal; -internal record RetryDescriptor(object Event, EventHandlerDescriptor EventHandlerDescriptor, RetryPolicy RetryPolicy); +internal abstract class RetryDescriptor +{ + protected RetryDescriptor(object @event, RetryPolicy retryPolicy) + { + Event = @event; + RetryPolicy = retryPolicy; + } + + public object Event { get; } + + public RetryPolicy RetryPolicy { get; } +} diff --git a/src/M.EventBrokerSlim/Internal/RetryQueue.cs b/src/M.EventBrokerSlim/Internal/RetryQueue.cs index a3106e1..5f7fb90 100644 --- a/src/M.EventBrokerSlim/Internal/RetryQueue.cs +++ b/src/M.EventBrokerSlim/Internal/RetryQueue.cs @@ -70,7 +70,7 @@ private static async Task Poll(object state) self._polling = true; self._semaphore.Release(); - await Task.Delay(TimeSpan.FromMilliseconds(50), self._cancellationToken).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromMilliseconds(25), self._cancellationToken).ConfigureAwait(false); } } } diff --git a/src/M.EventBrokerSlim/Internal/ThreadPoolEventHandlerRunner.cs b/src/M.EventBrokerSlim/Internal/ThreadPoolEventHandlerRunner.cs index f667e23..e61908e 100644 --- a/src/M.EventBrokerSlim/Internal/ThreadPoolEventHandlerRunner.cs +++ b/src/M.EventBrokerSlim/Internal/ThreadPoolEventHandlerRunner.cs @@ -13,6 +13,7 @@ internal sealed class ThreadPoolEventHandlerRunner { private readonly ChannelReader _channelReader; private readonly EventHandlerRegistry _eventHandlerRegistry; + private readonly DelegateHandlerRegistry _delegateHandlerRegistry; private readonly CancellationTokenSource _cancellationTokenSource; private readonly ILogger _logger; private readonly SemaphoreSlim _semaphore; @@ -22,18 +23,22 @@ internal ThreadPoolEventHandlerRunner( Channel channel, IServiceScopeFactory serviceScopeFactory, EventHandlerRegistry eventHandlerRegistry, + DelegateHandlerRegistry delegateHandlerRegistry, CancellationTokenSource cancellationTokenSource, ILogger? logger) { _channelReader = channel.Reader; _eventHandlerRegistry = eventHandlerRegistry; + _delegateHandlerRegistry = delegateHandlerRegistry; _cancellationTokenSource = cancellationTokenSource; _logger = logger ?? new NullLogger(); _semaphore = new SemaphoreSlim(_eventHandlerRegistry.MaxConcurrentHandlers, _eventHandlerRegistry.MaxConcurrentHandlers); var retryQueue = new RetryQueue(channel.Writer, cancellationTokenSource.Token); var retryPolicyPool = new DefaultObjectPool(new RetryPolicyPooledObjectPolicy(), _eventHandlerRegistry.MaxConcurrentHandlers); - var contextPooledObjectPolicy = new HandlerExecutionContextPooledObjectPolicy(retryPolicyPool, _semaphore, serviceScopeFactory, _logger, retryQueue); + var executorPool = new DefaultObjectPool(new ExecutorPooledObjectPolicy(), _eventHandlerRegistry.MaxConcurrentHandlers * _delegateHandlerRegistry.MaxPipelineLength()); + var delegateParametersArrayObjectPool = new DefaultObjectPool(new DelegateParameterArrayPooledObjectPolicy(), _eventHandlerRegistry.MaxConcurrentHandlers); + var contextPooledObjectPolicy = new HandlerExecutionContextPooledObjectPolicy(retryPolicyPool, _semaphore, serviceScopeFactory, _logger, retryQueue, delegateParametersArrayObjectPool, executorPool); _contextObjectPool = new DefaultObjectPool(contextPooledObjectPolicy, _eventHandlerRegistry.MaxConcurrentHandlers); contextPooledObjectPolicy.ContextObjectPool = _contextObjectPool; } @@ -55,7 +60,8 @@ private async ValueTask ProcessEvents() { var type = @event.GetType(); var eventHandlers = _eventHandlerRegistry.GetEventHandlers(type); - if(eventHandlers == default) + var delegateEventHandlers = _delegateHandlerRegistry.GetHandlers(type); + if(eventHandlers.Length == 0 && delegateEventHandlers.Length == 0) { if(!_eventHandlerRegistry.DisableMissingHandlerWarningLog) { @@ -71,16 +77,35 @@ private async ValueTask ProcessEvents() var eventHandlerDescriptor = eventHandlers[i]; - var context = _contextObjectPool.Get().Initialize(@event, eventHandlerDescriptor, retryDescriptor, token); + var context = _contextObjectPool.Get().Initialize(@event, eventHandlerDescriptor, null, retryDescriptor, token); _ = Task.Factory.StartNew(static async x => await HandleEvent(x!), context); } + + for(int i = 0; i < delegateEventHandlers.Length; i++) + { + await _semaphore.WaitAsync(token).ConfigureAwait(false); + + var delegateHandlerDescriptor = delegateEventHandlers[i]; + + var context = _contextObjectPool.Get().Initialize(@event, null, delegateHandlerDescriptor, retryDescriptor, token); + _ = Task.Factory.StartNew(static async x => await HandleEventWithDelegate(x!), context); + } } else { await _semaphore.WaitAsync(token).ConfigureAwait(false); - var context = _contextObjectPool.Get().Initialize(retryDescriptor.Event, retryDescriptor.EventHandlerDescriptor, retryDescriptor, token); - _ = Task.Factory.StartNew(static async x => await HandleEvent(x!), context); + HandlerExecutionContext? context = null; + if(retryDescriptor is EventHandlerRetryDescriptor) + { + context = _contextObjectPool.Get().Initialize(retryDescriptor.Event, ((EventHandlerRetryDescriptor)retryDescriptor).EventHandlerDescriptor, null, retryDescriptor, token); + _ = Task.Factory.StartNew(static async x => await HandleEvent(x!), context); + } + else if(retryDescriptor is DelegateHandlerRetryDescriptor) + { + context = _contextObjectPool.Get().Initialize(retryDescriptor.Event, null, ((DelegateHandlerRetryDescriptor)retryDescriptor).DelegateHandlerDescriptor, retryDescriptor, token); + _ = Task.Factory.StartNew(static async x => await HandleEventWithDelegate(x!), context); + } } } } @@ -121,7 +146,7 @@ private static async Task HandleEvent(object state) } catch(Exception errorHandlingException) { - // suppress further exeptions + // suppress further exceptions context.LogUnhandledExceptionFromOnError(service.GetType(), errorHandlingException); } } @@ -130,4 +155,120 @@ private static async Task HandleEvent(object state) await context.CompleteAsync().ConfigureAwait(false); } } + + private static async Task HandleEventWithDelegate(object state) + { + var context = (HandlerExecutionContext)state; + var retryPolicy = context.RetryPolicy!; + var @event = context.Event!; + + if(context.CancellationToken.IsCancellationRequested) + { + return; + } + + DelegateHandlerDescriptor handlerDescriptor = context.DelegateHandlerDescriptor!; + + object[] services = context.BorrowDelegateParametersArray(); + using var scope = context.CreateScope(); + var executor = context.BorrowExecutor().Initialize(scope, handlerDescriptor, context.Event!, services, retryPolicy, context.CancellationToken); + try + { + await executor.Execute(); + } + catch(Exception exception) + { + context.LogDelegateEventHandlerError(handlerDescriptor.EventType, exception); + } + finally + { + context.ReturnDelegateParametersArray(services); + context.ReturnExecutor(executor); + await context.CompleteAsync().ConfigureAwait(false); + } + } + + internal class Executor : INextHandler + { + private static readonly int _endOfPipeline = -1; + +#pragma warning disable CS8618 // Justification: Due to object pooling, these cannot be passed in the constructor. + private IServiceScope _scope; + private DelegateHandlerDescriptor _handler; + private object _event; + private object[] _parameters; + private IRetryPolicy _retryPolicy; + private CancellationToken _cancellationToken; +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + + private int _currentHandler; + + public Executor Initialize(IServiceScope scope, DelegateHandlerDescriptor handler, object @event, object[] parameters, IRetryPolicy retryPolicy, CancellationToken cancellationToken) + { + _scope = scope; + _handler = handler; + _event = @event; + _parameters = parameters; + _retryPolicy = retryPolicy; + _cancellationToken = cancellationToken; + + _currentHandler = handler.Pipeline.Count > 0 ? handler.Pipeline.Count - 1 : _endOfPipeline; + return this; + } + + public async Task Execute() + { + if(HandlerAlreadyExecuted()) + { + // if handler call INextHandler.Execute() - do nothing, causing it to continue execution + return; + } + + var handler = ShouldExecuteHandler() ? _handler : _handler.Pipeline[_currentHandler]; + _currentHandler--; + Array.Clear(_parameters); + for(int i = 0; i < handler.ParamTypes.Length; i++) + { + if(handler.ParamTypes[i] == _event.GetType()) + { + _parameters[i] = _event; + } + else if(handler.ParamTypes[i] == typeof(CancellationToken)) + { + _parameters[i] = _cancellationToken; + } + else if(handler.ParamTypes[i] == typeof(IRetryPolicy)) + { + _parameters[i] = _retryPolicy; + } + else if(handler.ParamTypes[i] == typeof(INextHandler)) + { + _parameters[i] = this; + } + else + { + _parameters[i] = _scope.ServiceProvider.GetRequiredService(handler.ParamTypes[i]); + } + } + + await DelegateHelper.ExecuteDelegateHandler(handler.Handler, _parameters, handler.ParamTypes.Length).ConfigureAwait(false); + } + + private bool ShouldExecuteHandler() => _currentHandler == -1; + + private bool HandlerAlreadyExecuted() => _currentHandler < -1; + + internal void Clear() + { +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + _scope = null; + _handler = null; + _event = null; + _parameters = null; + _retryPolicy = null; + _cancellationToken = default; +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + _currentHandler = 0; + } + } } diff --git a/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/Events.cs b/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/Events.cs new file mode 100644 index 0000000..6a6de4a --- /dev/null +++ b/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/Events.cs @@ -0,0 +1,9 @@ +namespace M.EventBrokerSlim.Tests.DelegateHandlerTests; + +public record TestEventBase(int Number); + +public record Event1(int Number) : TestEventBase(Number); + +public record Event2(int Number) : TestEventBase(Number); + +public record Event3(int Number) : TestEventBase(Number); diff --git a/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/ExceptionHandlingTests.cs b/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/ExceptionHandlingTests.cs new file mode 100644 index 0000000..0c8549b --- /dev/null +++ b/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/ExceptionHandlingTests.cs @@ -0,0 +1,113 @@ +using MELT; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace M.EventBrokerSlim.Tests.DelegateHandlerTests; + +public class ExceptionHandlingTests +{ + private readonly DelegateHandlerRegistryBuilder _builder; + private readonly ITestOutputHelper _output; + private readonly EventsTracker _eventsTracker; + + public ExceptionHandlingTests(ITestOutputHelper output) + { + _output = output; + _builder = new DelegateHandlerRegistryBuilder(); + _eventsTracker = new EventsTracker(); + } + + [Fact] + public async Task Exception_WhenResolvingHandlerParameters_IsLogged() + { + // Arrange + var services = ServiceProviderHelper.BuildWithLogger( + sc => sc.AddEventBroker() + .AddSingleton(_builder) + .AddSingleton(_eventsTracker)); + + _builder.RegisterHandler((string notRegistered) => Task.CompletedTask); + + using var scope = services.CreateScope(); + var eventBroker = scope.ServiceProvider.GetRequiredService(); + + // Act + await eventBroker.Publish(new Event1(1)); + + await _eventsTracker.Wait(TimeSpan.FromSeconds(1)); + + // Assert + _output.WriteLine($"Elapsed: {_eventsTracker.Elapsed}"); + var provider = (TestLoggerProvider)scope.ServiceProvider.GetServices().Single(x => x is TestLoggerProvider); + + var log = Assert.Single(provider.Sink.LogEntries); + Assert.Equal(LogLevel.Error, log.LogLevel); + Assert.Equal($"Unhandled exception executing delegate handler for event {typeof(Event1).FullName}", log.Message); + Assert.Equal("No service for type 'System.String' has been registered.", log.Exception?.Message); + } + + [Fact] + public async Task Unhandled_Exception_WhenExecuting_IsLogged() + { + // Arrange + var services = ServiceProviderHelper.BuildWithLogger( + sc => sc.AddEventBroker() + .AddSingleton(_builder) + .AddSingleton(_eventsTracker)); + + _builder.RegisterHandler(async (Event1 @event, EventsTracker tracker) => + { + await Task.CompletedTask; + tracker.Track(@event); + throw new NotImplementedException(); + }); + + using var scope = services.CreateScope(); + + var eventBroker = scope.ServiceProvider.GetRequiredService(); + + // Act + await eventBroker.Publish(new Event1(1)); + + await _eventsTracker.Wait(timeout: TimeSpan.FromSeconds(1)); + + // Assert + _output.WriteLine($"Elapsed: {_eventsTracker.Elapsed}"); + var provider = (TestLoggerProvider)scope.ServiceProvider.GetServices().Single(x => x is TestLoggerProvider); + + var log = Assert.Single(provider.Sink.LogEntries); + Assert.Equal(LogLevel.Error, log.LogLevel); + Assert.Equal($"Unhandled exception executing delegate handler for event {typeof(Event1).FullName}", log.Message); + Assert.Equal("The method or operation is not implemented.", log.Exception?.Message); + } + + [Fact] + public async Task Shutdown_During_Handling_TaskCanceledException_IsLogged() + { + // Arrange + var services = ServiceProviderHelper.BuildWithLogger( + sc => sc.AddEventBroker() + .AddSingleton(_builder) + .AddSingleton(_eventsTracker)); + + _builder.RegisterHandler(async (CancellationToken cancellationToken) => await Task.Delay(200, cancellationToken)); + + using var scope = services.CreateScope(); + + var eventBroker = scope.ServiceProvider.GetRequiredService(); + + // Act + await eventBroker.Publish(new Event1(1)); + await Task.Delay(50); + eventBroker.Shutdown(); + await Task.Delay(50); + + // Assert + var provider = (TestLoggerProvider)scope.ServiceProvider.GetServices().Single(x => x is TestLoggerProvider); + + var log = Assert.Single(provider.Sink.LogEntries); + Assert.Equal(LogLevel.Error, log.LogLevel); + Assert.Equal($"Unhandled exception executing delegate handler for event {typeof(Event1).FullName}", log.Message); + Assert.Equal("A task was canceled.", log.Exception?.Message); + } +} diff --git a/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/HandlerExecutionTests.cs b/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/HandlerExecutionTests.cs new file mode 100644 index 0000000..dc581ca --- /dev/null +++ b/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/HandlerExecutionTests.cs @@ -0,0 +1,318 @@ +using Xunit.Abstractions; + +namespace M.EventBrokerSlim.Tests.DelegateHandlerTests; + +public class HandlerExecutionTests +{ + private readonly ITestOutputHelper _output; + private readonly ServiceProvider _serviceProvider; + private readonly DelegateHandlerRegistryBuilder _builder; + private readonly EventsTracker _tracker; + + public HandlerExecutionTests(ITestOutputHelper output) + { + _output = output; + _builder = new DelegateHandlerRegistryBuilder(); + _tracker = new EventsTracker(); + _serviceProvider = ServiceProviderHelper.Build( + x => x.AddEventBroker() + .AddSingleton(_tracker) + .AddSingleton(_builder)); + } + + [Fact] + public async Task Event_Injected_In_Handler() + { + // Arrange + _builder.RegisterHandler(static async (Event1 event1, EventsTracker tracker) => await tracker.TrackAsync(event1)) + .WrapWith(static async (Event1 event1, EventsTracker tracker, INextHandler next) => + { + tracker.Track(event1); + await next.Execute(); + }); + using var scope = _serviceProvider.CreateScope(); + var eventBroker = scope.ServiceProvider.GetRequiredService(); + _tracker.ExpectedItemsCount = 2; + var event1 = new Event1(1); + + // Act + await eventBroker.Publish(event1); + await _tracker.Wait(TimeSpan.FromSeconds(1)); + + // Assert + var items = _tracker.Items.Select(x => x.Item).OfType().ToArray(); + Assert.Equal(2, items.Length); + Assert.Single(items.Distinct(), event1); + + _output.WriteLine($"Elapsed: {_tracker.Elapsed}"); + } + + [Fact] + public async Task CancellationToken_Injected_In_Handler() + { + // Arrange + _builder.RegisterHandler(static async (CancellationToken cancellationToken, EventsTracker tracker) => await tracker.TrackAsync(cancellationToken)) + .WrapWith(static async (CancellationToken cancellationToken, EventsTracker tracker, INextHandler next) => + { + tracker.Track(cancellationToken); + await next.Execute(); + }); + using var scope = _serviceProvider.CreateScope(); + var eventBroker = scope.ServiceProvider.GetRequiredService(); + _tracker.ExpectedItemsCount = 2; + + // Act + await eventBroker.Publish(new Event1(1)); + await _tracker.Wait(TimeSpan.FromSeconds(1)); + + // Assert + var items = _tracker.Items.Select(x => x.Item).OfType().ToArray(); + Assert.Equal(2, items.Length); + Assert.Single(items.Distinct()); + + _output.WriteLine($"Elapsed: {_tracker.Elapsed}"); + } + + [Fact] + public async Task RetryPolicy_Injected_In_Handler() + { + // Arrange + _builder.RegisterHandler(static async (IRetryPolicy retryPolicy, EventsTracker tracker) => await tracker.TrackAsync(retryPolicy)) + .WrapWith(static async (IRetryPolicy retryPolicy, EventsTracker tracker, INextHandler next) => + { + tracker.Track(retryPolicy); + await next.Execute(); + }); + using var scope = _serviceProvider.CreateScope(); + var eventBroker = scope.ServiceProvider.GetRequiredService(); + _tracker.ExpectedItemsCount = 2; + + // Act + await eventBroker.Publish(new Event1(1)); + await _tracker.Wait(TimeSpan.FromSeconds(1)); + + // Assert + var items = _tracker.Items.Select(x => x.Item).OfType().ToArray(); + Assert.Equal(2, items.Length); + Assert.Single(items.Distinct()); + + _output.WriteLine($"Elapsed: {_tracker.Elapsed}"); + } + + [Fact] + public async Task Wrappers_Executed_In_Outer_To_Inner_Order() + { + // Arrange + _builder.RegisterHandler(static async (Event1 event1, EventsTracker tracker) => await tracker.TrackAsync("handler")) + .WrapWith(static async (Event1 event1, EventsTracker tracker, INextHandler next) => + { + tracker.Track("wrapper1"); + await next.Execute(); + }) + .WrapWith(static async (Event1 event1, EventsTracker tracker, INextHandler next) => + { + tracker.Track("wrapper2"); + await next.Execute(); + }) + .WrapWith(static async (Event1 event1, EventsTracker tracker, INextHandler next) => + { + tracker.Track("wrapper3"); + await next.Execute(); + }); + using var scope = _serviceProvider.CreateScope(); + var eventBroker = scope.ServiceProvider.GetRequiredService(); + _tracker.ExpectedItemsCount = 4; + + // Act + await eventBroker.Publish(new Event1(1)); + await _tracker.Wait(TimeSpan.FromSeconds(1)); + + // Assert + var items = _tracker.Items.OrderBy(x => x.Timestamp).Select(x => x.Item).OfType().ToArray(); + Assert.Equal(new[] { "wrapper3", "wrapper2", "wrapper1", "handler" }, items); + + _output.WriteLine($"Elapsed: {_tracker.Elapsed}"); + } + + [Fact] + public async Task Calling_NextHandler_Multiple_Times_Has_No_Effect() + { + // Arrange + _builder.RegisterHandler(static async (Event1 event1, EventsTracker tracker) => await tracker.TrackAsync("handler")) + .WrapWith(static async (Event1 event1, EventsTracker tracker, INextHandler next) => + { + tracker.Track("wrapper1"); + await next.Execute(); + await next.Execute(); + await next.Execute(); + }) + .WrapWith(static async (Event1 event1, EventsTracker tracker, INextHandler next) => + { + tracker.Track("wrapper2"); + await next.Execute(); + await next.Execute(); + }) + .WrapWith(static async (Event1 event1, EventsTracker tracker, INextHandler next) => + { + tracker.Track("wrapper3"); + await next.Execute(); + await next.Execute(); + await next.Execute(); + }); + using var scope = _serviceProvider.CreateScope(); + var eventBroker = scope.ServiceProvider.GetRequiredService(); + _tracker.ExpectedItemsCount = 4; + + // Act + await eventBroker.Publish(new Event1(1)); + await _tracker.Wait(TimeSpan.FromSeconds(1)); + + // Assert + var items = _tracker.Items.OrderBy(x => x.Timestamp).Select(x => x.Item).OfType().ToArray(); + Assert.Equal(new[] { "wrapper3", "wrapper2", "wrapper1", "handler" }, items); + + _output.WriteLine($"Elapsed: {_tracker.Elapsed}"); + } + + [Fact] + public async Task Calling_Next_FromHandler_Has_No_Effect() + { + // Arrange + _builder.RegisterHandler(static async (Event1 event1, INextHandler next, EventsTracker tracker) => + { + tracker.Track(event1); + await next.Execute(); + }); + + using var scope = _serviceProvider.CreateScope(); + var eventBroker = scope.ServiceProvider.GetRequiredService(); + _tracker.ExpectedItemsCount = 1; + + // Act + await eventBroker.Publish(new Event1(1)); + await _tracker.Wait(TimeSpan.FromSeconds(1)); + + // Assert + var items = _tracker.Items.Select(x => x.Item).OfType().ToArray(); + Assert.Single(items); + + _output.WriteLine($"Elapsed: {_tracker.Elapsed}"); + } + + [Fact] + public async Task When_Wrapper_Does_Not_Call_NextExecute_Handler_Not_Executed() + { + // Arrange + _builder.RegisterHandler(static async (Event1 event1, EventsTracker tracker) => await tracker.TrackAsync(event1)) + .WrapWith(static async (Event1 event1, EventsTracker tracker, INextHandler next) => await tracker.TrackAsync(event1)); + using var scope = _serviceProvider.CreateScope(); + var eventBroker = scope.ServiceProvider.GetRequiredService(); + _tracker.ExpectedItemsCount = 2; + + // Act + await eventBroker.Publish(new Event1(1)); + await _tracker.Wait(TimeSpan.FromSeconds(1)); + + // Assert + var items = _tracker.Items.Select(x => x.Item).OfType().ToArray(); + Assert.Single(items); + + _output.WriteLine($"Elapsed: {_tracker.Elapsed}"); + } + + [Fact] + public async Task Deferred_Publish_Is_Handled() + { + // Arrange + _builder.RegisterHandler(static async (Event1 event1, EventsTracker tracker) => await tracker.TrackAsync(event1)) + .WrapWith(static async (Event1 event1, EventsTracker tracker, INextHandler next) => + { + tracker.Track(event1); + await next.Execute(); + }); + using var scope = _serviceProvider.CreateScope(); + var eventBroker = scope.ServiceProvider.GetRequiredService(); + _tracker.ExpectedItemsCount = 2; + + // Act + await eventBroker.PublishDeferred(new Event1(1), TimeSpan.FromMilliseconds(200)); + await _tracker.Wait(TimeSpan.FromSeconds(1)); + + // Assert + var items = _tracker.Items.Select(x => x.Item).OfType().ToArray(); + Assert.Equal(2, items.Length); + + _output.WriteLine($"Elapsed: {_tracker.Elapsed}"); + } + + [Fact] + public async Task Retry_From_Handler() + { + // Arrange + _builder.RegisterHandler(static async (Event1 event1, IRetryPolicy retryPolicy, EventsTracker tracker) => + { + await tracker.TrackAsync(event1); + if(retryPolicy.Attempt < 2) + { + retryPolicy.RetryAfter(TimeSpan.FromMilliseconds(100)); + } + }); + using var scope = _serviceProvider.CreateScope(); + var eventBroker = scope.ServiceProvider.GetRequiredService(); + _tracker.ExpectedItemsCount = 3; + + // Act + await eventBroker.Publish(new Event1(1)); + await _tracker.Wait(TimeSpan.FromSeconds(1)); + + // Assert + var items = _tracker.Items.OrderBy(x => x.Timestamp).Select(x => x.Item).OfType().ToArray(); + Assert.Equal(3, items.Length); + var timestamps = _tracker.Items.OrderBy(x => x.Timestamp).Select(x => x.Timestamp).ToArray(); + for(int i = timestamps.Length - 1; i == 1; i--) + { + Assert.Equal(100d, (timestamps[i] - timestamps[i - 1]).TotalMilliseconds, 50d); + } + + _output.WriteLine($"Elapsed: {_tracker.Elapsed}"); + } + + [Fact] + public async Task Retry_From_Wrapper() + { + // Arrange + _builder.RegisterHandler(static async (Event1 event1, IRetryPolicy retryPolicy, EventsTracker tracker) => await tracker.TrackAsync("handler")) + .WrapWith(static async (Event1 event1, IRetryPolicy retryPolicy, EventsTracker tracker, INextHandler next) => + { + await next.Execute(); + tracker.Track(event1); + if(retryPolicy.Attempt < 2) + { + retryPolicy.RetryAfter(TimeSpan.FromMilliseconds(100)); + } + }); + using var scope = _serviceProvider.CreateScope(); + var eventBroker = scope.ServiceProvider.GetRequiredService(); + _tracker.ExpectedItemsCount = 6; + + // Act + await eventBroker.Publish(new Event1(1)); + await _tracker.Wait(TimeSpan.FromSeconds(1)); + + // Assert + + var handler = _tracker.Items.Select(x => x.Item).OfType().Where(x => x == "handler").ToArray(); + Assert.Equal(3, handler.Length); + + var items = _tracker.Items.Select(x => x.Item).OfType().ToArray(); + Assert.Equal(3, items.Length); + + var timestamps = _tracker.Items.OrderBy(x => x.Timestamp).Select(x => x.Timestamp).ToArray(); + for(int i = timestamps.Length - 1; i == 1; i--) + { + Assert.Equal(100d, (timestamps[i] - timestamps[i - 1]).TotalMilliseconds, 50d); + } + + _output.WriteLine($"Elapsed: {_tracker.Elapsed}"); + } +} diff --git a/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/HandlerSettings.cs b/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/HandlerSettings.cs new file mode 100644 index 0000000..f256e2c --- /dev/null +++ b/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/HandlerSettings.cs @@ -0,0 +1,3 @@ +namespace M.EventBrokerSlim.Tests.DelegateHandlerTests; + +public record HandlerSettings(int RetryAttempts, TimeSpan Delay); diff --git a/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/LoadTests.cs b/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/LoadTests.cs new file mode 100644 index 0000000..e84a4ac --- /dev/null +++ b/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/LoadTests.cs @@ -0,0 +1,119 @@ +using Xunit.Abstractions; + +namespace M.EventBrokerSlim.Tests.DelegateHandlerTests; + +public class LoadTests +{ + private readonly ITestOutputHelper _output; + + public LoadTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public async Task Load_MultipleDelegateHandlers_With_Retry() + { + // Arrange + var registryBuilder = new DelegateHandlerRegistryBuilder(); + var services = ServiceProviderHelper.Build( + sc => sc.AddEventBroker(x => x.WithMaxConcurrentHandlers(5)) + .AddSingleton(new HandlerSettings(RetryAttempts: 3, Delay: TimeSpan.FromMilliseconds(100))) + .AddSingleton() + .AddSingleton(registryBuilder)); + + registryBuilder + .RegisterHandler(DelegateEventHandlers.TestEventHandler1) + .WrapWith(DelegateEventHandlers.TestEventHandler1ErrorHandler) + .Builder() + .RegisterHandler(DelegateEventHandlers.TestEventHandler1) + .WrapWith(DelegateEventHandlers.TestEventHandler1ErrorHandler) + .Builder() + .RegisterHandler(DelegateEventHandlers.TestEventHandler1) + .WrapWith(DelegateEventHandlers.TestEventHandler1ErrorHandler); + + registryBuilder.RegisterHandler(DelegateEventHandlers.TestEventHandler2); + registryBuilder.RegisterHandler(DelegateEventHandlers.TestEventHandler2); + registryBuilder.RegisterHandler(DelegateEventHandlers.TestEventHandler2); + registryBuilder.RegisterHandler(DelegateEventHandlers.TestEventHandler3); + registryBuilder.RegisterHandler(DelegateEventHandlers.TestEventHandler3); + registryBuilder.RegisterHandler(DelegateEventHandlers.TestEventHandler3); + + using var scope = services.CreateScope(); + + var eventBroker = scope.ServiceProvider.GetRequiredService(); + var eventsTracker = scope.ServiceProvider.GetRequiredService(); + + const int EventsCount = 100_000; + eventsTracker.ExpectedItemsCount = 3 * (3 * EventsCount + EventsCount / 250 * 3 + EventsCount / 500 * 3); + + // Act + foreach(var i in Enumerable.Range(1, EventsCount)) + { + await eventBroker.Publish(new Event1(i)); + await eventBroker.Publish(new Event2(i)); + await eventBroker.Publish(new Event3(i)); + } + + await eventsTracker.Wait(TimeSpan.FromSeconds(10)); + + // Assert + _output.WriteLine($"Elapsed: {eventsTracker.Elapsed}"); + + var counters = eventsTracker.Items + .Select(x => x.Item) + .GroupBy(x => x.GetType()) + .Select(x => (Type: x.Key, Count: x.Count())) + .ToArray(); + // 1 event, 3 handlers, one handler does not retry, other retries one each 250 events 3 times, other retries one each 500 events 3 times + Assert.Equal(3 * EventsCount + EventsCount / 250 * 3 + EventsCount / 500 * 3, counters[0].Count); + Assert.Equal(3 * EventsCount + EventsCount / 250 * 3 + EventsCount / 500 * 3, counters[1].Count); + Assert.Equal(3 * EventsCount + EventsCount / 250 * 3 + EventsCount / 500 * 3, counters[2].Count); + } + + public static class DelegateEventHandlers + { + public static Task TestEventHandler1(T @event, EventsTracker tracker) where T : TestEventBase + { + tracker.Track(@event); + if(@event.Number % 250 == 0) + { + throw new NotImplementedException(); + } + + return Task.CompletedTask; + } + + public static async Task TestEventHandler1ErrorHandler(T @event, INextHandler nextHandler, HandlerSettings settings, IRetryPolicy retryPolicy, EventsTracker tracker) where T : TestEventBase + { + try + { + await nextHandler.Execute(); + } + catch + { + if(@event.Number % 250 == 0 && retryPolicy.Attempt < settings.RetryAttempts) + { + retryPolicy.RetryAfter(settings.Delay); + } + } + } + + public static Task TestEventHandler2(T @event, IRetryPolicy retryPolicy, EventsTracker tracker, HandlerSettings settings) where T : TestEventBase + { + tracker.Track(@event); + if(@event.Number % 500 == 0 && retryPolicy.Attempt < settings.RetryAttempts) + { + retryPolicy.RetryAfter(settings.Delay); + } + + return Task.CompletedTask; + } + + public static Task TestEventHandler3(T @event, EventsTracker tracker) where T : TestEventBase + { + tracker.Track(@event!); + return Task.CompletedTask; + } + } +} diff --git a/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/RegistrationTests.cs b/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/RegistrationTests.cs new file mode 100644 index 0000000..c55c0e7 --- /dev/null +++ b/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/RegistrationTests.cs @@ -0,0 +1,259 @@ +using Xunit.Abstractions; + +namespace M.EventBrokerSlim.Tests.DelegateHandlerTests; + +public class RegistrationTests +{ + private readonly DelegateHandlerRegistryBuilder _builder; + private readonly ITestOutputHelper _output; + private readonly EventsTracker _eventsTracker; + + public RegistrationTests(ITestOutputHelper output) + { + _output = output; + _builder = new DelegateHandlerRegistryBuilder(); + _eventsTracker = new EventsTracker(); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + [InlineData(8)] + [InlineData(9)] + [InlineData(10)] + [InlineData(11)] + [InlineData(12)] + [InlineData(13)] + [InlineData(14)] + [InlineData(15)] + [InlineData(16)] + public async Task Handler_Parameters_Are_Resolved_From_Container(int handlerParametersCount) + { + // Arrange + var services = ServiceProviderHelper.BuildWithLogger( + sc => sc.AddEventBroker() + .AddSingleton(_builder) + .AddSingleton(_eventsTracker) + .AddAllTestTypes()); + + _builder.RegisterHandler(GetHandler(handlerParametersCount, _eventsTracker)); + + _eventsTracker.ExpectedItemsCount = 1; + + using var scope = services.CreateScope(); + var eventBroker = scope.ServiceProvider.GetRequiredService(); + + // Act + await eventBroker.Publish(new Event1(1)); + + await _eventsTracker.Wait(TimeSpan.FromSeconds(1)); + + // Assert + Assert.Single(_eventsTracker.Items); + _output.WriteLine($"Elapsed: {_eventsTracker.Elapsed}"); + } + + [Fact] + public void When_Handler_Parameters_Exceed_Limit_Throws() + { + var exception = Assert.Throws(() => _builder.RegisterHandler(GetHandler(17, _eventsTracker))); + Assert.Equal("Delegate can't have more than 16 arguments.", exception.Message); + } + + [Fact] + public void When_Delegate_Does_Not_Return_Task_Throws() + { + var exception = Assert.Throws(() => _builder.RegisterHandler(() => 1)); + Assert.Equal("Delegate must return a Task.", exception.Message); + } + + [Fact] + public async void Registration_After_EvenBroker_Is_Created_Throws() + { + // Arrange + var services = ServiceProviderHelper.Build(sc => sc.AddEventBroker().AddSingleton(_builder)); + + using var scope = services.CreateScope(); + var eventBroker = scope.ServiceProvider.GetRequiredService(); + await eventBroker.Publish(new Event1(1)); + + // Act + var exception = Assert.Throws(() => _builder.RegisterHandler(() => Task.CompletedTask)); + + // Assert + Assert.Equal("Registry is closed. Please complete registrations before IEventBroker is resolved.", exception.Message); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + [InlineData(8)] + [InlineData(9)] + [InlineData(10)] + [InlineData(11)] + [InlineData(12)] + [InlineData(13)] + [InlineData(14)] + [InlineData(15)] + [InlineData(16)] + public async Task Wrapper_Parameters_Are_Resolved_From_Container(int handlerParametersCount) + { + // Arrange + var services = ServiceProviderHelper.BuildWithLogger( + sc => sc.AddEventBroker() + .AddSingleton(_builder) + .AddSingleton(_eventsTracker) + .AddAllTestTypes()); + + _builder.RegisterHandler(() => Task.CompletedTask) + .WrapWith(GetHandler(handlerParametersCount, _eventsTracker)); + + _eventsTracker.ExpectedItemsCount = 1; + + using var scope = services.CreateScope(); + var eventBroker = scope.ServiceProvider.GetRequiredService(); + + // Act + await eventBroker.Publish(new Event1(1)); + + await _eventsTracker.Wait(TimeSpan.FromSeconds(1)); + + // Assert + Assert.Single(_eventsTracker.Items); + _output.WriteLine($"Elapsed: {_eventsTracker.Elapsed}"); + } + + [Fact] + public void When_Wrapper_Parameters_Exceed_Limit_Throws() + { + var exception = Assert.Throws(() => _builder.RegisterHandler(() => Task.CompletedTask).WrapWith(GetHandler(17, _eventsTracker))); + Assert.Equal("Delegate can't have more than 16 arguments.", exception.Message); + } + + [Fact] + public void When_Wrapper_Does_Not_Return_Task_Throws() + { + var exception = Assert.Throws(() => _builder.RegisterHandler(() => Task.CompletedTask).WrapWith(() => 1)); + Assert.Equal("Delegate must return a Task.", exception.Message); + } + + [Fact] + public async void Wrapper_Registration_After_EvenBroker_Is_Created_Throws() + { + // Arrange + var services = ServiceProviderHelper.Build(sc => sc.AddEventBroker().AddSingleton(_builder)); + + var handler = _builder.RegisterHandler(() => Task.CompletedTask); + + using var scope = services.CreateScope(); + var eventBroker = scope.ServiceProvider.GetRequiredService(); + await eventBroker.Publish(new Event1(1)); + + // Act + var exception = Assert.Throws(() => handler.WrapWith(() => Task.CompletedTask)); + + // Assert + Assert.Equal("Registry is closed. Please complete registrations before IEventBroker is resolved.", exception.Message); + } + + [Fact] + public async Task No_Builder_Registered() + { + // Arrange + var services = ServiceProviderHelper.BuildWithLogger( + sc => sc.AddEventBroker(x => x.AddScoped()) + .AddSingleton(_eventsTracker)); + + using var scope = services.CreateScope(); + var eventBroker = scope.ServiceProvider.GetRequiredService(); + + _eventsTracker.ExpectedItemsCount = 1; + + // Act + await eventBroker.Publish(new Event1(1)); + + await _eventsTracker.Wait(TimeSpan.FromSeconds(1)); + + // Assert + Assert.Single(_eventsTracker.Items); + } + + [Fact] + public async Task Multiple_Builders_Registered() + { + _builder.RegisterHandler(async (EventsTracker tracker) => await tracker.TrackAsync("handler1")); + + var builder2 = new DelegateHandlerRegistryBuilder(); + builder2.RegisterHandler(async (EventsTracker tracker) => await tracker.TrackAsync("handler2")); + + var builder3 = new DelegateHandlerRegistryBuilder(); + builder3.RegisterHandler(async (EventsTracker tracker) => await tracker.TrackAsync("handler3")); + + // Arrange + var services = ServiceProviderHelper.BuildWithLogger( + sc => sc.AddEventBroker() + .AddSingleton(_builder) + .AddSingleton(builder2) + .AddSingleton(builder3) + .AddSingleton(_eventsTracker)); + + using var scope = services.CreateScope(); + var eventBroker = scope.ServiceProvider.GetRequiredService(); + + _eventsTracker.ExpectedItemsCount = 3; + + // Act + await eventBroker.Publish(new Event1(1)); + + await _eventsTracker.Wait(TimeSpan.FromSeconds(1)); + + // Assert + Assert.Equal(3, _eventsTracker.Items.Count); + Assert.Equal(new[] { "handler1", "handler2", "handler3" }, _eventsTracker.Items.Select(x => x.Item).Cast().Order().ToArray()); + } + +#pragma warning disable RCS1163 // Unused parameter + private static Delegate GetHandler(int parametersCount, EventsTracker _eventsTracker) + => parametersCount switch + { + 0 => async () => await _eventsTracker.TrackAsync(1), + 1 => async (A1 a1) => await _eventsTracker.TrackAsync(1), + 2 => async (A1 a1, A2 a2) => await _eventsTracker.TrackAsync(1), + 3 => async (A1 a1, A2 a2, A3 a3) => await _eventsTracker.TrackAsync(1), + 4 => async (A1 a1, A2 a2, A3 a3, A4 a4) => await _eventsTracker.TrackAsync(1), + 5 => async (A1 a1, A2 a2, A3 a3, A4 a4, A4 a5) => await _eventsTracker.TrackAsync(1), + 6 => async (A1 a1, A2 a2, A3 a3, A4 a4, A4 a5, A4 a6) => await _eventsTracker.TrackAsync(1), + 7 => async (A1 a1, A2 a2, A3 a3, A4 a4, A4 a5, A4 a6, A7 a7) => await _eventsTracker.TrackAsync(1), + 8 => async (A1 a1, A2 a2, A3 a3, A4 a4, A4 a5, A4 a6, A7 a7, A8 a8) => await _eventsTracker.TrackAsync(1), + 9 => async (A1 a1, A2 a2, A3 a3, A4 a4, A4 a5, A4 a6, A7 a7, A8 a8, A9 a9) => await _eventsTracker.TrackAsync(1), + 10 => async (A1 a1, A2 a2, A3 a3, A4 a4, A4 a5, A4 a6, A7 a7, A8 a8, A9 a9, A10 a10) => await _eventsTracker.TrackAsync(1), + 11 => async (A1 a1, A2 a2, A3 a3, A4 a4, A4 a5, A4 a6, A7 a7, A8 a8, A9 a9, A10 a10, A11 a11) => await _eventsTracker.TrackAsync(1), + 12 => async (A1 a1, A2 a2, A3 a3, A4 a4, A4 a5, A4 a6, A7 a7, A8 a8, A9 a9, A10 a10, A11 a11, A12 a12) => await _eventsTracker.TrackAsync(1), + 13 => async (A1 a1, A2 a2, A3 a3, A4 a4, A4 a5, A4 a6, A7 a7, A8 a8, A9 a9, A10 a10, A11 a11, A12 a12, A13 a13) => await _eventsTracker.TrackAsync(1), + 14 => async (A1 a1, A2 a2, A3 a3, A4 a4, A4 a5, A4 a6, A7 a7, A8 a8, A9 a9, A10 a10, A11 a11, A12 a12, A13 a13, A14 a14) => await _eventsTracker.TrackAsync(1), + 15 => async (A1 a1, A2 a2, A3 a3, A4 a4, A4 a5, A4 a6, A7 a7, A8 a8, A9 a9, A10 a10, A11 a11, A12 a12, A13 a13, A14 a14, A15 a15) => await _eventsTracker.TrackAsync(1), + 16 => async (A1 a1, A2 a2, A3 a3, A4 a4, A4 a5, A4 a6, A7 a7, A8 a8, A9 a9, A10 a10, A11 a11, A12 a12, A13 a13, A14 a14, A15 a15, A16 a16) => await _eventsTracker.TrackAsync(1), + 17 => async (A1 a1, A2 a2, A3 a3, A4 a4, A4 a5, A4 a6, A7 a7, A8 a8, A9 a9, A10 a10, A11 a11, A12 a12, A13 a13, A14 a14, A15 a15, A16 a16, A17 a17) => await _eventsTracker.TrackAsync(1), + _ => throw new NotImplementedException(), + }; +#pragma warning restore RCS1163 // Unused parameter + + class Handler1(EventsTracker tracker) : IEventHandler + { + public async Task Handle(Event1 @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) => await tracker.TrackAsync(@event); + + public Task OnError(Exception exception, Event1 @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) => throw new NotImplementedException(); + } +} diff --git a/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/ServiceCollectionExtensions.cs b/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..1e3d063 --- /dev/null +++ b/test/M.EventBrokerSlim.Tests/DelegateHandlerTests/ServiceCollectionExtensions.cs @@ -0,0 +1,42 @@ +namespace M.EventBrokerSlim.Tests.DelegateHandlerTests; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddAllTestTypes(this IServiceCollection serviceCollection) => + serviceCollection.AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient(); +} + +public record A1(); +public record A2(); +public record A3(); +public record A4(); +public record A5(); +public record A6(); +public record A7(); +public record A8(); +public record A9(); +public record A10(); +public record A11(); +public record A12(); +public record A13(); +public record A14(); +public record A15(); +public record A16(); +public record A17(); + diff --git a/test/M.EventBrokerSlim.Tests/EventBrokerTests.cs b/test/M.EventBrokerSlim.Tests/EventBrokerTests.cs index 793505f..33b451b 100644 --- a/test/M.EventBrokerSlim.Tests/EventBrokerTests.cs +++ b/test/M.EventBrokerSlim.Tests/EventBrokerTests.cs @@ -110,7 +110,7 @@ public async Task Publish_AfterShutdown_Throws() eventsRecorder.Expect(1); await eventBroker.Publish(new TestEvent(CorrelationId: 1)); - var completed = await eventsRecorder.WaitForExpected(TimeSpan.FromMilliseconds(50)); + var completed = await eventsRecorder.WaitForExpected(TimeSpan.FromSeconds(1)); eventBroker.Shutdown(); var exception = await Assert.ThrowsAsync(async () => await eventBroker.Publish(new TestEvent(CorrelationId: 2))); @@ -140,7 +140,7 @@ public async Task PublishDeferred_AfterShutdown_DoesNotThrow() eventBroker.Shutdown(); await eventBroker.PublishDeferred(new TestEvent(CorrelationId: 1), TimeSpan.FromMilliseconds(20)); - var completed = await eventsRecorder.WaitForExpected(TimeSpan.FromMilliseconds(100)); + var completed = await eventsRecorder.WaitForExpected(TimeSpan.FromSeconds(1)); // Assert Assert.False(completed); @@ -185,11 +185,11 @@ public async Task PublishDeferred_ExecutesHandler_After_DeferredDuration() // Act eventsRecorder.Expect(1); - var calledPublisheDeferredAt = DateTime.UtcNow; + var calledPublishDeferredAt = DateTime.UtcNow; await eventBroker.PublishDeferred(new TestEvent(CorrelationId: 1), TimeSpan.FromMilliseconds(200)); - var completed = await eventsRecorder.WaitForExpected(TimeSpan.FromMilliseconds(250)); + var completed = await eventsRecorder.WaitForExpected(TimeSpan.FromSeconds(1)); // Assert Assert.True(completed); @@ -197,7 +197,7 @@ public async Task PublishDeferred_ExecutesHandler_After_DeferredDuration() Assert.Equal(1, eventsRecorder.HandledEventIds[0]); var handlerExecutedAt = scope.ServiceProvider.GetRequiredService().ExecutedAt; - Assert.True(handlerExecutedAt - calledPublisheDeferredAt >= TimeSpan.FromMilliseconds(200)); + Assert.True(handlerExecutedAt - calledPublishDeferredAt >= TimeSpan.FromMilliseconds(200)); } [Fact] @@ -217,7 +217,7 @@ public async Task PublishDeferred_DoesNotBlock_Publish() await eventBroker.PublishDeferred(new TestEvent(CorrelationId: 1), TimeSpan.FromMilliseconds(300)); await eventBroker.Publish(new TestEvent(CorrelationId: 2)); - var completed = await eventsRecorder.WaitForExpected(TimeSpan.FromMilliseconds(350)); + var completed = await eventsRecorder.WaitForExpected(TimeSpan.FromSeconds(1)); // Assert Assert.True(completed); @@ -251,7 +251,7 @@ public async Task PublishDeferred_DelayedTasks_Cancelled_OnShutdown() eventBroker.Shutdown(); - var completed = await eventsRecorder.WaitForExpected(TimeSpan.FromMilliseconds(300)); + var completed = await eventsRecorder.WaitForExpected(TimeSpan.FromSeconds(1)); // Assert Assert.False(completed); @@ -327,7 +327,7 @@ public async Task Shutdown_PendingEvents_AreNot_Processed() eventBroker.Shutdown(); - var completed = await eventsRecorder.WaitForExpected(TimeSpan.FromMilliseconds(300)); + var completed = await eventsRecorder.WaitForExpected(TimeSpan.FromSeconds(1)); // Assert Assert.False(completed); @@ -383,18 +383,18 @@ public record TestEvent( public class TestEventHandler : IEventHandler { - private readonly EventsRecorder _eventsRecoder; + private readonly EventsRecorder _eventsRecorder; private readonly Timestamp? _timestamp; public TestEventHandler(EventsRecorder eventsRecorder, Timestamp? timestamp = null) { - _eventsRecoder = eventsRecorder; + _eventsRecorder = eventsRecorder; _timestamp = timestamp; } public async Task Handle(TestEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) { - _eventsRecoder.Notify(@event); + _eventsRecorder.Notify(@event); if(_timestamp is not null) { @@ -414,7 +414,7 @@ public async Task Handle(TestEvent @event, IRetryPolicy retryPolicy, Cancellatio public async Task OnError(Exception exception, TestEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) { - _eventsRecoder.Notify(exception, @event); + _eventsRecorder.Notify(exception, @event); if(@event.ErrorHandlingDuration != default) { diff --git a/test/M.EventBrokerSlim.Tests/EventRecorder.cs b/test/M.EventBrokerSlim.Tests/EventRecorder.cs index 0fca50b..ce61821 100644 --- a/test/M.EventBrokerSlim.Tests/EventRecorder.cs +++ b/test/M.EventBrokerSlim.Tests/EventRecorder.cs @@ -6,10 +6,10 @@ public class EventsRecorder where T : notnull { private readonly ConcurrentDictionary _expected = new(); private readonly TimeSpan _waitForItemsTimeout = TimeSpan.FromMilliseconds(20); - private readonly ConcurrentBag _exceptions = new(); - private readonly ConcurrentBag<(T id, long tick)> _events = new(); - private readonly ConcurrentBag<(int id, long tick)> _handlerInstances = new(); - private readonly ConcurrentBag<(int id, long tick)> _scopeInstances = new(); + private readonly ConcurrentBag _exceptions = []; + private readonly ConcurrentBag<(T id, long tick)> _events = []; + private readonly ConcurrentBag<(int id, long tick)> _handlerInstances = []; + private readonly ConcurrentBag<(int id, long tick)> _scopeInstances = []; public Exception[] Exceptions => _exceptions.ToArray(); diff --git a/test/M.EventBrokerSlim.Tests/EventsTracker.cs b/test/M.EventBrokerSlim.Tests/EventsTracker.cs index bd57654..cc27b97 100644 --- a/test/M.EventBrokerSlim.Tests/EventsTracker.cs +++ b/test/M.EventBrokerSlim.Tests/EventsTracker.cs @@ -1,18 +1,54 @@ using System.Collections.Concurrent; +using System.Diagnostics; namespace M.EventBrokerSlim.Tests; public class EventsTracker { - public void Track(object @event) + private readonly Stopwatch _stopwatch = new (); + private CancellationTokenSource? _cancellationTokenSource; + + public int ExpectedItemsCount { get; set; } = int.MaxValue; + + public TimeSpan Elapsed => _stopwatch.Elapsed; + + public void Track(object item) { - Items.Add((@event, DateTime.UtcNow)); + Items.Add((item, DateTime.UtcNow)); + if(Items.Count == ExpectedItemsCount && _cancellationTokenSource is not null) + { + _cancellationTokenSource.Cancel(); + _stopwatch.Stop(); + } } - public ConcurrentBag<(object Event, DateTime Timestamp)> Items { get; } = new(); + public Task TrackAsync(object item) + { + Track(item); + return Task.CompletedTask; + } + + public ConcurrentBag<(object Item, DateTime Timestamp)> Items { get; } = []; public async Task Wait(TimeSpan timeout) { - await Task.Delay(timeout); + if(Items.Count == ExpectedItemsCount) + { + return; + } + + _stopwatch.Start(); + _cancellationTokenSource = new CancellationTokenSource(timeout); + try + { + await Task.Delay(timeout, _cancellationTokenSource.Token); + } + catch(TaskCanceledException) + { + } + finally + { + _stopwatch.Stop(); + } } } diff --git a/test/M.EventBrokerSlim.Tests/ExceptionHandlingTests.cs b/test/M.EventBrokerSlim.Tests/ExceptionHandlingTests.cs index 3b6ba15..7211d17 100644 --- a/test/M.EventBrokerSlim.Tests/ExceptionHandlingTests.cs +++ b/test/M.EventBrokerSlim.Tests/ExceptionHandlingTests.cs @@ -166,6 +166,7 @@ public Task OnError(Exception exception, TestEvent @event, IRetryPolicy retryPol { throw new NotImplementedException(); } + return Task.CompletedTask; } } @@ -179,7 +180,7 @@ public TestEventHandler1(string input) _input = input; } - public Task Handle(TestEvent @even, IRetryPolicy retryPolicyt, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task Handle(TestEvent @even, IRetryPolicy retryPolicy, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task OnError(Exception exception, TestEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) => throw new NotImplementedException(); } diff --git a/test/M.EventBrokerSlim.Tests/HandlerExecutionTests.cs b/test/M.EventBrokerSlim.Tests/HandlerExecutionTests.cs index 91aad22..4da8f91 100644 --- a/test/M.EventBrokerSlim.Tests/HandlerExecutionTests.cs +++ b/test/M.EventBrokerSlim.Tests/HandlerExecutionTests.cs @@ -27,7 +27,7 @@ public async Task MaxConcurrentHandlers_IsOne_HandlersAreExecuted_Sequentially() await eventBroker.Publish(event1); await eventBroker.Publish(event2); - var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromMilliseconds(100)); + var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromSeconds(1)); // Assert Assert.True(completed); @@ -59,7 +59,7 @@ public async Task MaxConcurrentHandlers_IsGreaterThanOne_HandlersAreExecuted_InP await eventBroker.Publish(event1); await eventBroker.Publish(event2); - var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromMilliseconds(150)); + var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromSeconds(1)); // Assert Assert.True(completed); @@ -149,26 +149,26 @@ public record TestEvent(int CorrelationId, TimeSpan TimeToRun = default) : ITrac public class TestEventHandler : IEventHandler { - private readonly EventsRecorder _eventsRecoder; + private readonly EventsRecorder _eventsRecorder; public TestEventHandler(EventsRecorder eventsRecorder) { - _eventsRecoder = eventsRecorder; + _eventsRecorder = eventsRecorder; } public async Task Handle(TestEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) { if(@event.TimeToRun != default) { - await Task.Delay(@event.TimeToRun); + await Task.Delay(@event.TimeToRun, cancellationToken); } - _eventsRecoder.Notify(@event); + _eventsRecorder.Notify(@event); } public Task OnError(Exception exception, TestEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) { - _eventsRecoder.Notify(exception, @event); + _eventsRecorder.Notify(exception, @event); return Task.CompletedTask; } } diff --git a/test/M.EventBrokerSlim.Tests/HandlerRegistrationTests.cs b/test/M.EventBrokerSlim.Tests/HandlerRegistrationTests.cs index 5f597ab..5c9a248 100644 --- a/test/M.EventBrokerSlim.Tests/HandlerRegistrationTests.cs +++ b/test/M.EventBrokerSlim.Tests/HandlerRegistrationTests.cs @@ -47,7 +47,7 @@ public void MaxConcurrentHandlers_SetTo_Zero_Throws() paramName: "maxConcurrentHandlers", testCode: () => serviceCollection.AddEventBroker(x => x.WithMaxConcurrentHandlers(0))); - Assert.Equal("MaxConcurrentHandlers should be greater than zero (Parameter 'maxConcurrentHandlers')", exception.Message); + Assert.Equal("MaxConcurrentHandlers should be greater than zero. (Parameter 'maxConcurrentHandlers')", exception.Message); } [Fact] @@ -59,7 +59,7 @@ public void MaxConcurrentHandlers_SetTo_Negative_Throws() paramName: "maxConcurrentHandlers", testCode: () => serviceCollection.AddEventBroker(x => x.WithMaxConcurrentHandlers(rand.Next(int.MinValue, -1)))); - Assert.Equal("MaxConcurrentHandlers should be greater than zero (Parameter 'maxConcurrentHandlers')", exception.Message); + Assert.Equal("MaxConcurrentHandlers should be greater than zero. (Parameter 'maxConcurrentHandlers')", exception.Message); } [Fact] @@ -87,7 +87,7 @@ public async Task Handlers_RegisteredWith_AddEventBroker_AreExecuted() await eventBroker.Publish(testEvent); - var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromMilliseconds(100)); + var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromSeconds(1)); // Assert Assert.True(completed); @@ -119,7 +119,7 @@ public async Task Handlers_RegisteredBefore_AddEventBroker_AreExecuted() await eventBroker.Publish(testEvent); - var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromMilliseconds(100)); + var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromSeconds(1)); // Assert Assert.True(completed); @@ -151,7 +151,7 @@ public async Task Handlers_RegisteredAfter_AddEventBroker_AreExecuted() await eventBroker.Publish(testEvent); - var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromMilliseconds(100)); + var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromSeconds(1)); // Assert Assert.True(completed); @@ -181,7 +181,7 @@ public async Task Handlers_RegisteredBeforeAndAfter_AddEventBroker_AreExecuted() await eventBroker.Publish(testEvent); - var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromMilliseconds(100)); + var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromSeconds(1)); // Assert Assert.True(completed); diff --git a/test/M.EventBrokerSlim.Tests/HandlerScopeAndInstanceTests.cs b/test/M.EventBrokerSlim.Tests/HandlerScopeAndInstanceTests.cs index e0ec1e2..b0b55de 100644 --- a/test/M.EventBrokerSlim.Tests/HandlerScopeAndInstanceTests.cs +++ b/test/M.EventBrokerSlim.Tests/HandlerScopeAndInstanceTests.cs @@ -24,7 +24,7 @@ public async Task Handler_RegisteredAsTransient_Executed_ByDifferentInstances_An await eventBroker.Publish(event1); await eventBroker.Publish(event2); - var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromMilliseconds(200)); + var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromSeconds(1)); // Assert Assert.True(completed); @@ -58,7 +58,7 @@ public async Task Handler_RegisteredAsSingleton_Executed_BySameInstance() await eventBroker.Publish(event1); await eventBroker.Publish(event2); - var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromMilliseconds(50)); + var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromSeconds(1)); // Assert Assert.True(completed); @@ -92,7 +92,7 @@ public async Task Handler_RegisteredAsScoped_Executed_ByDifferentInstances_And_D await eventBroker.Publish(event1); await eventBroker.Publish(event2); - var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromMilliseconds(50)); + var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromSeconds(1)); // Assert Assert.True(completed); diff --git a/test/M.EventBrokerSlim.Tests/LoadTests.cs b/test/M.EventBrokerSlim.Tests/LoadTests.cs index e6bdfa3..cbb3a87 100644 --- a/test/M.EventBrokerSlim.Tests/LoadTests.cs +++ b/test/M.EventBrokerSlim.Tests/LoadTests.cs @@ -27,6 +27,7 @@ public async Task Load_MultipleHandlers_With_Retry() var eventsTracker = scope.ServiceProvider.GetRequiredService(); const int EventsCount = 100_000; + eventsTracker.ExpectedItemsCount = 3 * (3 * EventsCount + EventsCount / 250 * 3 + EventsCount / 500 * 3); // Act foreach(var i in Enumerable.Range(1, EventsCount)) @@ -36,15 +37,15 @@ public async Task Load_MultipleHandlers_With_Retry() await eventBroker.Publish(new Event3("event", i)); } - await eventsTracker.Wait(TimeSpan.FromSeconds(3)); + await eventsTracker.Wait(TimeSpan.FromSeconds(10)); // Assert var counters = eventsTracker.Items - .Select(x => x.Event) + .Select(x => x.Item) .GroupBy(x => x.GetType()) .Select(x => (Type: x.Key, Count: x.Count())) .ToArray(); - // 1 event, 3 handlers, one handler does not retry, other retrires one each 250 events 3 times, other retrires one each 500 events 3 times + // 1 event, 3 handlers, one handler does not retry, other retries one each 250 events 3 times, other retries one each 500 events 3 times Assert.Equal(3 * EventsCount + EventsCount / 250 * 3 + EventsCount / 500 * 3, counters[0].Count); Assert.Equal(3 * EventsCount + EventsCount / 250 * 3 + EventsCount / 500 * 3, counters[1].Count); Assert.Equal(3 * EventsCount + EventsCount / 250 * 3 + EventsCount / 500 * 3, counters[2].Count); @@ -88,6 +89,7 @@ public Task Handle(T @event, IRetryPolicy retryPolicy, CancellationToken cancell { throw new NotImplementedException(); } + return Task.CompletedTask; } @@ -97,6 +99,7 @@ public Task OnError(Exception exception, T @event, IRetryPolicy retryPolicy, Can { retryPolicy.RetryAfter(_settings.Delay); } + return Task.CompletedTask; } } @@ -119,6 +122,7 @@ public Task Handle(T @event, IRetryPolicy retryPolicy, CancellationToken cancell { retryPolicy.RetryAfter(_settings.Delay); } + return Task.CompletedTask; } diff --git a/test/M.EventBrokerSlim.Tests/MultipleHandlersTests.cs b/test/M.EventBrokerSlim.Tests/MultipleHandlersTests.cs index f7e597a..5e6c697 100644 --- a/test/M.EventBrokerSlim.Tests/MultipleHandlersTests.cs +++ b/test/M.EventBrokerSlim.Tests/MultipleHandlersTests.cs @@ -27,7 +27,7 @@ public async Task MultipleHandlers_AllExecuted() await eventBroker.Publish(testEvent); - var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromMilliseconds(150)); + var completed = await eventsRecorder.WaitForExpected(timeout: TimeSpan.FromSeconds(1)); // Assert Assert.True(completed); diff --git a/test/M.EventBrokerSlim.Tests/OrderOfRetriesTests.cs b/test/M.EventBrokerSlim.Tests/OrderOfRetriesTests.cs index 8cd59a9..9309b72 100644 --- a/test/M.EventBrokerSlim.Tests/OrderOfRetriesTests.cs +++ b/test/M.EventBrokerSlim.Tests/OrderOfRetriesTests.cs @@ -32,7 +32,7 @@ public async Task Retries_ExecutedInCorrectOrder_RespectingDelays(int maxConcurr // Assert Assert.Equal(5, eventsTracker.Items.Count); - var eventsByTimeHandled = eventsTracker.Items.OrderBy(x => x.Timestamp).Select(x => x.Event).ToArray(); + var eventsByTimeHandled = eventsTracker.Items.OrderBy(x => x.Timestamp).Select(x => x.Item).ToArray(); Assert.Equal(eventsByTimeHandled[0], event1); Assert.Equal(eventsByTimeHandled[1], event2); Assert.Equal(eventsByTimeHandled[2], event2); @@ -68,6 +68,7 @@ public Task Handle(TestEvent1 @event, IRetryPolicy retryPolicy, CancellationToke { retryPolicy.RetryAfter(TimeSpan.FromMilliseconds(800)); } + throw new NotImplementedException(); } @@ -93,6 +94,7 @@ public Task Handle(TestEvent2 @event, IRetryPolicy retryPolicy, CancellationToke { retryPolicy.RetryAfter(TimeSpan.FromMilliseconds(100)); } + throw new NotImplementedException(); } diff --git a/test/M.EventBrokerSlim.Tests/RetryFromHandleTests.cs b/test/M.EventBrokerSlim.Tests/RetryFromHandleTests.cs index 95421f3..f498a59 100644 --- a/test/M.EventBrokerSlim.Tests/RetryFromHandleTests.cs +++ b/test/M.EventBrokerSlim.Tests/RetryFromHandleTests.cs @@ -20,16 +20,17 @@ public async Task Handle_SingleRetry_RetriesOnce_With_GivenDelay(int maxConcurre var eventBroker = scope.ServiceProvider.GetRequiredService(); var eventsTracker = scope.ServiceProvider.GetRequiredService(); + eventsTracker.ExpectedItemsCount = 2; var event1 = new TestEvent("test"); // Act await eventBroker.Publish(event1); - await eventsTracker.Wait(TimeSpan.FromMilliseconds(200)); + await eventsTracker.Wait(TimeSpan.FromSeconds(1)); // Assert Assert.Equal(2, eventsTracker.Items.Count); var timestamps = eventsTracker.Items.OrderBy(x => x.Timestamp).Select(x => x.Timestamp).ToArray(); - Assert.Equal(100, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 60); + Assert.Equal(100, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 50); } [Theory] @@ -50,16 +51,17 @@ public async Task Handle_SingleRetry_RetriesOnce_With_ZeroDelay(int maxConcurren var eventBroker = scope.ServiceProvider.GetRequiredService(); var eventsTracker = scope.ServiceProvider.GetRequiredService(); + eventsTracker.ExpectedItemsCount = 2; var event1 = new TestEvent("test"); // Act await eventBroker.Publish(event1); - await eventsTracker.Wait(TimeSpan.FromMilliseconds(100)); + await eventsTracker.Wait(TimeSpan.FromSeconds(1)); // Assert Assert.Equal(2, eventsTracker.Items.Count); var timestamps = eventsTracker.Items.OrderBy(x => x.Timestamp).Select(x => x.Timestamp).ToArray(); - Assert.Equal(0, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 60); + Assert.Equal(0, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 50); } [Theory] @@ -73,25 +75,26 @@ public async Task Handle_MultipleRetries_RetriesMultipleTimes_With_GivenDelay(in sc => sc.AddEventBroker( x => x.WithMaxConcurrentHandlers(maxConcurrentHandlers) .AddTransient()) - .AddSingleton(new HandlerSettings(RetryAttempts: 3, Delay: TimeSpan.FromMilliseconds(150))) + .AddSingleton(new HandlerSettings(RetryAttempts: 3, Delay: TimeSpan.FromMilliseconds(200))) .AddSingleton()); using var scope = services.CreateScope(); var eventBroker = scope.ServiceProvider.GetRequiredService(); var eventsTracker = scope.ServiceProvider.GetRequiredService(); + eventsTracker.ExpectedItemsCount = 4; var event1 = new TestEvent("test"); // Act await eventBroker.Publish(event1); - await eventsTracker.Wait(TimeSpan.FromMilliseconds(700)); + await eventsTracker.Wait(TimeSpan.FromSeconds(2)); // Assert Assert.Equal(4, eventsTracker.Items.Count); var timestamps = eventsTracker.Items.OrderBy(x => x.Timestamp).Select(x => x.Timestamp).ToArray(); - Assert.Equal(150, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 60); - Assert.Equal(150, (timestamps[2] - timestamps[1]).TotalMilliseconds, tolerance: 60); - Assert.Equal(150, (timestamps[3] - timestamps[2]).TotalMilliseconds, tolerance: 60); + Assert.Equal(200, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 50); + Assert.Equal(200, (timestamps[2] - timestamps[1]).TotalMilliseconds, tolerance: 50); + Assert.Equal(200, (timestamps[3] - timestamps[2]).TotalMilliseconds, tolerance: 50); } [Theory] @@ -112,15 +115,16 @@ public async Task Handle_MultipleRetries_Event_IsTheSameInstance_EveryTime(int m var eventBroker = scope.ServiceProvider.GetRequiredService(); var eventsTracker = scope.ServiceProvider.GetRequiredService(); + eventsTracker.ExpectedItemsCount = 4; var event1 = new TestEvent("test"); // Act await eventBroker.Publish(event1); - await eventsTracker.Wait(TimeSpan.FromMilliseconds(700)); + await eventsTracker.Wait(TimeSpan.FromSeconds(2)); // Assert Assert.Equal(4, eventsTracker.Items.Count); - Assert.All(eventsTracker.Items.Select(x => x.Event), x => Assert.Same(event1, x)); + Assert.All(eventsTracker.Items.Select(x => x.Item), x => Assert.Same(event1, x)); } public class TestEvent(string Info) @@ -132,7 +136,6 @@ public record HandlerSettings(int RetryAttempts, TimeSpan Delay); public class TestEventHandler : IEventHandler { - private readonly Random _random = new(); private readonly EventsTracker _tracker; private readonly HandlerSettings _settings; @@ -142,14 +145,15 @@ public TestEventHandler(HandlerSettings settings, EventsTracker tracker) _tracker = tracker; } - public async Task Handle(TestEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) + public Task Handle(TestEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) { _tracker.Track(@event); - await Task.Delay(_random.Next(1, 10)); if(retryPolicy.Attempt < _settings.RetryAttempts) { retryPolicy.RetryAfter(_settings.Delay); } + + return Task.CompletedTask; } public Task OnError(Exception exception, TestEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) diff --git a/test/M.EventBrokerSlim.Tests/RetryFromHandleUsingDelayDelegateTests.cs b/test/M.EventBrokerSlim.Tests/RetryFromHandleUsingDelayDelegateTests.cs index 7cd70cd..a97033c 100644 --- a/test/M.EventBrokerSlim.Tests/RetryFromHandleUsingDelayDelegateTests.cs +++ b/test/M.EventBrokerSlim.Tests/RetryFromHandleUsingDelayDelegateTests.cs @@ -13,23 +13,24 @@ public async Task Handle_SingleRetry_RetriesOnce_With_GivenDelay(int maxConcurre sc => sc.AddEventBroker( x => x.WithMaxConcurrentHandlers(maxConcurrentHandlers) .AddTransient()) - .AddSingleton(new HandlerSettings(RetryAttempts: 1, Delay: TimeSpan.FromMilliseconds(100))) + .AddSingleton(new HandlerSettings(RetryAttempts: 1, Delay: TimeSpan.FromMilliseconds(300))) .AddSingleton()); using var scope = services.CreateScope(); var eventBroker = scope.ServiceProvider.GetRequiredService(); var eventsTracker = scope.ServiceProvider.GetRequiredService(); + eventsTracker.ExpectedItemsCount = 2; var event1 = new TestEvent("test"); // Act await eventBroker.Publish(event1); - await eventsTracker.Wait(TimeSpan.FromMilliseconds(200)); + await eventsTracker.Wait(TimeSpan.FromSeconds(1)); // Assert Assert.Equal(2, eventsTracker.Items.Count); var timestamps = eventsTracker.Items.OrderBy(x => x.Timestamp).Select(x => x.Timestamp).ToArray(); - Assert.Equal(100, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 60); + Assert.Equal(100, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 50); } [Theory] @@ -50,11 +51,12 @@ public async Task Handle_MultipleRetries_RetriesMultipleTimes_With_GivenDelay(in var eventBroker = scope.ServiceProvider.GetRequiredService(); var eventsTracker = scope.ServiceProvider.GetRequiredService(); + eventsTracker.ExpectedItemsCount = 4; var event1 = new TestEvent("test"); // Act await eventBroker.Publish(event1); - await eventsTracker.Wait(TimeSpan.FromMilliseconds(1200)); + await eventsTracker.Wait(TimeSpan.FromSeconds(3)); // Assert Assert.Equal(4, eventsTracker.Items.Count); @@ -86,11 +88,11 @@ public async Task Handle_MultipleRetries_Event_IsTheSameInstance_EveryTime(int m // Act await eventBroker.Publish(event1); - await eventsTracker.Wait(TimeSpan.FromMilliseconds(1200)); + await eventsTracker.Wait(TimeSpan.FromSeconds(2)); // Assert Assert.Equal(4, eventsTracker.Items.Count); - Assert.All(eventsTracker.Items.Select(x => x.Event), x => Assert.Same(event1, x)); + Assert.All(eventsTracker.Items.Select(x => x.Item), x => Assert.Same(event1, x)); } public class TestEvent(string Info) @@ -102,7 +104,6 @@ public record HandlerSettings(int RetryAttempts, TimeSpan Delay); public class TestEventHandler : IEventHandler { - private readonly Random _random = new(); private readonly EventsTracker _tracker; private readonly HandlerSettings _settings; @@ -112,14 +113,15 @@ public TestEventHandler(HandlerSettings settings, EventsTracker tracker) _tracker = tracker; } - public async Task Handle(TestEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) + public Task Handle(TestEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) { _tracker.Track(@event); - await Task.Delay(_random.Next(1, 10)); if(retryPolicy.Attempt < _settings.RetryAttempts) { retryPolicy.RetryAfter((attempt, lastDelay) => TimeSpan.FromMilliseconds(100 * (attempt + 1)) + lastDelay); } + + return Task.CompletedTask; } public Task OnError(Exception exception, TestEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) diff --git a/test/M.EventBrokerSlim.Tests/RetryFromOnErrorTests.cs b/test/M.EventBrokerSlim.Tests/RetryFromOnErrorTests.cs index d3b15f7..21ee30d 100644 --- a/test/M.EventBrokerSlim.Tests/RetryFromOnErrorTests.cs +++ b/test/M.EventBrokerSlim.Tests/RetryFromOnErrorTests.cs @@ -13,23 +13,24 @@ public async Task OnError_SingleRetry_RetriesOnce_With_GivenDelay(int maxConcurr sc => sc.AddEventBroker( x => x.WithMaxConcurrentHandlers(maxConcurrentHandlers) .AddTransient()) - .AddSingleton(new HandlerSettings(RetryAttempts: 1, Delay: TimeSpan.FromMilliseconds(100))) + .AddSingleton(new HandlerSettings(RetryAttempts: 1, Delay: TimeSpan.FromMilliseconds(300))) .AddSingleton()); using var scope = services.CreateScope(); var eventBroker = scope.ServiceProvider.GetRequiredService(); var eventsTracker = scope.ServiceProvider.GetRequiredService(); + eventsTracker.ExpectedItemsCount = 2; var event1 = new TestEvent("test"); // Act await eventBroker.Publish(event1); - await eventsTracker.Wait(TimeSpan.FromMilliseconds(200)); + await eventsTracker.Wait(TimeSpan.FromSeconds(1)); // Assert Assert.Equal(2, eventsTracker.Items.Count); var timestamps = eventsTracker.Items.OrderBy(x => x.Timestamp).Select(x => x.Timestamp).ToArray(); - Assert.Equal(100, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 60); + Assert.Equal(300, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 50); } [Theory] @@ -43,25 +44,26 @@ public async Task OnError_MultipleRetries_RetriesMultipleTimes_With_GivenDelay(i sc => sc.AddEventBroker( x => x.WithMaxConcurrentHandlers(maxConcurrentHandlers) .AddTransient()) - .AddSingleton(new HandlerSettings(RetryAttempts: 3, Delay: TimeSpan.FromMilliseconds(150))) + .AddSingleton(new HandlerSettings(RetryAttempts: 3, Delay: TimeSpan.FromMilliseconds(300))) .AddSingleton()); using var scope = services.CreateScope(); var eventBroker = scope.ServiceProvider.GetRequiredService(); var eventsTracker = scope.ServiceProvider.GetRequiredService(); + eventsTracker.ExpectedItemsCount = 4; var event1 = new TestEvent("test"); // Act await eventBroker.Publish(event1); - await eventsTracker.Wait(TimeSpan.FromMilliseconds(700)); + await eventsTracker.Wait(TimeSpan.FromSeconds(2)); // Assert Assert.Equal(4, eventsTracker.Items.Count); var timestamps = eventsTracker.Items.OrderBy(x => x.Timestamp).Select(x => x.Timestamp).ToArray(); - Assert.Equal(150, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 60); - Assert.Equal(150, (timestamps[2] - timestamps[1]).TotalMilliseconds, tolerance: 60); - Assert.Equal(150, (timestamps[3] - timestamps[2]).TotalMilliseconds, tolerance: 60); + Assert.Equal(300, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 50); + Assert.Equal(300, (timestamps[2] - timestamps[1]).TotalMilliseconds, tolerance: 50); + Assert.Equal(300, (timestamps[3] - timestamps[2]).TotalMilliseconds, tolerance: 50); } [Theory] @@ -82,18 +84,19 @@ public async Task OnError_MultipleRetries_RetriesMultipleTimes_With_ZeroDelay(in var eventBroker = scope.ServiceProvider.GetRequiredService(); var eventsTracker = scope.ServiceProvider.GetRequiredService(); + eventsTracker.ExpectedItemsCount = 4; var event1 = new TestEvent("test"); // Act await eventBroker.Publish(event1); - await eventsTracker.Wait(TimeSpan.FromMilliseconds(700)); + await eventsTracker.Wait(TimeSpan.FromSeconds(1)); // Assert Assert.Equal(4, eventsTracker.Items.Count); var timestamps = eventsTracker.Items.OrderBy(x => x.Timestamp).Select(x => x.Timestamp).ToArray(); - Assert.Equal(0, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 60); - Assert.Equal(0, (timestamps[2] - timestamps[1]).TotalMilliseconds, tolerance: 60); - Assert.Equal(0, (timestamps[3] - timestamps[2]).TotalMilliseconds, tolerance: 60); + Assert.Equal(0, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 50); + Assert.Equal(0, (timestamps[2] - timestamps[1]).TotalMilliseconds, tolerance: 50); + Assert.Equal(0, (timestamps[3] - timestamps[2]).TotalMilliseconds, tolerance: 50); } [Theory] @@ -107,22 +110,23 @@ public async Task OnError_MultipleRetries_Event_IsTheSameInstance_EveryTime(int sc => sc.AddEventBroker( x => x.WithMaxConcurrentHandlers(maxConcurrentHandlers) .AddTransient()) - .AddSingleton(new HandlerSettings(RetryAttempts: 3, Delay: TimeSpan.FromMilliseconds(150))) + .AddSingleton(new HandlerSettings(RetryAttempts: 3, Delay: TimeSpan.FromMilliseconds(300))) .AddSingleton()); using var scope = services.CreateScope(); var eventBroker = scope.ServiceProvider.GetRequiredService(); var eventsTracker = scope.ServiceProvider.GetRequiredService(); + eventsTracker.ExpectedItemsCount = 4; var event1 = new TestEvent("test"); // Act await eventBroker.Publish(event1); - await eventsTracker.Wait(TimeSpan.FromMilliseconds(700)); + await eventsTracker.Wait(TimeSpan.FromSeconds(2)); // Assert Assert.Equal(4, eventsTracker.Items.Count); - Assert.All(eventsTracker.Items.Select(x => x.Event), x => Assert.Same(event1, x)); + Assert.All(eventsTracker.Items.Select(x => x.Item), x => Assert.Same(event1, x)); } public class TestEvent(string Info) @@ -134,7 +138,6 @@ public record HandlerSettings(int RetryAttempts, TimeSpan Delay); public class TestEventHandler : IEventHandler { - private readonly Random _random = new(); private readonly EventsTracker _tracker; private readonly HandlerSettings _settings; @@ -144,9 +147,8 @@ public TestEventHandler(HandlerSettings settings, EventsTracker tracker) _tracker = tracker; } - public async Task Handle(TestEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) + public Task Handle(TestEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) { - await Task.Delay(_random.Next(1, 10)); throw new NotImplementedException(); } diff --git a/test/M.EventBrokerSlim.Tests/RetryFromOnErrorUsingDelayDelegateTests.cs b/test/M.EventBrokerSlim.Tests/RetryFromOnErrorUsingDelayDelegateTests.cs index efd7351..0e97282 100644 --- a/test/M.EventBrokerSlim.Tests/RetryFromOnErrorUsingDelayDelegateTests.cs +++ b/test/M.EventBrokerSlim.Tests/RetryFromOnErrorUsingDelayDelegateTests.cs @@ -1,4 +1,6 @@ -namespace M.EventBrokerSlim.Tests; +using System; + +namespace M.EventBrokerSlim.Tests; public class RetryFromOnErrorUsingDelayDelegateTests { @@ -20,16 +22,17 @@ public async Task OnError_SingleRetry_RetriesOnce_With_GivenDelay(int maxConcurr var eventBroker = scope.ServiceProvider.GetRequiredService(); var eventsTracker = scope.ServiceProvider.GetRequiredService(); + eventsTracker.ExpectedItemsCount = 2; var event1 = new TestEvent("test"); // Act await eventBroker.Publish(event1); - await eventsTracker.Wait(TimeSpan.FromMilliseconds(300)); + await eventsTracker.Wait(TimeSpan.FromSeconds(2)); // Assert Assert.Equal(2, eventsTracker.Items.Count); var timestamps = eventsTracker.Items.OrderBy(x => x.Timestamp).Select(x => x.Timestamp).ToArray(); - Assert.Equal(100, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 60); + Assert.Equal(100, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 50); } [Theory] @@ -50,18 +53,19 @@ public async Task OnError_MultipleRetries_RetriesMultipleTimes_With_GivenDelay(i var eventBroker = scope.ServiceProvider.GetRequiredService(); var eventsTracker = scope.ServiceProvider.GetRequiredService(); + eventsTracker.ExpectedItemsCount = 4; var event1 = new TestEvent("test"); // Act await eventBroker.Publish(event1); - await eventsTracker.Wait(TimeSpan.FromMilliseconds(1200)); + await eventsTracker.Wait(TimeSpan.FromSeconds(2)); // Assert Assert.Equal(4, eventsTracker.Items.Count); var timestamps = eventsTracker.Items.OrderBy(x => x.Timestamp).Select(x => x.Timestamp).ToArray(); - Assert.Equal(100, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 60); - Assert.Equal(300, (timestamps[2] - timestamps[1]).TotalMilliseconds, tolerance: 60); - Assert.Equal(600, (timestamps[3] - timestamps[2]).TotalMilliseconds, tolerance: 80); + Assert.Equal(100, (timestamps[1] - timestamps[0]).TotalMilliseconds, tolerance: 50); + Assert.Equal(300, (timestamps[2] - timestamps[1]).TotalMilliseconds, tolerance: 50); + Assert.Equal(600, (timestamps[3] - timestamps[2]).TotalMilliseconds, tolerance: 50); } [Theory] @@ -82,15 +86,16 @@ public async Task OnError_MultipleRetries_Event_IsTheSameInstance_EveryTime(int var eventBroker = scope.ServiceProvider.GetRequiredService(); var eventsTracker = scope.ServiceProvider.GetRequiredService(); + eventsTracker.ExpectedItemsCount = 4; var event1 = new TestEvent("test"); // Act await eventBroker.Publish(event1); - await eventsTracker.Wait(TimeSpan.FromMilliseconds(1200)); + await eventsTracker.Wait(TimeSpan.FromSeconds(3)); // Assert Assert.Equal(4, eventsTracker.Items.Count); - Assert.All(eventsTracker.Items.Select(x => x.Event), x => Assert.Same(event1, x)); + Assert.All(eventsTracker.Items.Select(x => x.Item), x => Assert.Same(event1, x)); } public class TestEvent(string Info) @@ -102,7 +107,6 @@ public record HandlerSettings(int RetryAttempts, TimeSpan Delay); public class TestEventHandler : IEventHandler { - private readonly Random _random = new(); private readonly EventsTracker _tracker; private readonly HandlerSettings _settings; @@ -112,9 +116,8 @@ public TestEventHandler(HandlerSettings settings, EventsTracker tracker) _tracker = tracker; } - public async Task Handle(TestEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) + public Task Handle(TestEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) { - await Task.Delay(_random.Next(1, 10)); throw new NotImplementedException(); } diff --git a/test/M.EventBrokerSlim.Tests/RetryOverrideFromOnErrorTests.cs b/test/M.EventBrokerSlim.Tests/RetryOverrideFromOnErrorTests.cs index 8c2ec37..d350e48 100644 --- a/test/M.EventBrokerSlim.Tests/RetryOverrideFromOnErrorTests.cs +++ b/test/M.EventBrokerSlim.Tests/RetryOverrideFromOnErrorTests.cs @@ -20,11 +20,12 @@ public async Task OnError_Overrides_Handle_RetryPolicy_Delay(int maxConcurrentHa var eventBroker = scope.ServiceProvider.GetRequiredService(); var eventsTracker = scope.ServiceProvider.GetRequiredService(); + eventsTracker.ExpectedItemsCount = 2; var event1 = new TestEvent("test"); // Act await eventBroker.Publish(event1); - await eventsTracker.Wait(TimeSpan.FromMilliseconds(500)); + await eventsTracker.Wait(TimeSpan.FromSeconds(2)); // Assert Assert.Equal(2, eventsTracker.Items.Count); @@ -41,7 +42,6 @@ public record HandlerSettings(int RetryAttempts, TimeSpan Delay); public class TestEventHandler : IEventHandler { - private readonly Random _random = new(); private readonly EventsTracker _tracker; private readonly HandlerSettings _settings; @@ -51,9 +51,8 @@ public TestEventHandler(HandlerSettings settings, EventsTracker tracker) _tracker = tracker; } - public async Task Handle(TestEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) + public Task Handle(TestEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken) { - await Task.Delay(_random.Next(1, 10)); if(retryPolicy.Attempt < _settings.RetryAttempts) { retryPolicy.RetryAfter(_settings.Delay); diff --git a/test/M.EventBrokerSlim.Tests/RetryPolicyTests.cs b/test/M.EventBrokerSlim.Tests/RetryPolicyTests.cs index f9c4ab4..47b278f 100644 --- a/test/M.EventBrokerSlim.Tests/RetryPolicyTests.cs +++ b/test/M.EventBrokerSlim.Tests/RetryPolicyTests.cs @@ -24,12 +24,12 @@ public async Task MultipleRetries_RetryPolicy_IsTheSameInstance_EveryTime(int ma // Act await eventBroker.Publish(event1); - await eventsTracker.Wait(TimeSpan.FromMilliseconds(400)); + await eventsTracker.Wait(TimeSpan.FromSeconds(1)); // Assert Assert.Equal(8, eventsTracker.Items.Count); - var retryPolicy = eventsTracker.Items.First().Event; - Assert.All(eventsTracker.Items.Select(x => x.Event), x => Assert.Same(retryPolicy, x)); + var retryPolicy = eventsTracker.Items.First().Item; + Assert.All(eventsTracker.Items.Select(x => x.Item), x => Assert.Same(retryPolicy, x)); } public class TestEvent(string Info) @@ -57,6 +57,7 @@ public Task Handle(TestEvent @event, IRetryPolicy retryPolicy, CancellationToken { retryPolicy.RetryAfter(_settings.Delay); } + throw new NotImplementedException(); } diff --git a/test/M.EventBrokerSlim.Tests/ServiceProviderHelper.cs b/test/M.EventBrokerSlim.Tests/ServiceProviderHelper.cs index 6c95740..e0d3708 100644 --- a/test/M.EventBrokerSlim.Tests/ServiceProviderHelper.cs +++ b/test/M.EventBrokerSlim.Tests/ServiceProviderHelper.cs @@ -25,6 +25,13 @@ public static ServiceProvider BuildWithEventsRecorderAndLogger(Action configure) + { + ServiceCollection serviceCollection = CreateServiceCollection(configure); + serviceCollection.AddLogging(x => x.AddDebug().AddTest()); + return serviceCollection.BuildServiceProvider(true); + } + private static ServiceCollection CreateServiceCollection(Action configure) { var serviceCollection = new ServiceCollection();