using System.Threading.Channels;
using Elsa.Mediator.Contracts;
using Elsa.Mediator.Middleware.Notification;
using Elsa.Mediator.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Elsa.Mediator.HostedServices;
///
/// Continuously reads from a channel to which notifications can be sent, publishing each received notification.
///
public class BackgroundEventPublisherHostedService : BackgroundService
{
private readonly int _workerCount;
private readonly INotificationsChannel _notificationsChannel;
private readonly IServiceScopeFactory _scopeFactory;
private readonly List> _outputs;
private readonly ILogger _logger;
///
public BackgroundEventPublisherHostedService(IOptions options, INotificationsChannel notificationsChannel, IServiceScopeFactory scopeFactory, ILogger logger)
{
_workerCount = options.Value.NotificationWorkerCount;
_notificationsChannel = notificationsChannel;
_scopeFactory = scopeFactory;
_logger = logger;
_outputs = new(_workerCount);
}
///
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
// Index to round-robin distribute notifications across worker channels
var index = 0;
using var scope = _scopeFactory.CreateScope();
var notificationSender = scope.ServiceProvider.GetRequiredService();
// Create multiple output channels and start worker tasks for parallel processing
for (var i = 0; i < _workerCount; i++)
{
var output = Channel.CreateUnbounded();
_outputs.Add(output);
// Start a background task to process notifications from this output channel
_ = ReadOutputAsync(output, notificationSender, cancellationToken);
}
var channelReader = _notificationsChannel.Reader;
// Continuously read notifications from the input channel and distribute them to worker channels
// using round-robin distribution for load balancing
await foreach (var notification in channelReader.ReadAllAsync(cancellationToken))
{
var output = _outputs[index];
await output.Writer.WriteAsync(notification, cancellationToken);
// Move to the next worker in a circular fashion
index = (index + 1) % _workerCount;
}
// When the input channel is completed, complete all output channels
foreach (var output in _outputs)
{
output.Writer.Complete();
}
}
///
/// Processes notifications from an output channel asynchronously.
///
/// The channel to read notifications from
/// The service used to send notifications
/// Cancellation token from the hosted service
private async Task ReadOutputAsync(Channel output, INotificationSender notificationSender, CancellationToken cancellationToken)
{
await foreach (var notificationContext in output.Reader.ReadAllAsync(cancellationToken))
{
try
{
var notification = notificationContext.Notification;
// Link the cancellation tokens so that cancellation can happen from either source
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, notificationContext.CancellationToken);
await notificationSender.SendAsync(notification, NotificationStrategy.Sequential, linkedTokenSource.Token);
}
catch (OperationCanceledException e)
{
_logger.LogDebug(e, "An operation was cancelled while processing the queue");
}
catch (Exception e)
{
_logger.LogError(e, "An unhandled exception occured while processing the queue");
}
}
}
}