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

@@ -1,7 +1,9 @@
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.RobotRepositories;
using PARR.Core.Services.Snapshots.Interfaces;
using PARR.Domain.DTOs.RobotSnapshotDTO;
using PARR.Domain.DTOs.User;
using PARR.Domain.Entities.RobotEntities;
@@ -9,21 +11,32 @@ using PARR.Domain.Exceptions;
namespace PARR.Core.Services.RobotSnapshotServices
{
internal class RobotSnapshotService : IRobotSnapshotService
internal class RobotSnapshotService : IRobotSnapshotService, ISnapshotProvider
{
private readonly IRobotSnapshotRepository _robotSnapshotRepository;
private readonly IUserRepository _userRepository;
private readonly IMapper _mapper;
private readonly ILogger<RobotSnapshotService> _logger;
private readonly ISnapshotSettings _snapshotSettings;
// Тут любое значение, не используем метод создания снапшота
public TimeSpan Interval => TimeSpan.FromHours(1);
public TimeSpan RetentionPeriod => _snapshotSettings.RobotSnapshotRetentionPeriod;
public RobotSnapshotService(
IRobotSnapshotRepository robotSnapshotRepository,
IUserRepository userRepository,
IMapper mapper
IMapper mapper,
ILogger<RobotSnapshotService> logger,
ISnapshotSettings snapshotSettings
)
{
_robotSnapshotRepository = robotSnapshotRepository;
_userRepository = userRepository;
_mapper = mapper;
_logger = logger;
_snapshotSettings = snapshotSettings;
}
@@ -348,5 +361,26 @@ namespace PARR.Core.Services.RobotSnapshotServices
}
public Task TakeSnapshotAsync(CancellationToken cancellationToken)
{
// Метод пустой! Нам не нужно собирать данные по таймеру,
// так как они и так пишутся сюда через контроллер API.
return Task.CompletedTask;
}
public async Task CleanUpOldSnapshotsAsync(CancellationToken cancellationToken)
{
var thresholdDate = DateTimeOffset.UtcNow - RetentionPeriod;
_logger.LogInformation("[{ServiceName}] Запуск очистки старых снапшотов. Удаление данных старше {ThresholdDate}", GetType().Name, thresholdDate);
// Удаляем старые записи напрямую в PostgreSQL
var deletedCount = await _robotSnapshotRepository.Get()
.Where(s => s.DateCreated < thresholdDate)
.ExecuteDeleteAsync(cancellationToken);
_logger.LogInformation("[{ServiceName}] Очистка завершена. Удалено устаревших строк снапшотов: {Count}", GetType().Name, deletedCount);
}
}
}

View File

