MASA Framework - EventBus design

Posted by NathanLedet on Fri, 14 Jan 2022 13:39:09 +0100

summary

The publish subscribe mode is used to decouple different architecture levels, and can also be used to isolate the interaction between businesses

advantage:

  • loose coupling
  • crosscutting concern
  • Testability
  • event driven

Publish subscribe mode

The publisher sends the message to the subscriber through the dispatching center. The dispatching center solves the relationship between the publisher and the subscriber to ensure that the message can be delivered to the subscriber.

Publishers and subscribers do not know each other. Publishers only publish messages to the dispatching center, while subscribers only care about the types of messages they subscribe to

Multi subscriber order preserving execution

It is true that similar statements are rarely seen in common publish subscribe patterns. However, in the actual business, we will have similar requirements. For a message, the scheduling center coordinates multiple subscribers to execute the message in order, and can also pass the message processed by the previous subscriber to the next subscriber. In this way, we can not only retain the characteristics of publish subscribe mode, but also have the characteristics of sequential execution logic.

A little thought: if the configuration of EventBus supports dynamic adjustment, can the execution order of business be dynamically arranged and combined?

In other words, it may provide a possibility for in-process workflow

Event Sourcing

An event driven architecture pattern that can be used for auditing and traceability

  • Event driven architecture
  • Take events as facts
  • The view of business data generated by event calculation can be persistent or non persistent

CQRS (separation of responsibilities for command query)

CQRS is an architecture pattern that can separate the implementation of change model and query model

Event Sourcing & CQRS

Event traceability can cooperate well with CQRS

  • While persisting events to the Event Store in the Command Handler, a final view is calculated in real time to the View DB for query and display
  • In Query, you can get the latest status through the View DB, replay events through the Event Store to verify the View or use it for more rigorous business

Saga

Saga is a long-lived transaction, which is decomposed into a set of sub transactions that can be interleaved. Each of these sub transactions is a real transaction that maintains database consistency

  • Each Saga consists of a series of sub transaction ti
  • Each Ti has a corresponding compensation action Ci, which is used to cancel the result caused by Ti

Two execution sequences

  • T1, T2, T3...[Tx retry]...,Tn
  • T1, T2, ..., Tj, Cj,..., C2, C1

Two recovery strategies

  • backward recovery, which compensates all completed transactions if any sub transaction fails. That is, the second execution sequence mentioned above, where j is the wrong sub transaction. The effect of this method is to undo all previous successful sub transactions and undo the execution results of the whole Saga
  • Forward recovery, forward recovery, retry failed transactions, assuming that each sub transaction will succeed in the end. For the scenario that must succeed, the execution sequence is similar to this: T1, T2,..., TJ (failure), TJ (retry),..., Tn, where j is the sub transaction with error. Ci is not required in this case

Class view of BuildingBlocks

As an interface standard, BuildingBlocks does not have too many interference implementation methods. It only retains the most basic function flow restrictions to achieve the minimum function set of EventBus. Whether to implement the subscription relationship based on interface or feature is left to Contrib to decide.

event

Publish / subscribe for local events

  • IEvent: event interface. IEvent < tresult > is the basic event interface with return value
  • Ieventhandler < tevent >: event handler interface. Isagaeventhandler < tevent > provides basic interface requirements for the implementation of Saga
  • Imidleware < tevent >: a middleware interface that allows pre-processing actions and closing actions after time execution to be mounted before event execution
  • IEventBus: event bus interface, which is used to send events and provide subscription relationship maintenance and additional function execution

Integration event

Publish / subscribe for cross process events

  • Integration eventlog: the integration event log is used to implement the message model of the local message table
  • Iiintegrationeventlogservice: integrated event log service interface
  • ITopic: publish / subscribe topics
  • Iiintegrationevent: integration event interface
  • Iiintegrationeventbus: integrated event bus, which is used for the event bus of cross process calls

CQRS

Used to separate the implementation of the change model from the query model

  • Iquery < tresult >: query interface
  • Iqueryhandler < tcommand, tresult >: query processor interface
  • ICommand: an interface that can be used to add, delete, modify and other instructions
  • Icommandhandler < tcommand >: instruction processor interface

