96 lines
3.2 KiB
C#
96 lines
3.2 KiB
C#
|
|
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;
|
||
|
|
}
|
||
|
|
}
|