[Abp vNext Source Analysis] - 13. Local Event Bus and Distributed Event Bus (Rabbit MQ)

1. Brief introduction

ABP vNext encapsulates two event bus structures. The first is the local event bus implemented by ABP vNext itself, which cannot be published and subscribed across projects. The second is a distributed event bus, which ABP vNext encapsulates an abstraction layer to define itself and writes a basic implementation using RabbitMQ.

In terms of usage, the two event buses work essentially the same.

Event bus is distributed in two modules. Inside the Volo.Abp.EventBus module, the abstract interface of event bus and the implementation of local event bus are defined. The implementation of Distributed Event Bus is defined within the Volo.Abp.EventBus.RabbitMQ module, which is based on the RabbitMQ message queue as seen from the project name.


However, the project does not refer directly to the RabbitMQ.Client package, but rather to the Volo.Abp.RabbitMQ project itself. This is because ABP implements a background job manager based on RabbitMQ in addition to the distributed event bus.

The ABP vNext framework abstracts some objects and places them inside the Volo.Abp.RabbitMQ project for definition and implementation.

2. Source Code Analysis

Registration of 2.1 Event Processors

Analyzing the source code starts with a module for a project, and AbpEventBusModule, the module for the Volo.Abp.EventBus library, does only one thing. When a component is registered, it is assigned to the Hadlers property of AbpLocalEventBusOptions and AbpDistributedEventBusOptions, depending on the component's implementation interface (ILocalEventHandler or IDistributedEventHandler).

That is, when a developer-defined event handler relies on injection, its type is added to the configuration class of the event bus for subsequent use.

2.2 Event Bus Interface

From the unit test of the event bus module, we know that event publishing and subscribing are done through two subinterfaces of IEventBus (ILocalEventBus/IDistributedEventBus). There are three behaviors in the definition of the IEventBus interface: publish, subscribe, and unsubscribe.

For both ILocalEventBus and IDistributedEventBus interfaces, they provide a special subscription method for local event processors and distributed processors.

ILocalEventBus:

/// <summary>
/// Defines interface of the event bus.
/// </summary>
public interface ILocalEventBus : IEventBus
{
    /// <summary>
    /// Registers to an event. 
    /// Same (given) instance of the handler is used for all event occurrences.
    /// </summary>
    /// <typeparam name="TEvent">Event type</typeparam>
    /// <param name="handler">Object to handle the event</param>
    IDisposable Subscribe<TEvent>(ILocalEventHandler<TEvent> handler)
        where TEvent : class;
}

IDistributedEventBus:

public interface IDistributedEventBus : IEventBus
{
    /// <summary>
    /// Registers to an event. 
    /// Same (given) instance of the handler is used for all event occurrences.
    /// </summary>
    /// <typeparam name="TEvent">Event type</typeparam>
    /// <param name="handler">Object to handle the event</param>
    IDisposable Subscribe<TEvent>(IDistributedEventHandler<TEvent> handler)
        where TEvent : class;
}

2.3 Event Bus Basic Flow and Implementation

Like other modules, ABP vNext also abstracts an EventBusBase type as its base class implementation because of its distributed event bus and local event bus.

In general, we define an event, subscribe to it, specify an event handler, and publish the event at a certain time. For example, the following code:

First, an event handler is defined specifically for handling EntityChangedEventData <MyEntity>events.

public class MyEventHandler : ILocalEventHandler<EntityChangedEventData<MyEntity>>
{
    public int EntityChangedEventCount { get; set; }

    public Task HandleEventAsync(EntityChangedEventData<MyEntity> eventData)
    {
        EntityChangedEventCount++;
        return Task.CompletedTask;
    }
}
var handler = new MyEventHandler();

LocalEventBus.Subscribe<EntityChangedEventData<MyEntity>>(handler);

await LocalEventBus.PublishAsync(new EntityCreatedEventData<MyEntity>(new MyEntity()));

Subscription to 2.3.1 events

You can see that the subscription method defined by ILocalEventBus is used here, jumping to the internal implementation, and it is also the method invoked by EventBus.

