feat(core, dal): WorkloadCacheService - методы для работы с кэшем отчетности workload
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user