Server-Sent Events (or WebSockets) broadcasting in load balancing scenario with Redis

Few weeks ago I've received a question under my Server-Sent Events middleware for ASP.NET Core repository. I was quite busy at the time so I've only provided a short answer, but I've also promised to myself to describe the problem and solution in details as soon as possible. This post is me fulfilling that promise.

The problem

The question was about using Server-Sent Events in load balancing scenario. Under the hood Sever-Sent Events is using long-lived HTTP connection for delivering the messages. This means that client is connected to specific instance of the application behind the load balancer. It can look like on the diagram below where Client A is connected to Instance 1 and Client B to Instance 2.

Server-Sent Events with Load Balancing Diagram

The problem arises when a message resulting from an operation performed on Instance 1 needs to be broadcasted to all clients (so also Client B).

The solution

In order to solve the problem a communication channel is required, which instances can use to redistribute the messages. One way for achieving such communication channel is publish-subscribe pattern. Typical topology of publish-subscribe pattern implementation introduces a message broker.

Server-Sent Events with Load Balancing and Publish-Subscribe Pattern Diagram

Instead of sending the message directly to clients the application sends it to the broker. Then the broker sends the message to all subscribers (which may include the original sender) and they send it to the clients.

One example of such broker can be Redis with its Pub/Sub functionality.

The implementation

The starting point for the implementation will be demo project from my original post about Server-Sent Events. It has a notification functionality which allows client for sending messages to other clients.

public class NotificationsController : Controller
{
    private INotificationsServerSentEventsService _serverSentEventsService;

    public NotificationsController(INotificationsServerSentEventsService serverSentEventsService)
    {
        _serverSentEventsService = serverSentEventsService;
    }

    ...

    [ActionName("sse-notifications-sender")]
    [AcceptVerbs("POST")]
    public async Task<IActionResult> Sender(NotificationsSenderViewModel viewModel)
    {
        if (!String.IsNullOrEmpty(viewModel.Notification))
        {
            await _serverSentEventsService.SendEventAsync(new ServerSentEvent
            {
                Type = viewModel.Alert ? "alert" : null,
                Data = new List<string>(viewModel.Notification.Split(new string[] { "\r\n", "\n" },
                    StringSplitOptions.None))
            });
        }

        ModelState.Clear();

        return View("Sender", new NotificationsSenderViewModel());
    }
}

The controller is directly interacting with the Server-Sent Events middleware. This is the part which should be abstracted to allow using Redis when desired. A simple service for sending messages can be extracted.

public interface INotificationsService
{
    Task SendNotificationAsync(string notification, bool alert);
}

internal class LocalNotificationsService : INotificationsService
{
    private INotificationsServerSentEventsService _notificationsServerSentEventsService;

    public LocalNotificationsService(INotificationsServerSentEventsService notificationsServerSentEventsService)
    {
        _notificationsServerSentEventsService = notificationsServerSentEventsService;
    }

    public Task SendNotificationAsync(string notification, bool alert)
    {
        return _notificationsServerSentEventsService.SendEventAsync(new ServerSentEvent
        {
            Type = alert ? "alert" : null,
            Data = new List<string>(notification.Split(new string[] { "\r\n", "\n" },
                StringSplitOptions.None))
        });
    }
}

With the service in place the controller can be refactored.

public class NotificationsController : Controller
{
    private INotificationsService _notificationsService;

    public NotificationsController(INotificationsService notificationsService)
    {
        _notificationsService = notificationsService;
    }

    ...

    [ActionName("sse-notifications-sender")]
    [AcceptVerbs("POST")]
    public async Task<IActionResult> Sender(NotificationsSenderViewModel viewModel)
    {
        if (!String.IsNullOrEmpty(viewModel.Notification))
        {
            await _notificationsService.SendNotificationAsync(viewModel.Notification, viewModel.Alert);
        }

        ModelState.Clear();

        return View("Sender", new NotificationsSenderViewModel());
    }
}

Now the Redis based implementation of INotificationsService can be created. I've decided to use StackExchange.Redis which is a very popular Redis client for .NET (it's also being used by ASP.NET Core) with good documentation. The implementation is straightforward, the only challenge is distinguishing regular notifications from alerts. In context of publish-subscribe pattern one of approaches can be filtering based on topics. With this approach the application should use different channels for different types of messages.

internal class RedisNotificationsService : INotificationsService
{
    private const string NOTIFICATIONS_CHANNEL = "NOTIFICATIONS";
    private const string ALERTS_CHANNEL = "ALERTS";

    private ConnectionMultiplexer _redis;
    private INotificationsServerSentEventsService _notificationsServerSentEventsService;

    public RedisNotificationsService(INotificationsServerSentEventsService notificationsServerSentEventsService)
    {
        _redis = ConnectionMultiplexer.Connect("localhost");
        _notificationsServerSentEventsService = notificationsServerSentEventsService;

        ISubscriber subscriber = _redis.GetSubscriber();

        subscriber.Subscribe(NOTIFICATIONS_CHANNEL, async (channel, message) =>
        {
            await SendSseEventAsync(message, false);
        });

        subscriber.Subscribe(ALERTS_CHANNEL, async (channel, message) =>
        {
            await SendSseEventAsync(message, true);
        });
    }

    public Task SendNotificationAsync(string notification, bool alert)
    {
        ISubscriber subscriber = _redis.GetSubscriber();

        return subscriber.PublishAsync(alert ? ALERTS_CHANNEL : NOTIFICATIONS_CHANNEL, notification);
    }

    private Task SendSseEventAsync(string notification, bool alert)
    {
        return _notificationsServerSentEventsService.SendEventAsync(new ServerSentEvent
        {
            Type = alert ? "alert" : null,
            Data = new List<string>(notification.Split(new string[] { "\r\n", "\n" },
                StringSplitOptions.None))
        });
    }
}

The implementation can be further improved by extracting a base class with Server-Sent Events related functionality, but the service is ready to be used (it just needs to be registered).

The demo available on GitHub also provides configuration options for Redis connection and which INotificationsService implementation to use.

This approach can be used in exactly same way if the WebSockets are used instead of Server-Sent Events, or in any other scenario which requires similar communication pattern.