public virtual IDisposable Subscribe<TEvent>(ILocalEventHandler<TEvent> handler) where TEvent : class
{
    // Calls the Subscribe method of the base class and passes the type of TEvent and the event handler.
    return Subscribe(typeof(TEvent), handler);
}
public virtual IDisposable Subscribe(Type eventType, IEventHandler handler)
{
    return Subscribe(eventType, new SingleInstanceHandlerFactory(handler));
}

You can see that a SingleInstanceHandlerFactory object is passed here. What is this for? The name suggests that this is a factory, a factory used to create Handlers, and a single-instance event processor factory.

Below is the definition of the IEventHandlerFactory interface and the SingleInstanceHandlerFactory implementation.

public interface IEventHandlerFactory
{
    // Gets an event handler wrapper object that can be called after the event handler has finished executing
    // IEventHandlerDisposeWrapper.Dispose() is released.
    IEventHandlerDisposeWrapper GetHandler();

    // Determines whether the same event handler already exists in an existing set of event handler factories.
    bool IsInFactories(List<IEventHandlerFactory> handlerFactories);
}

public class SingleInstanceHandlerFactory : IEventHandlerFactory
{
    // The event handler instance passed when the factory is constructed.
    public IEventHandler HandlerInstance { get; }


    public SingleInstanceHandlerFactory(IEventHandler handler)
    {
        HandlerInstance = handler;
    }

    // Wrap event handler instances through EventHandlerDisposeWrapper.
    public IEventHandlerDisposeWrapper GetHandler()
    {
        return new EventHandlerDisposeWrapper(HandlerInstance);
    }

    // Determines if an event handler for HandlerInstance already exists.
    public bool IsInFactories(List<IEventHandlerFactory> handlerFactories)
    {
        return handlerFactories
            .OfType<SingleInstanceHandlerFactory>()
            .Any(f => f.HandlerInstance == HandlerInstance);
    }
}

There are also three different implementations for the IEventHandlerFactory factory, which are illustrated in the following table.

Implementation TypeEffect
IocEventHandlerFactoryEach factory corresponds to a type of event handler and resolves the specific event handler through ScopeFactory. The life cycle is controlled by the scope, and when the scope is released, the corresponding event handler instance is destroyed.
SingleInstanceHandlerFactoryEach factory corresponds to a separate event handler instance, which is controlled by the creator.
TransientEventHandlerFactoryEach factory corresponds to an event handler type, except that it is not resolved by IoC, but constructed by the Activator.CreateInstance() method, which is an instantaneous object that is released when the wrapper is called Dispose.
TransientEventHandlerFactory<THandler>Each factory corresponds to the specified THandler event handler and has the same life cycle as the factory above.

These factories are those that use different factories for different subscription overloads or specify their own event handlers during subscription operations.

public virtual IDisposable Subscribe<TEvent, THandler>()
    where TEvent : class
    where THandler : IEventHandler, new()
{
    return Subscribe(typeof(TEvent), new TransientEventHandlerFactory<THandler>());
}

public virtual IDisposable Subscribe(Type eventType, IEventHandler handler)
{
    return Subscribe(eventType, new SingleInstanceHandlerFactory(handler));
}

However, there is a special behavior that allows developers to avoid explicit subscriptions. In the EventBus type, a SubscribeHandlers (ITypeList <IEventHandler> handlers) method is defined. This method receives a collection of types and traverses the collection to get the event type TEvent that the event handler listens on from the definition of the event handler.

Once the event type is obtained and the event handler type is known, the event bus can subscribe to events of type TEvent and use the IocEventHandlerFactory factory to construct the event handler.

protected virtual void SubscribeHandlers(ITypeList<IEventHandler> handlers)
{
    // The type of traversal event handler, which is essentially the collection passed to XXXOptions at module startup.
    foreach (var handler in handlers)
    {
        // Obtain all interface definitions for the event handler and iterate through the interfaces to check.
        var interfaces = handler.GetInterfaces();
        foreach (var @interface in interfaces)
        {
            // If the interface does not implement the IEventHandler type, it is ignored.
            if (!typeof(IEventHandler).GetTypeInfo().IsAssignableFrom(@interface))
            {
                continue;
            }

            // Obtain the event type defined from the generic parameter.
            var genericArgs = @interface.GetGenericArguments();
            // Subscription occurs when the generic parameter exactly matches 1.
            if (genericArgs.Length == 1)
            {
                Subscribe(genericArgs[0], new IocEventHandlerFactory(ServiceScopeFactory, handler));
            }
        }
    }
}

