Hand-Making Wheels: Implementing EventBus Based on Redis

Posted by jonasr on Mon, 29 Jul 2019 06:31:07 +0200

Hand-Making Wheels: Implementing EventBus Based on Redis
Intro#
Last time we built a simple memory-based EventBus, but it would be inappropriate to cross-system, so we have this Redis-based EventBus exploration.

The implementation of this paper is based on StackExchange.Redis.

RedisEventStore Implementation#
Since it is naturally impossible to reuse memory-based EventStore for cross-system EventBus, an EventStore InRedis is designed based on Redis, which is implemented by Hash based on redis. EventKey of Event is used as field Name and EventHandler corresponding to Event is used as value.

EventStoreInRedis implementation:

Copy
public class EventStoreInRedis : IEventStore
{

protected readonly string EventsCacheKey;
protected readonly ILogger Logger;

private readonly IRedisWrapper Wrapper;

public EventStoreInRedis(ILogger<EventStoreInRedis> logger)
{
    Logger = logger;
    Wrapper = new RedisWrapper(RedisConstants.EventStorePrefix);

    EventsCacheKey = RedisManager.RedisConfiguration.EventStoreCacheKey;
}

public bool AddSubscription<TEvent, TEventHandler>()
    where TEvent : IEventBase
    where TEventHandler : IEventHandler<TEvent>
{
    var eventKey = GetEventKey<TEvent>();
    var handlerType = typeof(TEventHandler);
    if (Wrapper.Database.HashExists(EventsCacheKey, eventKey))
    {
        var handlers = Wrapper.Unwrap<HashSet<Type>>(Wrapper.Database.HashGet(EventsCacheKey, eventKey));

        if (handlers.Contains(handlerType))
        {
            return false;
        }
        handlers.Add(handlerType);
        Wrapper.Database.HashSet(EventsCacheKey, eventKey, Wrapper.Wrap(handlers));
        return true;
    }
    else
    {
        return Wrapper.Database.HashSet(EventsCacheKey, eventKey, Wrapper.Wrap(new HashSet<Type> { handlerType }), StackExchange.Redis.When.NotExists);
    }
}

public bool Clear()
{
    return Wrapper.Database.KeyDelete(EventsCacheKey);
}

public ICollection<Type> GetEventHandlerTypes<TEvent>() where TEvent : IEventBase
{
    var eventKey = GetEventKey<TEvent>();
    return Wrapper.Unwrap<HashSet<Type>>(Wrapper.Database.HashGet(EventsCacheKey, eventKey));
}

public string GetEventKey<TEvent>()
{
    return typeof(TEvent).FullName;
}

public bool HasSubscriptionsForEvent<TEvent>() where TEvent : IEventBase
{
    var eventKey = GetEventKey<TEvent>();
    return Wrapper.Database.HashExists(EventsCacheKey, eventKey);
}

public bool RemoveSubscription<TEvent, TEventHandler>()
    where TEvent : IEventBase
    where TEventHandler : IEventHandler<TEvent>
{
    var eventKey = GetEventKey<TEvent>();
    var handlerType = typeof(TEventHandler);

    if (!Wrapper.Database.HashExists(EventsCacheKey, eventKey))
    {
        return false;
    }

    var handlers = Wrapper.Unwrap<HashSet<Type>>(Wrapper.Database.HashGet(EventsCacheKey, eventKey));

    if (!handlers.Contains(handlerType))
    {
        return false;
    }

    handlers.Remove(handlerType);
    Wrapper.Database.HashSet(EventsCacheKey, eventKey, Wrapper.Wrap(handlers));
    return true;
}

}
RedisWrapper and more specific code can refer to my implementation of Redis extensions https://github.com/WeihanLi/WeihanLi.Redis

RedisEventBus Implementation#
RedisEventBus is based on Redis's PUB/SUB. There are some small problems in the implementation. I want to make sure that every EventHandler registers only once even if it registers several times. But I haven't found a good implementation yet. If you have any ideas, please point out and talk with me. Specific implementation details are as follows:

