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; /// /// Background service that monitors event streams and delivers events to persistent subscriptions. /// public sealed class SubscriptionDeliveryHostedService : BackgroundService { private readonly IPersistentSubscriptionStore _subscriptionStore; private readonly IEventStreamStore _eventStore; private readonly IPersistentSubscriptionDeliveryService _deliveryService; private readonly ISubscriptionManager _subscriptionManager; private readonly ILogger _logger; private readonly TimeSpan _pollInterval = TimeSpan.FromMilliseconds(500); public SubscriptionDeliveryHostedService( IPersistentSubscriptionStore subscriptionStore, IEventStreamStore eventStore, IPersistentSubscriptionDeliveryService deliveryService, ISubscriptionManager subscriptionManager, ILogger 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"); } } }