188 lines
7.1 KiB
Plaintext
188 lines
7.1 KiB
Plaintext
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Svrnty.CQRS.Events.Abstractions;
|
|
using Svrnty.CQRS.Events.Abstractions.Subscriptions;
|
|
|
|
namespace Svrnty.CQRS.Events.Subscriptions;
|
|
|
|
/// <summary>
|
|
/// Background service that monitors event streams and delivers events to persistent subscriptions.
|
|
/// </summary>
|
|
public sealed class SubscriptionDeliveryHostedService : BackgroundService
|
|
{
|
|
private readonly IPersistentSubscriptionStore _subscriptionStore;
|
|
private readonly IEventStreamStore _eventStore;
|
|
private readonly IPersistentSubscriptionDeliveryService _deliveryService;
|
|
private readonly ISubscriptionManager _subscriptionManager;
|
|
private readonly ILogger<SubscriptionDeliveryHostedService> _logger;
|
|
private readonly TimeSpan _pollInterval = TimeSpan.FromMilliseconds(500);
|
|
|
|
public SubscriptionDeliveryHostedService(
|
|
IPersistentSubscriptionStore subscriptionStore,
|
|
IEventStreamStore eventStore,
|
|
IPersistentSubscriptionDeliveryService deliveryService,
|
|
ISubscriptionManager subscriptionManager,
|
|
ILogger<SubscriptionDeliveryHostedService> logger)
|
|
{
|
|
_subscriptionStore = subscriptionStore ?? throw new ArgumentNullException(nameof(subscriptionStore));
|
|
_eventStore = eventStore ?? throw new ArgumentNullException(nameof(eventStore));
|
|
_deliveryService = deliveryService ?? throw new ArgumentNullException(nameof(deliveryService));
|
|
_subscriptionManager = subscriptionManager ?? throw new ArgumentNullException(nameof(subscriptionManager));
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
_logger.LogInformation("Subscription delivery service started");
|
|
|
|
try
|
|
{
|
|
while (!stoppingToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
await ProcessSubscriptionDeliveriesAsync(stoppingToken);
|
|
await CleanupExpiredSubscriptionsAsync(stoppingToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error processing subscription deliveries");
|
|
}
|
|
|
|
await Task.Delay(_pollInterval, stoppingToken);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
_logger.LogInformation("Subscription delivery service stopping");
|
|
}
|
|
finally
|
|
{
|
|
_logger.LogInformation("Subscription delivery service stopped");
|
|
}
|
|
}
|
|
|
|
private async Task ProcessSubscriptionDeliveriesAsync(CancellationToken cancellationToken)
|
|
{
|
|
// Get all active subscriptions
|
|
var activeSubscriptions = await _subscriptionStore.GetByStatusAsync(
|
|
SubscriptionStatus.Active,
|
|
cancellationToken);
|
|
|
|
if (activeSubscriptions.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Group subscriptions by correlation ID for efficient processing
|
|
var subscriptionsByCorrelation = activeSubscriptions
|
|
.GroupBy(s => s.CorrelationId)
|
|
.ToList();
|
|
|
|
foreach (var group in subscriptionsByCorrelation)
|
|
{
|
|
var correlationId = group.Key;
|
|
|
|
try
|
|
{
|
|
// Find the minimum last delivered sequence across all subscriptions for this correlation
|
|
var minSequence = group.Min(s => s.LastDeliveredSequence);
|
|
|
|
// Read new events from the stream (using correlation ID as stream name)
|
|
var newEvents = await _eventStore.ReadStreamAsync(
|
|
streamName: correlationId,
|
|
fromOffset: minSequence + 1,
|
|
maxCount: 50,
|
|
cancellationToken: cancellationToken);
|
|
|
|
if (newEvents.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
_logger.LogDebug(
|
|
"Processing {Count} new events for correlation {CorrelationId}",
|
|
newEvents.Count,
|
|
correlationId);
|
|
|
|
// Deliver each event to matching subscriptions
|
|
foreach (var eventData in newEvents)
|
|
{
|
|
foreach (var subscription in group)
|
|
{
|
|
// Skip if event already delivered
|
|
if (eventData.Sequence <= subscription.LastDeliveredSequence)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Check if this event type should be delivered
|
|
if (!subscription.ShouldDeliverEventType(eventData.EventType))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Check delivery mode
|
|
if (subscription.DeliveryMode == DeliveryMode.OnReconnect)
|
|
{
|
|
// Don't deliver now, wait for client to catch up
|
|
continue;
|
|
}
|
|
|
|
if (subscription.DeliveryMode == DeliveryMode.Batched)
|
|
{
|
|
// TODO: Implement batched delivery
|
|
// For now, treat as immediate
|
|
}
|
|
|
|
// Mark as delivered
|
|
subscription.MarkDelivered(eventData.Sequence);
|
|
await _subscriptionStore.UpdateAsync(subscription, cancellationToken);
|
|
|
|
_logger.LogDebug(
|
|
"Delivered event {EventType} (seq {Sequence}) to subscription {SubscriptionId}",
|
|
eventData.EventType,
|
|
eventData.Sequence,
|
|
subscription.Id);
|
|
|
|
// Check if this is a terminal event
|
|
if (subscription.IsTerminalEvent(eventData.EventType))
|
|
{
|
|
subscription.Complete();
|
|
await _subscriptionStore.UpdateAsync(subscription, cancellationToken);
|
|
|
|
_logger.LogInformation(
|
|
"Terminal event {EventType} received, subscription {SubscriptionId} completed",
|
|
eventData.EventType,
|
|
subscription.Id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex,
|
|
"Error processing events for correlation {CorrelationId}",
|
|
correlationId);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task CleanupExpiredSubscriptionsAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await _subscriptionManager.CleanupExpiredSubscriptionsAsync(cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error cleaning up expired subscriptions");
|
|
}
|
|
}
|
|
}
|