using PARR.Core.Common.Interfaces; using PARR.Core.Services.Snapshots.Interfaces; namespace PARR.SnapshotWorker { public class Worker : BackgroundService { private readonly ILogger _logger; private readonly IIntervalService _intervalService; private readonly IServiceProvider _serviceProvider; // Хранилище времени СЛЕДУЮЩЕГО запуска для каждого сервиса private readonly Dictionary _nextRunTimers = new(); // День последней успешной очистки базы данных private int _lastCleanupDay = -1; // Интервал, должен быть меньше минуты, чтобы попадать во все возможные интервалы TimeSpan tickInterval = TimeSpan.FromSeconds(25); public Worker( ILogger logger, IIntervalService intervalService, IServiceProvider serviceProvider ) { _logger = logger; _intervalService = intervalService; _serviceProvider = serviceProvider; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Запуск фонового сервиса создания системных снапшотов"); await _intervalService.IntervalInitAsync(async () => { var now = DateTimeOffset.UtcNow; using (var scope = _serviceProvider.CreateScope()) { // Собираем абсолютно все зарегистрированные сервисы снапшотов var providers = scope.ServiceProvider.GetServices(); #region Параллельный сбор снапшотов var tasksToRun = new List(); foreach (var provider in providers) { var providerKey = provider.GetType().FullName!; // Если сервис видим впервые, планируем его первый старт прямо сейчас if (!_nextRunTimers.ContainsKey(providerKey)) { _nextRunTimers[providerKey] = now; } // Если текущее время добежало до запланированного «будильника» if (now >= _nextRunTimers[providerKey]) { // СРАЗУ планируем следующий старт, чтобы сетка времени не съезжала // из-за времени выполнения самого метода _nextRunTimers[providerKey] = now.Add(provider.Interval); // Добавляем таску в список для параллельного выполнения (без await!) tasksToRun.Add(ExecuteSafelyAsync(provider, () => provider.TakeSnapshotAsync(stoppingToken), "Создание снапшота")); } } // Запускаем все готовые снапшоты ОДНОВРЕМЕННО if (tasksToRun.Any()) { await Task.WhenAll(tasksToRun); } #endregion #region Ежедневная очистка БД // Если наступили новые сутки в формате UTC — запускаем ротацию старых данных if (now.Day != _lastCleanupDay) { _logger.LogInformation("Наступили новые сутки. Запуск процесса очистки устаревших снапшотов..."); foreach (var provider in providers) { await ExecuteSafelyAsync(provider, () => provider.CleanUpOldSnapshotsAsync(stoppingToken), "Очистка старых данных"); } // Запоминаем, что за сегодня очистку уже провели успешно _lastCleanupDay = now.Day; } #endregion } }, tickInterval); } private async Task ExecuteSafelyAsync(ISnapshotProvider provider, Func action, string operationName) { try { await action(); } catch (Exception ex) { _logger.LogError(ex, "Критическая ошибка во время операции '{Operation}' в сервисе {ProviderName}", operationName, provider.GetType().Name); } } } }