feat(core,dal,domain,snapshotWorker): Воркер по управлению снапшотами. Создание сгапшотов для таблицы RobotConfigurations

This commit is contained in:
Mikhail Trubnikov
2026-07-13 15:29:48 +10:00
parent 9194d127d2
commit 50556ef605
28 changed files with 4953 additions and 26 deletions

View File

@@ -0,0 +1,114 @@
using PARR.Core.Common.Interfaces;
using PARR.Core.Services.Snapshots.Interfaces;
namespace PARR.SnapshotWorker
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IIntervalService _intervalService;
private readonly IServiceProvider _serviceProvider;
// Хранилище времени СЛЕДУЮЩЕГО запуска для каждого сервиса
private readonly Dictionary<string, DateTimeOffset> _nextRunTimers = new();
// День последней успешной очистки базы данных
private int _lastCleanupDay = -1;
// Интервал, должен быть меньше минуты, чтобы попадать во все возможные интервалы
TimeSpan tickInterval = TimeSpan.FromSeconds(25);
public Worker(
ILogger<Worker> 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<ISnapshotProvider>();
#region Параллельный сбор снапшотов
var tasksToRun = new List<Task>();
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<Task> action, string operationName)
{
try
{
await action();
}
catch (Exception ex)
{
_logger.LogError(ex, "Критическая ошибка во время операции '{Operation}' в сервисе {ProviderName}", operationName, provider.GetType().Name);
}
}
}
}