using Books.Api.Infrastructure; using EventFlow.Aggregates; using EventFlow.ReadStores; using Hangfire; namespace Books.Api.EventFlow.Infrastructure; public class ReadStoresResilienceStrategy( ILogger logger, IScheduler scheduler) : IDispatchToReadStoresResilienceStrategy { public Task BeforeUpdateAsync( IReadStoreManager readStoreManager, IReadOnlyCollection domainEvents, CancellationToken cancellationToken) { return Task.CompletedTask; } public Task HandleUpdateFailedAsync( IReadStoreManager readStoreManager, IReadOnlyCollection domainEvents, Exception exception, CancellationToken cancellationToken) { var firstEvent = domainEvents.FirstOrDefault(); var aggregateId = firstEvent?.GetIdentity()?.Value ?? "unknown"; var readModelType = readStoreManager.ReadModelType.Name; logger.LogError(exception, "[RESILIENCE] Failed to update read model {ReadModelType} for aggregate {AggregateId}. " + "Events: {EventCount}. This may cause data inconsistency.", readModelType, aggregateId, domainEvents.Count); // Schedule a retry job to repopulate the read model if (firstEvent != null) { var aggregateType = firstEvent.AggregateType.Name; scheduler.EnqueueJob( job => job.RepopulateReadModelAsync( aggregateType, aggregateId, readModelType), TimeSpan.FromSeconds(30)); logger.LogWarning( "[RESILIENCE] Scheduled read model repopulation job for {ReadModelType} / {AggregateId}", readModelType, aggregateId); } // Return false to indicate we handled the failure (don't rethrow) return Task.FromResult(false); } public Task UpdateSucceededAsync( IReadStoreManager readStoreManager, IReadOnlyCollection domainEvents, CancellationToken cancellationToken) { return Task.CompletedTask; } } public class ReadModelRepopulationJob(ILogger logger) { [AutomaticRetry(Attempts = 3, DelaysInSeconds = [30, 60, 120])] public Task RepopulateReadModelAsync( string aggregateType, string aggregateId, string readModelType) { logger.LogInformation( "[REPOPULATION] Starting read model repopulation for {ReadModelType} / {AggregateId}", readModelType, aggregateId); // For now, we log the repopulation attempt // A full implementation would: // 1. Load all events for the aggregate from the event store // 2. Clear the existing read model entry // 3. Replay all events to rebuild the read model logger.LogWarning( "[REPOPULATION] Read model repopulation for {ReadModelType} / {AggregateId} " + "requires manual intervention. Check data consistency.", readModelType, aggregateId); return Task.CompletedTask; } }