dotnet-cqrs/Svrnty.CQRS.Events/Subscriptions/SubscriptionDeliveryHostedService.cs.disabled

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");
}
}
}