books/backend/Books.Api/EventFlow/Infrastructure/ReadStoresResilienceStrategy.cs

96 lines
3.2 KiB
C#
Raw Normal View History

using Books.Api.Infrastructure;
using EventFlow.Aggregates;
using EventFlow.ReadStores;
using Hangfire;
namespace Books.Api.EventFlow.Infrastructure;
public class ReadStoresResilienceStrategy(
ILogger<ReadStoresResilienceStrategy> logger,
IScheduler scheduler) : IDispatchToReadStoresResilienceStrategy
{
public Task BeforeUpdateAsync(
IReadStoreManager readStoreManager,
IReadOnlyCollection<IDomainEvent> domainEvents,
CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task<bool> HandleUpdateFailedAsync(
IReadStoreManager readStoreManager,
IReadOnlyCollection<IDomainEvent> 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<ReadModelRepopulationJob>(
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<IDomainEvent> domainEvents,
CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
public class ReadModelRepopulationJob(ILogger<ReadModelRepopulationJob> 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;
}
}