This subscription method is an abstract method in EventBus that is implemented on the local event bus and the distributed event bus, respectively. Let's start by explaining the logic of local events.

public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency
{
    protected ConcurrentDictionary<Type, List<IEventHandlerFactory>> HandlerFactories { get; }

    public LocalEventBus(
        IOptions<AbpLocalEventBusOptions> options,
        IServiceScopeFactory serviceScopeFactory)
        : base(serviceScopeFactory)
    {
        Options = options.Value;
        Logger = NullLogger<LocalEventBus>.Instance;

        HandlerFactories = new ConcurrentDictionary<Type, List<IEventHandlerFactory>>();

        // Call the method of the parent class to attempt to subscribe to the event handler that the module was scanned during initialization.
        SubscribeHandlers(Options.Handlers);
    }

    public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory)
    {
        GetOrCreateHandlerFactories(eventType)
            // Lock the collection to ensure thread safety.
            .Locking(factories =>
                {
                    // If a factory already exists within the collection, it is not added.
                    if (!factory.IsInFactories(factories))
                    {
                        factories.Add(factory);
                    }
                }
            );

        // Returns an event handler factory logout that cancels events previously subscribed to when the Dispose() method is called.
        return new EventHandlerFactoryUnregistrar(this, eventType, factory);
    }

    private List<IEventHandlerFactory> GetOrCreateHandlerFactories(Type eventType)
    {
        // Depending on the type of event, all event processor factories of that type are obtained from the dictionary.
        return HandlerFactories.GetOrAdd(eventType, (type) => new List<IEventHandlerFactory>());
    }
}

The above process, combined with EventBus and LocalEventBus, illustrates the event subscription process, in which event subscriptions operate on HandlerFactories, adding event processor factories for specified events, each of which is associated with a specific event processor instance/type.

Release of 2.3.2 events

When developers need to publish events, they typically pass the event type and event data to be triggered by invoking the responsive Publish Async method on the corresponding EventBus. In interfaces and base classes, signatures and implementations of two publishing methods are defined:

public virtual Task PublishAsync<TEvent>(TEvent eventData) where TEvent : class
{
    return PublishAsync(typeof(TEvent), eventData);
}

public abstract Task PublishAsync(Type eventType, object eventData);


The second method is also divided into local event bus implementation and distributed event bus implementation. Local events are relatively simple. First, we analyze the implementation of local event bus.

public override async Task PublishAsync(Type eventType, object eventData)
{
    // A set of exceptions is defined to receive all exceptions that occur when multiple event handlers execute.
    var exceptions = new List<Exception>();

    // Triggers the event handler.
    await TriggerHandlersAsync(eventType, eventData, exceptions);

    // If any exceptions occur, they are thrown to the previous call stack.
    if (exceptions.Any())
    {
        if (exceptions.Count == 1)
        {
            exceptions[0].ReThrow();
        }

        throw new AggregateException("More than one error has occurred while triggering the event: " + eventType, exceptions);
    }
}

You can see that the real trigger behavior is implemented inside TriggerHandlersAsync (Type eventType, object eventData, List <Exception> exceptions).

protected virtual async Task TriggerHandlersAsync(Type eventType, object eventData, List<Exception> exceptions)
{
    // For this purpose, it is equivalent to ConfigureAwait(false).
    // Specific can be referred to https://blogs.msdn.microsoft.com/benwilli/2017/02/09/an-alternative-to-configureawaitfalse-everywhere/ .
    await new SynchronizationContextRemover();

    // All of its event processor factories are derived from the type of event.
    foreach (var handlerFactories in GetHandlerFactories(eventType))
    {
        // Traverse through all event handler factories, get event handlers through Factory, and call Handler's HandleEventAsync method.
        foreach (var handlerFactory in handlerFactories.EventHandlerFactories)
        {
            await TriggerHandlerAsync(handlerFactory, handlerFactories.EventType, eventData, exceptions);
        }
    }

    // If the type inherits the IEventDataWithInheritableGenericArgument interface, it detects whether the generic parameter has a parent.
    // If there is a parent class, an event is published for its parent using the current event data.
    if (eventType.GetTypeInfo().IsGenericType &&
        eventType.GetGenericArguments().Length == 1 &&
        typeof(IEventDataWithInheritableGenericArgument).IsAssignableFrom(eventType))
    {
        var genericArg = eventType.GetGenericArguments()[0];
        var baseArg = genericArg.GetTypeInfo().BaseType;
        if (baseArg != null)
        {
            // Construct the event type of the base class, using the same generic definition as the current one, except that the generic parameter uses the base class.
            var baseEventType = eventType.GetGenericTypeDefinition().MakeGenericType(baseArg);
            // The construction parameters of the build type.
            var constructorArgs = ((IEventDataWithInheritableGenericArgument)eventData).GetConstructorArgs();
            // Construct a new instance of event data with event types and construction parameters.
            var baseEventData = Activator.CreateInstance(baseEventType, constructorArgs);
            // Publish similar events for the parent class.
            await PublishAsync(baseEventType, baseEventData);
        }
    }
}