Copy
public class RedisEventBus : IEventBus
{

private readonly IEventStore _eventStore;
private readonly ISubscriber _subscriber;
private readonly IServiceProvider _serviceProvider;

public RedisEventBus(IEventStore eventStore, IConnectionMultiplexer connectionMultiplexer, IServiceProvider serviceProvider)
{
    _eventStore = eventStore;
    _serviceProvider = serviceProvider;
    _subscriber = connectionMultiplexer.GetSubscriber();
}

private string GetChannelPrefix<TEvent>() where TEvent : IEventBase
{
    var eventKey = _eventStore.GetEventKey<TEvent>();
    var channelPrefix =
        $"{RedisManager.RedisConfiguration.EventBusChannelPrefix}{RedisManager.RedisConfiguration.KeySeparator}{eventKey}{RedisManager.RedisConfiguration.KeySeparator}";
    return channelPrefix;
}

private string GetChannelName<TEvent, TEventHandler>() where TEvent : IEventBase
    where TEventHandler : IEventHandler<TEvent>
    => GetChannelName<TEvent>(typeof(TEventHandler));

private string GetChannelName<TEvent>(Type eventHandlerType) where TEvent : IEventBase
{
    var channelPrefix = GetChannelPrefix<TEvent>();
    var channelName = $"{channelPrefix}{eventHandlerType.FullName}";

    return channelName;
}

public bool Publish<TEvent>(TEvent @event) where TEvent : IEventBase
{
    if (!_eventStore.HasSubscriptionsForEvent<TEvent>())
    {
        return false;
    }

    var eventData = @event.ToJson();
    var handlerTypes = _eventStore.GetEventHandlerTypes<TEvent>();
    foreach (var handlerType in handlerTypes)
    {
        var handlerChannelName = GetChannelName<TEvent>(handlerType);
        _subscriber.Publish(handlerChannelName, eventData);
    }

    return true;
}

public bool Subscribe<TEvent, TEventHandler>()
    where TEvent : IEventBase
    where TEventHandler : IEventHandler<TEvent>
{
    _eventStore.AddSubscription<TEvent, TEventHandler>();

    var channelName = GetChannelName<TEvent, TEventHandler>();

    //// TODO: if current client subscribed the channel
    //if (true)
    //{
    _subscriber.Subscribe(channelName, async (channel, eventMessage) =>
    {
        var eventData = eventMessage.ToString().JsonToType<TEvent>();
        var handler = _serviceProvider.GetServiceOrCreateInstance<TEventHandler>();
        if (null != handler)
        {
            await handler.Handle(eventData).ConfigureAwait(false);
        }
    });
    return true;
    //}

    //return false;
}

public bool Unsubscribe<TEvent, TEventHandler>()
    where TEvent : IEventBase
    where TEventHandler : IEventHandler<TEvent>
{
    _eventStore.RemoveSubscription<TEvent, TEventHandler>();

    var channelName = GetChannelName<TEvent, TEventHandler>();

    //// TODO: if current client subscribed the channel
    //if (true)
    //{
    _subscriber.Unsubscribe(channelName);
    return true;
    //}
    //return false;
}

}
Use examples:#
Generally speaking, it is the same as the previous one, but when we initialize the injection service, we need to replace IEventBus and IEventStore with the corresponding Redis implementation.

Registration Service

Copy
services.AddSingleton();
services.AddSingleton();
Register EventHandler

Copy
services.AddSingleton();
Subscription events

Copy
eventBus.Subscribe();
Publishing events

Copy
[HttpGet("{path}")]
public async Task GetByPath(string path, CancellationToken cancellationToken, [FromServices]IEventBus eventBus)
{

var notice = await _repository.FetchAsync(n => n.NoticeCustomPath == path, cancellationToken);
if (notice == null)
{
    return NotFound();
}
eventBus.Publish(new NoticeViewEvent { NoticeId = notice.NoticeId });
return Ok(notice);

}
Memo#
If we want to achieve event processing based on message queue, we need to pay attention to the duplication of messages. We may need to pay attention to the idempotency of business or the de-processing of a message in event processing.

I used a firewall designed based on the Redis atom increment feature in event processing using Redis, so that a message id can only be processed once in a period of time, and the source code can be realized: https://github.com/WeihanLi/ActivityReservation/blob/dev/ActivityReservation.Helper/Events/NoticeViewEvent.cs

Copy
public class NoticeViewEvent : EventBase
{

public Guid NoticeId { get; set; }

// UserId
// IP
// ...

}

public class NoticeViewEventHandler : IEventHandler
{

public async Task Handle(NoticeViewEvent @event)
{
    var firewallClient = RedisManager.GetFirewallClient($"{nameof(NoticeViewEventHandler)}_{@event.EventId}", TimeSpan.FromMinutes(5));
    if (await firewallClient.HitAsync())
    {
        await DependencyResolver.Current.TryInvokeServiceAsync<ReservationDbContext>(async dbContext =>
        {
            //var notice = await dbContext.Notices.FindAsync(@event.NoticeId);
            //notice.NoticeVisitCount += 1;
            //await dbContext.SaveChangesAsync();

            var conn = dbContext.Database.GetDbConnection();
            await conn.ExecuteAsync($@"UPDATE tabNotice SET NoticeVisitCount = NoticeVisitCount +1 WHERE NoticeId = @NoticeId", new { @event.NoticeId });
        });
    }
}

}
Reference#
https://github.com/WeihanLi/ActivityReservation
https://github.com/WeihanLi/WeihanLi.Redis
https://redis.io/topics/pubsub
Author: Weihan Li

Source: https://www.cnblogs.com/weihanli/p/implement-eventbus-with-redis-pubsub.html

Topics: Database Redis github firewall