namespace QuestionsApi.Services; public sealed class AnswerStatisticsMaintenanceService : BackgroundService { private readonly AnswerStatisticsStore _statistics; private readonly QuestionAttemptStore _attempts; private readonly ServiceMetrics _metrics; private readonly ILogger _logger; private readonly TimeSpan _interval; private readonly string _owner = $"{Environment.MachineName}-{Guid.NewGuid():N}"; public AnswerStatisticsMaintenanceService( AnswerStatisticsStore statistics, QuestionAttemptStore attempts, ServiceMetrics metrics, IConfiguration configuration, ILogger logger) { _statistics = statistics; _attempts = attempts; _metrics = metrics; _logger = logger; _interval = TimeSpan.FromSeconds(Math.Max(30, configuration.GetValue("AnswerStatistics:ReconcileSeconds", 300))); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { if (await _statistics.TryAcquireBackfillLeaseAsync(_owner, TimeSpan.FromMinutes(30))) { _logger.LogInformation("Backfilling materialized answer statistics"); await _statistics.RebuildAllAsync(); await _statistics.MarkBackfillCompleteAsync(_owner); _metrics.AnswerStatisticsBackfillCompleted(); _metrics.Reconciled(); } else if (await _statistics.IsBackfillCompleteAsync()) { _metrics.AnswerStatisticsBackfillCompleted(); } } catch (Exception exception) { _logger.LogError(exception, "Initial answer-statistics backfill failed"); } using var timer = new PeriodicTimer(_interval); while (await timer.WaitForNextTickAsync(stoppingToken)) { try { if (await _statistics.IsBackfillCompleteAsync()) _metrics.AnswerStatisticsBackfillCompleted(); var activeIds = await _attempts.GetActiveQuestionIdsAsync(DateTime.UtcNow.Subtract(_interval).Subtract(_interval)); await _statistics.RebuildQuestionsAsync(activeIds); _metrics.Reconciled(); } catch (Exception exception) { _logger.LogError(exception, "Answer-statistics reconciliation failed"); } } } }