Skip to content

Commit cf78fcb

Browse files
committed
merge: parallel event bus dispatch with catch-all, fault isolation, and DI auto-subscription
Evolve IEventBus/EventBusService from the serialized channel model to a parallel per-listener dispatcher: single-listener fast path, catch-all (IEvent) listeners, per-listener fault isolation, slow-listener telemetry, delegate Subscribe, and IDisposable unsubscribe tokens. Unify the sync/async listener split into a single contravariant IEventListener<T>. Add DI-native auto-subscription via RegisterEventListener<TEvent,TListener>() and a low-priority EventListenerActivator.
2 parents 267ca7b + f1940ef commit cf78fcb

24 files changed

Lines changed: 605 additions & 282 deletions

File tree

samples/SquidStd.Samples.Email/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262

6363
// Inbound: react to each received email on the event bus.
6464
var eventBus = bootstrap.Resolve<IEventBus>();
65-
eventBus.RegisterAsyncListener(new MailReceivedLogger());
65+
eventBus.RegisterListener(new MailReceivedLogger());
6666

6767
var outgoing = new OutgoingMailMessage
6868
{
@@ -85,7 +85,7 @@
8585
await bootstrap.StopAsync();
8686

8787
/// <summary>Logs every received email as it arrives on the event bus.</summary>
88-
public sealed class MailReceivedLogger : IAsyncEventListener<MailReceivedEvent>
88+
public sealed class MailReceivedLogger : IEventListener<MailReceivedEvent>
8989
{
9090
/// <summary>Handles a received-mail event.</summary>
9191
public Task HandleAsync(MailReceivedEvent eventData, CancellationToken cancellationToken)

samples/SquidStd.Samples.EventsJobsScheduling/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
#region step-1
2121

2222
var eventBus = bootstrap.Resolve<IEventBus>();
23-
eventBus.RegisterAsyncListener(new PingListener());
23+
eventBus.RegisterListener(new PingListener());
2424
await eventBus.PublishAsync(new PingEvent("hello"), CancellationToken.None);
2525

2626
#endregion
@@ -54,7 +54,7 @@
5454
public sealed record PingEvent(string Message) : IEvent;
5555

5656
/// <summary>Handles <see cref="PingEvent" />.</summary>
57-
public sealed class PingListener : IAsyncEventListener<PingEvent>
57+
public sealed class PingListener : IEventListener<PingEvent>
5858
{
5959
public Task HandleAsync(PingEvent eventData, CancellationToken cancellationToken)
6060
{
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using DryIoc;
2+
using SquidStd.Core.Interfaces.Events;
3+
4+
namespace SquidStd.Abstractions.Data.Internal.Events;
5+
6+
/// <summary>
7+
/// A declarative event-listener registration consumed by the bootstrap activator.
8+
/// The <see cref="Subscribe" /> closure captures the concrete event and listener types at
9+
/// registration time, so subscription needs no reflection.
10+
/// </summary>
11+
/// <param name="ListenerType">The concrete listener implementation type.</param>
12+
/// <param name="Subscribe">Resolves the listener and subscribes it to the bus.</param>
13+
public sealed record EventListenerRegistration(Type ListenerType, Action<IEventBus, IResolverContext> Subscribe);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using DryIoc;
2+
using SquidStd.Abstractions.Data.Internal.Events;
3+
using SquidStd.Abstractions.Extensions.Container;
4+
using SquidStd.Core.Interfaces.Events;
5+
6+
namespace SquidStd.Abstractions.Extensions.Events;
7+
8+
/// <summary>
9+
/// Registers event listeners for DI-native auto-subscription at bootstrap.
10+
/// </summary>
11+
public static class RegisterEventListenerExtension
12+
{
13+
/// <summary>
14+
/// Registers a listener implementation as a singleton and records it for auto-subscription.
15+
/// </summary>
16+
/// <typeparam name="TEvent">The event type the listener handles.</typeparam>
17+
/// <typeparam name="TListener">The listener implementation type.</typeparam>
18+
/// <param name="container">The DryIoc container.</param>
19+
/// <returns>The same container for chaining.</returns>
20+
public static IContainer RegisterEventListener<TEvent, TListener>(this IContainer container)
21+
where TEvent : IEvent
22+
where TListener : class, IEventListener<TEvent>
23+
{
24+
container.Register<TListener>(Reuse.Singleton);
25+
container.AddToRegisterTypedList(
26+
new EventListenerRegistration(
27+
typeof(TListener),
28+
(bus, resolver) => bus.RegisterListener<TEvent>(resolver.Resolve<TListener>())
29+
)
30+
);
31+
32+
return container;
33+
}
34+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace SquidStd.Core.Data.Events;
2+
3+
/// <summary>
4+
/// Options controlling event bus dispatch behavior.
5+
/// </summary>
6+
public sealed record EventBusOptions
7+
{
8+
/// <summary>
9+
/// Listeners whose handling exceeds this duration are logged as slow. Defaults to 100ms.
10+
/// </summary>
11+
public TimeSpan SlowListenerThreshold { get; init; } = TimeSpan.FromMilliseconds(100);
12+
}
Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,39 @@
11
namespace SquidStd.Core.Interfaces.Events;
22

33
/// <summary>
4-
/// Dispatches synchronous and asynchronous events to registered listeners.
4+
/// In-process event bus that dispatches events to registered listeners in parallel.
55
/// </summary>
66
public interface IEventBus
77
{
88
/// <summary>
9-
/// Dispatches an event to synchronous listeners and waits until every listener has completed.
9+
/// Publishes an event and blocks until every listener has completed.
1010
/// </summary>
1111
/// <param name="eventData">The event payload.</param>
1212
/// <typeparam name="TEvent">The event type.</typeparam>
1313
void Publish<TEvent>(TEvent eventData) where TEvent : IEvent;
1414

1515
/// <summary>
16-
/// Dispatches an event to asynchronous listeners and waits until every listener has completed.
16+
/// Publishes an event and completes when every listener has finished.
1717
/// </summary>
1818
/// <param name="eventData">The event payload.</param>
1919
/// <param name="cancellationToken">The cancellation token.</param>
2020
/// <typeparam name="TEvent">The event type.</typeparam>
21-
/// <returns>A task that completes after all asynchronous listeners finish.</returns>
22-
Task PublishAsync<TEvent>(TEvent eventData, CancellationToken cancellationToken) where TEvent : IEvent;
21+
/// <returns>A task that completes after all listeners finish.</returns>
22+
Task PublishAsync<TEvent>(TEvent eventData, CancellationToken cancellationToken = default) where TEvent : IEvent;
2323

2424
/// <summary>
25-
/// Registers an asynchronous listener for the specified event type.
25+
/// Registers a listener for the specified event type. Dispose the returned token to unsubscribe.
2626
/// </summary>
27-
/// <param name="listener">Listener that handles published events asynchronously.</param>
27+
/// <param name="listener">The listener to register.</param>
2828
/// <typeparam name="TEvent">The event type.</typeparam>
29-
void RegisterAsyncListener<TEvent>(IAsyncEventListener<TEvent> listener) where TEvent : IEvent;
29+
/// <returns>A token that unsubscribes the listener when disposed.</returns>
30+
IDisposable RegisterListener<TEvent>(IEventListener<TEvent> listener) where TEvent : IEvent;
3031

3132
/// <summary>
32-
/// Registers a synchronous listener for the specified event type.
33+
/// Subscribes a delegate handler for the specified event type. Dispose the returned token to unsubscribe.
3334
/// </summary>
34-
/// <param name="listener">Listener that handles published events.</param>
35+
/// <param name="handler">The handler invoked for each published event.</param>
3536
/// <typeparam name="TEvent">The event type.</typeparam>
36-
void RegisterListener<TEvent>(ISyncEventListener<TEvent> listener) where TEvent : IEvent;
37+
/// <returns>A token that unsubscribes the handler when disposed.</returns>
38+
IDisposable Subscribe<TEvent>(Func<TEvent, CancellationToken, Task> handler) where TEvent : IEvent;
3739
}

src/SquidStd.Core/Interfaces/Events/IAsyncEventListener.cs renamed to src/SquidStd.Core/Interfaces/Events/IEventListener.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
namespace SquidStd.Core.Interfaces.Events;
22

33
/// <summary>
4-
/// Handles an event asynchronously.
4+
/// Handles an event published on the SquidStd event bus.
55
/// </summary>
6-
/// <typeparam name="TEvent">The event type.</typeparam>
7-
public interface IAsyncEventListener<in TEvent> where TEvent : IEvent
6+
/// <typeparam name="TEvent">The event type handled by the listener.</typeparam>
7+
public interface IEventListener<in TEvent> where TEvent : IEvent
88
{
99
/// <summary>
10-
/// Handles the event.
10+
/// Handles a published event.
1111
/// </summary>
1212
/// <param name="eventData">The event payload.</param>
1313
/// <param name="cancellationToken">The cancellation token.</param>
1414
/// <returns>A task that completes when the listener finishes handling the event.</returns>
15-
Task HandleAsync(TEvent eventData, CancellationToken cancellationToken);
15+
Task HandleAsync(TEvent eventData, CancellationToken cancellationToken = default);
1616
}

src/SquidStd.Core/Interfaces/Events/ISyncEventListener.cs

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/SquidStd.Services.Core/Extensions/RegisterDefaultServicesExtensions.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using SquidStd.Abstractions.Extensions.Container;
55
using SquidStd.Abstractions.Extensions.Services;
66
using SquidStd.Core.Data.Bootstrap;
7+
using SquidStd.Core.Data.Events;
78
using SquidStd.Core.Data.Jobs;
89
using SquidStd.Core.Data.Metrics;
910
using SquidStd.Core.Data.Storage;
@@ -114,7 +115,19 @@ public IContainer RegisterDefaultCoreConfigSections()
114115
/// </summary>
115116
/// <returns>The same container for chaining.</returns>
116117
public IContainer RegisterEventBusService()
117-
=> container.RegisterStdService<IEventBus, EventBusService>(-1);
118+
{
119+
container.RegisterInstance(new EventBusOptions());
120+
container.RegisterDelegate<IEventBus>(
121+
resolver => new EventBusService(resolver.Resolve<EventBusOptions>()),
122+
Reuse.Singleton
123+
);
124+
container.AddToRegisterTypedList(
125+
new ServiceRegistrationData(typeof(IEventBus), typeof(EventBusService), -1)
126+
);
127+
container.RegisterStdService<EventListenerActivator, EventListenerActivator>(-900);
128+
129+
return container;
130+
}
118131

119132
/// <summary>
120133
/// Registers the default job system service in the container.

0 commit comments

Comments
 (0)