feat(core, dal): WorkloadCacheService - методы для работы с кэшем отчетности workload

This commit is contained in:
Mikhail Trubnikov
2026-05-05 14:52:17 +10:00
parent a89605d02a
commit 7d42a80556
10 changed files with 380 additions and 27 deletions

View File

@@ -31,7 +31,7 @@ namespace PARR.Core.Services.Workload.Implementations
}
public async System.Threading.Tasks.Task ProcessMessagesAsync(IMqSettings mqSettings)
public async Task ProcessMessagesAsync(IMqSettings mqSettings)
{
// этот метод вызывается в воркере
@@ -68,7 +68,7 @@ namespace PARR.Core.Services.Workload.Implementations
throw new Exception("Ошибка при подключении к RabbitMq");
}
public async System.Threading.Tasks.Task StopProcessingAsync()
public async Task StopProcessingAsync()
{
await rabbitService.DisposeAsync();
}

View File

@@ -1,33 +1,82 @@
using InfluxDB.Client.Api.Service;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Common.Interfaces;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Services.Shortcodes;
using PARR.Core.Services.Workload.Models;
using PARR.Domain.Cache.Models;
using PARR.Domain.Entities;
namespace PARR.Core.Services.Workload.Implementations
{
/// <summary>
/// Класс управления КЭШем для workload
/// </summary>
internal class WorkloadCacheService
public class WorkloadCacheService
{
private readonly ILogger<WorkloadCacheService> logger;
private readonly IRedisCacheService redisCacheService;
private readonly ITemplateRepository templateRepository;
private readonly IShortcodesService shortcodeService;
public WorkloadCacheService(
ILogger<WorkloadCacheService> logger,
IRedisCacheService redisCacheService,
ITemplateRepository templateRepository
//IShortcodeService
ITemplateRepository templateRepository,
IShortcodesService shortcodeService
)
{
this.logger = logger;
this.redisCacheService = redisCacheService;
this.templateRepository = templateRepository;
this.shortcodeService = shortcodeService;
}
//----------- Настройки, вынести -----------
/// <summary>
/// Ключ КЭШ шаблонов
/// </summary>
private readonly string[] cacheTemplateKey = { "template", "report", "params" };
/// <summary>
/// TTL кэша шаблонов
/// </summary>
private readonly TimeSpan cacheTemplateTtl = TimeSpan.FromHours(12);
/// <summary>
/// Допустимая разница записей в БД и в кэш
/// </summary>
private readonly int maxCacheDiff = 1000;
/// <summary>
/// Ключ КЭШ, отчет о загруженности, все ЗО
/// </summary>
private readonly string[] cacheWorkloadResponseAreaAllKey = { "workload", "response area", "all" };
/// <summary>
/// Ключ КЭШ, отчет о загруженности, одна ЗО
/// Формировать ключ, основа + Hash ЗО
/// </summary>
private readonly string[] cacheWorkloadResponseAreaItemKey = { "workload", "response area", "item" };
/// <summary>
/// Ключ КЭШ, отчет о загруженности, все РГ
/// </summary>
private readonly string[] cacheWorkloadWorkGroupAllKey = { "workload", "work group", "all" };
/// <summary>
/// Ключ КЭШ, отчет о загруженности, одна РГ
/// Формировать ключ, основа + Hash РГ
/// </summary>
private readonly string[] cacheWorkloadWorkGroupItemKey = { "workload", "work group", "item" };
/// <summary>
/// TTL кэша отчетов workload
/// </summary>
private readonly TimeSpan cacheWorkloadTtl = TimeSpan.FromMinutes(15);
//=========== Настройки, вынести ===========
/// <summary>
/// Создать КЭШ шаблонов.
/// КЭШ рабочих групп и зон ответственности
@@ -35,20 +84,51 @@ namespace PARR.Core.Services.Workload.Implementations
/// <returns></returns>
public async Task CreateTemplateCacheAsync()
{
await DeleteTemplateCacheAsync();
await DeleteWorkloadCacheAsync();
var dtStart = DateTimeOffset.UtcNow;
logger.LogInformation("Создание КЭШа шаблонов.");
//1. Получить из БД все шаблоны (тяжелый запрос, тянет вообще все данные, 8 сек)
var templates = await templateRepository.Get()
.AsNoTracking()
.Include(t=>t.Job)
// Загружаем дополнительные данные, чтоб быстрее считался шорткод
.Include(t => t.Job).ThenInclude(t => t.Group).ThenInclude(t => t.GroupType)
.ToListAsync();
//2. Получить по каждому шаблону ЗО и РГ
//3. Сохранить в Redis
logger.LogInformation("Получил из БД шаблонов {TemplateCount} шт.", templates.Count);
//redisCacheService
//2. Получить по каждому шаблону ЗО и РГ
var cacheData = new List<TemplateCache>();
int templateCount = 0;
foreach (var template in templates)
{
var (workGroup, responseArea) = await ApplyShortcodesAsync(template);
logger.LogDebug("[{TemplateCount}] Для шаблона {TemplateId} {TemplateName} получена ЗО '{ResponseArea}', РГ '{WorkGroup}'", templateCount, template.Id, template.Name, responseArea, workGroup);
var cacheItem = new TemplateCache
{
Data = new TemplateCacheDto { TemplateId = template.Id, ResponseArea = responseArea, WorkGroup = workGroup },
Source = nameof(WorkloadCacheService),
Timestamp = DateTimeOffset.UtcNow
};
cacheData.Add(cacheItem);
templateCount++;
}
//3. Сохранить в Redis
logger.LogDebug("Для сохранения в Redis подготовлен массив из {Count} строк", cacheData.Count);
var hashKey = redisCacheService.GetKey(cacheTemplateKey);
foreach (var item in cacheData)
{
await redisCacheService.SetHashFieldAsync(hashKey, item.Data.TemplateId.ToString(), item, cacheTemplateTtl);
}
var duration = DateTimeOffset.UtcNow - dtStart;
logger.LogInformation("Завершено формирование кэша. Key: {HashKey}, ttl: {Ttl}, кол-во записей: {CacheCount}. Продолжительность формирования КЭШ {Duration}", hashKey, cacheTemplateTtl, cacheData.Count, duration);
}
/// <summary>
@@ -57,7 +137,10 @@ namespace PARR.Core.Services.Workload.Implementations
/// <returns></returns>
public async Task DeleteTemplateCacheAsync()
{
var hashKey = redisCacheService.GetKey(cacheTemplateKey);
logger.LogDebug("Удаляю кэш шаблонов, {HashKey}", redisCacheService.GetKey(cacheTemplateKey));
await redisCacheService.DeleteHashAsync(hashKey);
}
/// <summary>
@@ -66,9 +149,134 @@ namespace PARR.Core.Services.Workload.Implementations
/// <returns></returns>
public async Task DeleteWorkloadCacheAsync()
{
logger.LogDebug("Удаляю кэш отчетов");
await redisCacheService.DeleteCachedDataAsync(redisCacheService.GetKey(cacheWorkloadResponseAreaAllKey));
await redisCacheService.DeleteKeysByPatternAsync(redisCacheService.GetKey(cacheWorkloadResponseAreaItemKey) + "*");
await redisCacheService.DeleteCachedDataAsync(redisCacheService.GetKey(cacheWorkloadWorkGroupAllKey));
await redisCacheService.DeleteKeysByPatternAsync(redisCacheService.GetKey(cacheWorkloadWorkGroupItemKey) + "*");
}
/// <summary>
/// Получить кэш РГ и ЗО всех шаблонов, получить недостающие поля из БД.
/// Сравнит кол-во записей в БД и в КЭШ, если не хватает до 1000 шаблонов, доформирует кэш,
/// если больше 1000, вернет пустой список, так как считается кэш неактуальным.
/// </summary>
/// <returns></returns>
public async Task<Dictionary<Guid, TemplateReportData>> GetTemplateReportDataAsync()
{
logger.LogDebug("Получаю данные для формирования отчета.");
// Получим кэш целиком
var cacheData = await redisCacheService.GetAllHashFieldsAsync<TemplateCache>(redisCacheService.GetKey(cacheTemplateKey));
if (cacheData.Count == 0)
{
logger.LogDebug("Записей в кэш 0. Конец.");
return new Dictionary<Guid, TemplateReportData>();
}
// Сравним кол-во записей в БД и в КЭШ, вдруг там разница больше допустимой
var templatesCount = await templateRepository.Get().Select(t => t.Id).CountAsync();
if (templatesCount - cacheData.Count > maxCacheDiff)
{
logger.LogInformation("Разница в количестве шаблонов в БД ({TemplateCount}) и в КЭШ ({CacheCount}) больше допустимой {MaxCacheDiff}. Считаем КЭШ неактуальным. Требуется повторное формирование КЭШа.",
templatesCount, cacheData.Count, maxCacheDiff);
return new Dictionary<Guid, TemplateReportData>();
}
// Получаем из БД только нужные данные для отчета
var templates = await templateRepository.Get()
.AsNoTracking()
.Select(t => new { Id = t.Id, IsActiveTemplate = t.IsActiveTemplate, IsActiveSchedule = t.IsActiveSchedule, Name = t.Name, NextRun = t.NextRun })
.ToListAsync();
var reportData = new Dictionary<Guid, TemplateReportData>();
foreach (var template in templates)
{
cacheData.TryGetValue(template.Id.ToString(), out var cache);
var (workGroup, responseArea) = ("", "");
var cacheDate = DateTimeOffset.UtcNow;
if (cache != null)
{
workGroup = cache.Data.WorkGroup;
responseArea = cache.Data.ResponseArea;
cacheDate = cache.Timestamp;
}
else
{
(workGroup, responseArea) = await GetTemplateProps(template.Id);
}
var data = new TemplateReportData(
template.Id,
responseArea,
workGroup,
template.Name,
template.NextRun,
template.IsActiveTemplate,
template.IsActiveSchedule,
DateTimeOffset.UtcNow);
reportData.Add(template.Id, data);
}
logger.LogDebug("Сформировано итоговых данных для отчета, строк: {Count}", reportData.Count);
return reportData;
}
/// <summary>
/// Получить РГ и ЗО из шорткода. Сохранить в КЭШ.
/// </summary>
/// <param name="templateId"></param>
/// <returns></returns>
private async Task<(string WorkGroup, string ResponseArea)> GetTemplateProps(Guid templateId)
{
logger.LogDebug("Дозагружаю недостающие данные из БД для шаблона {TemplateId}", templateId);
var template = await templateRepository.Get()
.AsNoTracking()
// Загружаем дополнительные данные, чтоб быстрее считался шорткод
.Include(t => t.Job).ThenInclude(t => t.Group).ThenInclude(t => t.GroupType)
.FirstOrDefaultAsync(t => t.Id == templateId);
if (template == null)
{
logger.LogWarning("Шаблон {TemplateId} не найден в БД", templateId);
return ($"РГ {templateId}", $"ЗО {templateId}");
}
var (workGroup, responseArea) = await ApplyShortcodesAsync(template);
// Запишем результат в кэш
var cacheItem = new TemplateCache
{
Data = new TemplateCacheDto { TemplateId = template.Id, ResponseArea = responseArea, WorkGroup = workGroup },
Source = nameof(WorkloadCacheService),
Timestamp = DateTimeOffset.UtcNow
};
await redisCacheService.SetHashFieldAsync(redisCacheService.GetKey(cacheTemplateKey), templateId.ToString(), cacheItem);
logger.LogDebug("Добавлена недостающая запись в кэш. {TemplateId}, '{WorkGroup}', '{ResponseArea}'", templateId, workGroup, responseArea);
return (workGroup, responseArea);
}
/// <summary>
/// Получить РГ и ЗО из шорткодов
/// </summary>
/// <param name="template"></param>
/// <returns></returns>
private async Task<(string WorkGroup, string ResponseArea)> ApplyShortcodesAsync(Template template)
{
var workGroup = await shortcodeService.ApplyShortcodesAsync(template.Job!.WorkGroupMask, template);
var responseArea = await shortcodeService.ApplyShortcodesAsync(template.Job!.ResponseAreaMask, template);
return (workGroup, responseArea);
}
}

View File

@@ -0,0 +1,24 @@
namespace PARR.Core.Services.Workload.Models
{
/// <summary>
/// Модель данных для формирования отчета по шаблонам.
/// Склеенные данные полученные из кэш и БД
/// </summary>
/// <param name="TemplateId"></param>
/// <param name="ResponseArea"></param>
/// <param name="WorkGroup"></param>
/// <param name="Name"></param>
/// <param name="NextRun"></param>
/// <param name="IsActiveTemplate"></param>
/// <param name="IsActiveSchedule"></param>
public record TemplateReportData(
Guid TemplateId,
string ResponseArea,
string WorkGroup,
string Name,
DateTimeOffset NextRun,
bool IsActiveTemplate,
bool IsActiveSchedule,
DateTimeOffset CacheDate
);
}