Inside the above code, no event handler is actually executed, and the actual event handler executor is executed in the following way. ABP vNext implements triggering of type inheritance events by introducing the IEventDataWithInheritableGenericArgument interface, which provides a GetConstructorArgs() method definition for generating construction parameters later.

For example, there is a base event called EntityEventData <Student>, and if the Student inherits from Person, an EntityEventData <Person> event is also published when the event is triggered.

Execution of 2.3.3 Event Processor

Execution of a true event processor is achieved by the following method, the general idea is to build an instance of an event processor through an event bus factory. By reflection, the HandleEventAsync() method of the event handler is invoked. If an exception occurs during processing, place the exception data in the List<Exception>collection.

protected virtual async Task TriggerHandlerAsync(IEventHandlerFactory asyncHandlerFactory, Type eventType, object eventData, List<Exception> exceptions)
{
    using (var eventHandlerWrapper = asyncHandlerFactory.GetHandler())
    {
        try
        {
            // Gets the type of event handler.
            var handlerType = eventHandlerWrapper.EventHandler.GetType();

            // Determines whether the event handler is a local or distributed event.
            if (ReflectionHelper.IsAssignableToGenericType(handlerType, typeof(ILocalEventHandler<>)))
            {
                // Get the method definition.
                var method = typeof(ILocalEventHandler<>)
                    .MakeGenericType(eventType)
                    .GetMethod(
                        nameof(ILocalEventHandler<object>.HandleEventAsync),
                        new[] { eventType }
                    );

                // Call the method using an instance created by the factory.
                await (Task)method.Invoke(eventHandlerWrapper.EventHandler, new[] { eventData });
            }
            else if (ReflectionHelper.IsAssignableToGenericType(handlerType, typeof(IDistributedEventHandler<>)))
            {
                var method = typeof(IDistributedEventHandler<>)
                    .MakeGenericType(eventType)
                    .GetMethod(
                        nameof(IDistributedEventHandler<object>.HandleEventAsync),
                        new[] { eventType }
                    );

                await (Task)method.Invoke(eventHandlerWrapper.EventHandler, new[] { eventData });
            }
            else
            {
                // If they are not, the type is incorrect and an exception is thrown.
                throw new AbpException("The object instance is not an event handler. Object type: " + handlerType.AssemblyQualifiedName);
            }
        }
        // Exceptions caught are added uniformly to the exception collection.
        catch (TargetInvocationException ex)
        {
            exceptions.Add(ex.InnerException);
        }
        catch (Exception ex)
        {
            exceptions.Add(ex);
        }
    }
}

2.4 Distributed Event Bus

The implementation of distributed event bus is stored in Volo.Abp.EventBus.RabbitMQ, which has less code and consists of three files.

Inside the RabbitMQ module, there are only two things to do. First, get three parameters of the AbpRabbitMqEventBusOptions configuration from the JSON configuration file, then parse the RabbitMqDistributedEventBus instance and call the initialization method (Initialize()).

[DependsOn(
    typeof(AbpEventBusModule),
    typeof(AbpRabbitMqModule))]
public class AbpEventBusRabbitMqModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        var configuration = context.Services.GetConfiguration();

        // Read the configuration from the configuration file.
        Configure<AbpRabbitMqEventBusOptions>(configuration.GetSection("RabbitMQ:EventBus"));
    }

    public override void OnApplicationInitialization(ApplicationInitializationContext context)
    {
        // Initialization method is called.
        context
            .ServiceProvider
            .GetRequiredService<RabbitMqDistributedEventBus>()
            .Initialize();
    }
}

Initialization of 2.4.1 Distributed Event Bus

public void Initialize()
{
    // Create a consumer and configure switches and queues.
    Consumer = MessageConsumerFactory.Create(
        new ExchangeDeclareConfiguration(
            AbpRabbitMqEventBusOptions.ExchangeName,
            type: "direct",
            durable: true
        ),
        new QueueDeclareConfiguration(
            AbpRabbitMqEventBusOptions.ClientName,
            durable: true,
            exclusive: false,
            autoDelete: false
        ),
        AbpRabbitMqEventBusOptions.ConnectionName
    );

    // Consumers consume messages with specific execution logic.
    Consumer.OnMessageReceived(ProcessEventAsync);

    // Call the method of the base class to automatically subscribe to the corresponding event.
    SubscribeHandlers(AbpDistributedEventBusOptions.Handlers);
}

Subscription to 2.4.2 Distributed Events

When defining distributed events, we must use EventNameAttribute to declare routing keys for events.

public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory)
{
    var handlerFactories = GetOrCreateHandlerFactories(eventType);

    if (factory.IsInFactories(handlerFactories))
    {
        return NullDisposable.Instance;
    }

    handlerFactories.Add(factory);

    if (handlerFactories.Count == 1) //TODO: Multi-threading!
    {
        // Bind a routing key for the consumer, and when the corresponding event is received, the previously bound method will be triggered.
        Consumer.BindAsync(EventNameAttribute.GetNameOrDefault(eventType));
    }

    return new EventHandlerFactoryUnregistrar(this, eventType, factory);
}

When subscribing, the basic process and local event bus are basically the same except Consumer.BindAsync().

2.4.3 Distributed Event Publishing

Distributed event bus overrides the publishing method by first serializing event data internally using IRabbitMqSerializer (based on JSON.NET) and then delivering messages.

public override Task PublishAsync(Type eventType, object eventData)
{
    var eventName = EventNameAttribute.GetNameOrDefault(eventType);
    // Serialize event data.
    var body = Serializer.Serialize(eventData);

    // Create a channel for communication.
    using (var channel = ConnectionPool.Get(AbpRabbitMqEventBusOptions.ConnectionName).CreateModel())
    {
        channel.ExchangeDeclare(
            AbpRabbitMqEventBusOptions.ExchangeName,
            "direct",
            durable: true
        );
        
        // Change delivery mode to persistent mode.
        var properties = channel.CreateBasicProperties();
        properties.DeliveryMode = RabbitMqConsts.DeliveryModes.Persistent;

        // Publish a new event.
        channel.BasicPublish(
            exchange: AbpRabbitMqEventBusOptions.ExchangeName,
            routingKey: eventName,
            mandatory: true,
            basicProperties: properties,
            body: body
        );
    }

    return Task.CompletedTask;
}

Execution of 2.4.4 Distributed Events

Execution logic is stored inside the ProcessEventAsync(IModel channel, BasicDeliverEventArgs ea) method, basically listening to the specified message, first deserializing the message, calling the parent TriggerHandlersAsync to execute the specific event handler.

private async Task ProcessEventAsync(IModel channel, BasicDeliverEventArgs ea)
{
    var eventName = ea.RoutingKey;
    var eventType = EventTypes.GetOrDefault(eventName);
    if (eventType == null)
    {
        return;
    }

    var eventData = Serializer.Deserialize(ea.Body, eventType);

    await TriggerHandlersAsync(eventType, eventData);
}

3. Summary

ABP vNext provides us with a relatively complete local event bus and a distributed event bus based on RabbitMQ. In the normal development process, our local event bus should still be used more frequently, while the distributed event bus is still in a semi-finished product, many advanced features have not been implemented, such as retry strategy. Therefore, to use distributed event bus, it is recommended to use more mature CAP libraries instead of ABP vNext distributed event bus.

Posted by petezaman on Sat, 23 Oct 2021 10:05:17 -0700