Event Bus

To complete the above functions, we need to use EventBus, which needs the following basic functions

  • Receive event
  • Maintain subscription relationship
  • Forwarding events

Receive and forward events

In fact, these two functions can be combined into one interface. The publisher calls Publish, and then the Event Bus forwards it according to the subscription relationship

Maintain subscription relationship

Yes Net project, our common methods for scanning automatic registration are interfaces and features

MediatR supports the interface to scan the event subscription relationship, for example: irequesthandler <, >

public class PingHandler : IRequestHandler<Ping, string>
{
    public Task<string> Handle(Ping request, CancellationToken cancellationToken)
    {
        return Task.FromResult("Pong");
    }
}

If your code cleanliness is not too high, maybe you hope so

public class NetHandler : IRequestHandler<Ping, string>, IRequestHandler<Telnet, string>
{
    public Task<string> Handle(Ping request, CancellationToken cancellationToken)
    {
        return Task.FromResult("Pong");
    }

    public Task<string> Handle(Telnet request, CancellationToken cancellationToken)
    {
        return Task.FromResult("Success");
    }
}

Looks okay? What if a lot?

Is there any way to solve this problem?

characteristic! Let's take an example

public class NetHandler
{
    [EventHandler]
    public Task PingAsync(PingEvent @event)
    {
        //TODO
    }

    [EventHandler]
    public Task TelnetAsync(TelnetEvent @event)
    {
        //TODO
    }
}

It seems that we have found a way out

Multi subscriber order preserving execution

It is true that sequential execution scenarios can be satisfied by pushing events layer by layer, but if you are surrounded by a large number of infinite dolls, you may need another way out. See the following example:

public class NetHandler
{
    [EventHandler(0)]
    public Task PingAsync(PingEvent @event)
    {
        //TODO
    }

    [EventHandler(1)]
    public Task LogAsync(PingEvent @event)
    {
        //TODO
    }
}

As long as the parameter is the same Event, it will be executed according to the Order of EventHandler.

Saga

What if the execution fails? If one of the two methods cannot be combined with the local transaction because it needs to call the remote service, can you rollback it for me?

Come on, SAGA, let's do another cancellation for you. It also supports retry mechanism and whether to ignore the cancellation of the current step.

Let's preset the scene first:

  1. Call CheckBalanceAsync to check the balance
  2. Call WithdrawAsync and throw exception
  3. Retry WithdrawAsync 3 times
  4. Call CancelWithdrawAsync

The code is as follows:

public class TransferHandler
{
    [EventHandler(1)]
    public Task CheckBalanceAsync(TransferEvent @event)
    {
        //TODO
    }

    [EventHandler(2, FailureLevels.ThrowAndCancel, enableRetry: true, retryTimes: 3)]
    public Task WithdrawAsync(TransferEvent @event)
    {
        //TODO
        throw new Exception();
    }

    [EventHandler(2, FailureLevels.Ignore, enableRetry: false, isCancel: true)]
    public Task CancelWithdrawAsync(TransferEvent @event)
    {
        //TODO
    }
}

AOP

For a business scenario, add a parameter validation to all commands before execution

We provide middleware, which allows you to do things related to crosscutting concerns like Russian Dolls (. Net Middleware)

public class LoggingMiddleware<TEvent>
    : IMiddleware<TEvent> where TEvent : notnull, IEvent
{
    private readonly ILogger<LoggingMiddleware<TEvent>> _logger;

    public LoggingMiddleware(ILogger<LoggingMiddleware<TEvent>> logger) => _logger = logger;

    public async Task HandleAsync(TEvent @event, EventHandlerDelegate next)
    {
        _logger.LogInformation("----- Handling command {EventName} ({@Event})", typeof(TEvent).FullName, @event);
         await next();
    }
}

Register DI

builder.Services.AddTransient(typeof(IMiddleware<>), typeof(LoggingMiddleware<>))

Full function list of MASA EventBus

  • Receive event
  • Maintain subscription relationship - Interface
  • Maintain subscription relationships - properties
  • Multi subscriber sequential execution
  • Forwarding events
  • Saga
  • AOP
  • UoW
  • Automatically turn transactions on and off