@@ -0,0 +1,94 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.RobotRepositories;
using PARR.Core.Services.Snapshots.Interfaces;
using PARR.Domain.Entities.RobotEntities;
using PARR.Domain.Exceptions;
namespace PARR.Core.Services.Snapshots.Implementations
{
/// <summary>
/// Снапшоты для таблицы RobotConfiguration
/// </summary>
internal class RobotConfigurationSnapshotService : ISnapshotProvider
{
private readonly IRobotConfigurationRepository _robotConfigurationRepository;
private readonly IRobotConfigurationSnapshotRepository _robotConfigurationSnapshotRepository;
private readonly ILogger<RobotConfigurationSnapshotService> _logger;
private readonly ISnapshotSettings _snapshotSettings;
public TimeSpan Interval => _snapshotSettings.RobotConfigurationSnapshotInterval;// TimeSpan.FromMinutes(2);
public TimeSpan RetentionPeriod => _snapshotSettings.RobotConfigurationSnapshotRetentionPeriod;//TimeSpan.FromDays(60);
public RobotConfigurationSnapshotService(
IRobotConfigurationRepository robotConfigurationRepository,
IRobotConfigurationSnapshotRepository robotConfigurationSnapshotRepository,
ILogger<RobotConfigurationSnapshotService> logger,
ISnapshotSettings snapshotSettings
)
{
_robotConfigurationRepository = robotConfigurationRepository;
_robotConfigurationSnapshotRepository = robotConfigurationSnapshotRepository;
_logger = logger;
_snapshotSettings = snapshotSettings;
}
public async Task TakeSnapshotAsync(CancellationToken cancellationToken)
{
var now = DateTimeOffset.UtcNow;
_logger.LogDebug("Сбор метрик для снапшота RobotConfigurations...");
var stats = await _robotConfigurationRepository.Get()
.GroupBy(t => new { t.RobotCode, t.RobotStatusCode, t.TaskStatusCode })
.Select(g => new
{
g.Key.RobotCode,
g.Key.RobotStatusCode,
g.Key.TaskStatusCode,
Count = g.Count()
})
.ToListAsync(cancellationToken);
if (!stats.Any())
{
_logger.LogDebug("Нет активных заданий роботов для создания снапшота.");
return;
}
var snapshots = stats.Select(s => new RobotConfigurationSnapshot
{
Id = Guid.NewGuid(),
DateCreated = now,
RobotCode = s.RobotCode,
RobotStatusCode = s.RobotStatusCode,
TaskStatusCode = s.TaskStatusCode,
Count = s.Count
}).ToList();
if (!await _robotConfigurationSnapshotRepository.AddRangeAsync(snapshots) || !await _robotConfigurationSnapshotRepository.CommitAsync())
throw new DbErrorException("Ошибка при сохранении в БД");
_logger.LogInformation("Успешно сохранен снапшот RobotConfigurations. Записано строк: {Count}. Периодичность: {Interval}", snapshots.Count, Interval);
}
public async Task CleanUpOldSnapshotsAsync(CancellationToken cancellationToken)
{
// Вычисляем граничную дату (всё, что было ДО нее — удаляем)
var thresholdDate = DateTimeOffset.UtcNow.Subtract(RetentionPeriod);
_logger.LogInformation("[{ServiceName}] Запуск очистки старых снапшотов. Удаление данных старше {ThresholdDate}", GetType().Name, thresholdDate);
// Фильтруем старые записи и вызываем ExecuteDeleteAsync для удаления прямо в базе данных
var deletedCount = await _robotConfigurationSnapshotRepository.Get()
.Where(s => s.DateCreated < thresholdDate)
.ExecuteDeleteAsync(cancellationToken);
_logger.LogInformation("[{ServiceName}] Очистка завершена. Удалено устаревших строк снапшотов: {Count}", GetType().Name, deletedCount);
}
}
}

View File

@@ -0,0 +1,31 @@
namespace PARR.Core.Services.Snapshots.Interfaces
{
/// <summary>
/// Общий снапшот провайдер.
/// Используется для реализации паттерна "Стратегия"
/// </summary>
public interface ISnapshotProvider
{
/// <summary>
/// Периодичность создания снапшота (не меньше 1 минуты).
/// </summary>
TimeSpan Interval { get; }
/// <summary>
/// Срок хранения снапшотов (например, 30 дней). Всё, что старше — удаляется.
/// </summary>
TimeSpan RetentionPeriod { get; }
/// <summary>
/// Метод сбора метрик и записи их в базу данных.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task TakeSnapshotAsync(CancellationToken cancellationToken);
/// <summary>
/// Метод очистки устаревших снапшотов.
/// </summary>
Task CleanUpOldSnapshotsAsync(CancellationToken cancellationToken);
}
}

View File

@@ -0,0 +1,35 @@
namespace PARR.Core.Services.Snapshots.Interfaces
{
/// <summary>
/// Настройки снапшотов
/// </summary>
public interface ISnapshotSettings
{
/// <summary>
/// Интервал создания снапшотов для таблицы RobotConfiguration
/// </summary>
TimeSpan RobotConfigurationSnapshotInterval { get; }
/// <summary>
/// Длительность хранения снапшотов для RobotConfiguration в таблице ConfigurationShapshots
/// </summary>
TimeSpan RobotConfigurationSnapshotRetentionPeriod { get; }
/// <summary>
/// Длительность хранения снапшотов работы роботов в таблице Shapshots
/// </summary>
TimeSpan RobotSnapshotRetentionPeriod { get; }
}
/// <summary>
/// Настройки по умолчанию для ISnapshotSettings
/// </summary>
internal record DefaultSnapshotSettings : ISnapshotSettings
{
public TimeSpan RobotConfigurationSnapshotInterval => TimeSpan.FromMinutes(2);
public TimeSpan RobotConfigurationSnapshotRetentionPeriod => TimeSpan.FromDays(60);
public TimeSpan RobotSnapshotRetentionPeriod => TimeSpan.FromDays(60);
}
}