Integration Event Bus

Event Bus for cross service, supporting final consistency and local message table

Pub/Sub

It provides a Pub Sub interface and provides a default implementation based on Dapr Pub/Sub

Local message table

It provides local message saving and UoW linkage interfaces, and provides default implementation based on EF Core

usage method

Enable Dapr Event Bus

builder.Services
    .AddDaprEventBus<IntegrationEventLogService>(options=>
    {
        options.UseUoW<CatalogDbContext>(dbOptions => dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=Password;database=test"))
               .UseEventLog<CatalogDbContext>();
        )
    });

Define Integration Event

public class DemoIntegrationEvent : IntegrationEvent
{
    public override string Topic { get; set; } = nameof(DemoIntegrationEvent);//dapr topic name

    //todo other properties
}

Define DbContext (not required. Defining DbContext can link local message tables with business transactions)

public class CustomDbContext : IntegrationEventLogContext
{
    public DbSet<User> Users { get; set; } = null!;

    public CustomDbContext(MasaDbContextOptions<CustomDbContext> options) : base(options)
    {

    }
}

Send Event

IIntegrationEventBus eventBus; // from DI
await eventBus.PublishAsync(new DemoIntegrationEvent());

Subscribe to Event (based on the version of Dapr Pub/Sub)

[Topic("pubsub", nameof(DomeIntegrationEvent))]
public async Task DomeIntegrationEventHandleAsync(DomeIntegrationEvent @event)
{
    //todo
}

Domain Event Bus

The ability to provide both Event Bus and Integration Event Bus in the field allows the event to be sent in real time or triggered at one time when saving

Domain Event Bus is the most complete capability, so using domain Event Bus is equivalent to enabling Event Bus and Integration Event Bus. Within domain Event Bus, event classification will be automatically coordinated to divert to Event Bus and Integration Event Bus

Enable Domain Event Bus

builder.Services
.AddDomainEventBus(options =>
{
    options.UseEventBus()//Use in-process events
        .UseUoW<CustomDbContext>(dbOptions => dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=P@ssw0rd;database=idientity"))
        .UseDaprEventBus<IntegrationEventLogService>()///Use cross-process events
        .UseEventLog<LocalMessageDbContext>()
        .UseRepository<CustomDbContext>();
})

Add DomainCommand

Domain Event is an in-process event and integration domainevent is a cross process event

public class RegisterUserSucceededIntegrationEvent : IntegrationDomainEvent
{
    public override string Topic { get; set; } = nameof(RegisterUserSucceededIntegrationEvent);

    public string Account { get; set; } = default!;
}

public class RegisterUserSucceededEvent : DomainEvent
{
    public string Account { get; set; } = default!;
}

In process event subscription

[EventHandler]
public Task RegisterUserHandlerAsync(RegisterUserDomainCommand command)
{
    //TODO
}

Cross process event subscription

[Topic("pubsub", nameof(RegisterUserSucceededIntegrationEvent))]
public async Task RegisterUserSucceededHandlerAsync(RegisterUserSucceededIntegrationEvent @event)
{
    //todo
}

Send DomainCommand

IDomainEventBus eventBus;//from DI
await eventBus.PublishAsync(new RegisterUserDomainCommand());

Usage scenario

  • Consider legacy system docking
  • Walking in clouds and non clouds
  • Flow calculation
  • Micro service decoupling and cross cluster communication (it is not difficult to change Dapr Pub/Sub to Dapr Binding)
  • Some AOP scenarios

summary

Event driven can solve the problems of some specific scenarios. Everything has two sides. Using such a complex pattern in an already simple business scenario will bring a lot of burden.

There is no end to learning.

Open source address

MASA.BuildingBlocks: https://github.com/masastack/MASA.BuildingBlocks

MASA.Contrib: https://github.com/masastack/MASA.Contrib

MASA.Utils: https://github.com/masastack/MASA.Utils

MASA.EShop: https://github.com/masalabs/MASA.EShop

If you are interested in our MASA Framework, whether it is code contribution, use or Issue, please contact us

Topics: C# .NET Framework