Files
parr_api/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateSynchronizer.cs

273 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Diagnostics;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
using PARR.Core.Services.MatchingStatusService;
using PARR.Core.Services.UnitFilterService;
using PARR.Domain.Cache.Models;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{
private readonly ILogger<GroupedTemplateSynchronizer> logger;
private readonly IJobGroupRepository jobGroupService;
private readonly IUnitFilterService unitFilterService;
private readonly IGroupedTemplateUnitFilter groupedTemplateUnitFilter;
private readonly IUnitInTemplateConflictMapper unitInTemplateConflictMapper;
private readonly IGroupedTemplateBuilder groupedTemplateBuilder;
private readonly IGroupedTemplateProcessor groupedTemplateProcessor;
private readonly ITemplateRepository templateService;
private readonly ITemplateDeactivator templateDeactivator;
private readonly IMatchingStatusService matchingStatusService;
public GroupedTemplateSynchronizer(
ILogger<GroupedTemplateSynchronizer> logger,
IJobGroupRepository jobGroupService,
IUnitFilterService unitFilterService,
IGroupedTemplateUnitFilter groupedTemplateUnitFilter,
IUnitInTemplateConflictMapper unitInTemplateConflictMapper,
IGroupedTemplateBuilder groupedTemplateBuilder,
IGroupedTemplateProcessor groupedTemplateProcessor,
ITemplateRepository templateService,
ITemplateDeactivator templateDeactivator,
IMatchingStatusService matchingStatusService)
{
this.logger = logger;
this.jobGroupService = jobGroupService;
this.unitFilterService = unitFilterService;
this.groupedTemplateUnitFilter = groupedTemplateUnitFilter;
this.unitInTemplateConflictMapper = unitInTemplateConflictMapper;
this.groupedTemplateBuilder = groupedTemplateBuilder;
this.groupedTemplateProcessor = groupedTemplateProcessor;
this.templateService = templateService;
this.templateDeactivator = templateDeactivator;
this.matchingStatusService = matchingStatusService;
}
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
}
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
{
var totalSw = Stopwatch.StartNew();
logger.LogInformation("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
// === Проверка: уже запущена? ===
var existingStatus = await matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
if (existingStatus.DetailsJobGroups?.Any() == true)
{
logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId);
return;
}
// === Устанавливаем статус "в процессе" ===
var initialStatus = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = "Начало синхронизации"
};
await matchingStatusService.SetMatchingStatusAsync(
jobGroupId,
SyncTaskEntityTypeEnum.JobGroup,
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
TimeSpan.FromMinutes(35)
);
try
{
// === ЭТАП 1: Загрузка JobGroup ===
var stageSw = Stopwatch.StartNew();
var jobGroup = await jobGroupService.Get()
.AsNoTracking()
.AsSingleQuery()
.Include(jg => jg.GroupType)
.Include(jg => jg.Jobs)
.ThenInclude(j => j.AutoControl)
.Include(jg => jg.Jobs)
.ThenInclude(j => j.UnitFilters)
.ThenInclude(uf => uf.RelationshipFilters)
.ThenInclude(rf => rf.UnitField)
.Include(jg => jg.Jobs)
.ThenInclude(jg => jg.Tnk)
.FirstOrDefaultAsync(jg => jg.Id == jobGroupId);
if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any())
{
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "JobGroup не найден или пуст");
return;
}
var jobsInGroup = jobGroup.Jobs.ToList();
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Загрузка JobGroup | Время: {Ms} мс | Jobs: {Count}",
jobGroupId, stageSw.ElapsedMilliseconds, jobsInGroup.Count);
// === Поиск эталонного Job ===
var maxJob = jobsInGroup
.Where(j => j.MaxValueRelationships.HasValue)
.OrderByDescending(j => j.MaxValueRelationships)
.FirstOrDefault();
if (maxJob == null)
{
logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Не найден Job с MaxValueRelationships");
return;
}
logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}).", maxJob.Id, maxJob.MaxValueRelationships);
// === ЭТАП 2: Фильтрация юнитов ===
stageSw.Restart();
var unitFilterResults = await unitFilterService.GetUnitsByJobFilterAsync(maxJob.Id);
stageSw.Stop();
var filterCount = unitFilterResults?.Count() ?? 0;
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Фильтрация юнитов | Время: {Ms} мс | Результат: {Count}",
jobGroupId, stageSw.ElapsedMilliseconds, filterCount);
if (unitFilterResults == null || !unitFilterResults.Any())
{
logger.LogInformation("Для JobGroup {JobGroupId} фильтры не дали Unit'ов с подходящими связями.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Фильтры не дали Unit'ов с подходящими связями");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
return;
}
// === ЭТАП 3: Групповая фильтрация ===
stageSw.Restart();
var finalFilteredUnits = await groupedTemplateUnitFilter.FilterAsync(unitFilterResults, jobGroup);
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Групповая фильтрация | Время: {Ms} мс | Результат: {Count}",
jobGroupId, stageSw.ElapsedMilliseconds, finalFilteredUnits.Count);
if (!finalFilteredUnits.Any())
{
logger.LogInformation("После применения правил фильтрации в JobGroup {JobGroupId} не осталось юнитов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет юнитов после фильтрации");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup); return;
}
// === ЭТАП 4: Разрешение конфликтов ===
stageSw.Restart();
var initialReverseMapping = await unitInTemplateConflictMapper.BuildMappingAsync(finalFilteredUnits, maxJob);
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Разрешение конфликтов | Время: {Ms} мс | Связей: {Count}",
jobGroupId, stageSw.ElapsedMilliseconds, initialReverseMapping.Count);
if (!initialReverseMapping.Any())
{
logger.LogInformation("После разрешения конфликтов в JobGroup {JobGroupId} не осталось связей.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет связей после разрешения конфликтов");
return;
}
// === ЭТАП 5: Построение структуры групп ===
stageSw.Restart();
var templateGroups = await groupedTemplateBuilder.BuildAsync(initialReverseMapping, jobGroup, maxJob);
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Построение групп | Время: {Ms} мс | Групп: {Count}",
jobGroupId, stageSw.ElapsedMilliseconds, templateGroups.Count);
if (!templateGroups.Any())
{
logger.LogInformation("После построения структуры групп в JobGroup {JobGroupId} не осталось данных.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Нет данных после построения групп");
return;
}
// === ЭТАП 6: Обработка групп (сравнение, обновление, MQ) ===
stageSw.Restart();
var expectedTemplateKeys = await groupedTemplateProcessor.ProcessAsync(
templateGroups,
jobsInGroup,
maxJob,
initiator);
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Обработка групп | Время: {Ms} мс | Ключей: {Count}",
jobGroupId, stageSw.ElapsedMilliseconds, expectedTemplateKeys.Count);
// === ЭТАП 7: Деактивация лишних шаблонов ===
stageSw.Restart();
await DeactivateUnusedTemplatesAsync(expectedTemplateKeys, jobGroupId, jobsInGroup, initiator);
stageSw.Stop();
logger.LogDebug("[Perf] JobGroup {JobGroupId} | Этап: Деактивация | Время: {Ms} мс",
jobGroupId, stageSw.ElapsedMilliseconds);
// === ИТОГО === totalSw.Stop();
logger.LogInformation(
"[Perf] JobGroup {JobGroupId} | ИТОГО: {TotalMs} мс",
jobGroupId, totalSw.ElapsedMilliseconds);
await UpdateMatchingStatusAsync(jobGroupId, "Синхронизация завершена успешно");
await matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
logger.LogInformation("Синхронизация шаблонов завершена для JobGroup {JobGroupId}.", jobGroupId);
}
catch (Exception ex)
{
totalSw.Stop();
logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId} через {ElapsedMs} мс", jobGroupId, totalSw.ElapsedMilliseconds);
await UpdateMatchingStatusAsync(jobGroupId, $"Ошибка: {ex.Message}");
throw;
}
}
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId);
}
private async Task DeactivateUnusedTemplatesAsync(
HashSet<(Guid JobId, Guid UnitId, int Index)> expectedKeys,
Guid jobGroupId,
List<Job> jobsInGroup,
HistoryInitiator initiator)
{
var allJobIdsInGroup = jobsInGroup.Select(j => j.Id).ToHashSet();
var allExistingTemplatesInGroup = await templateService.Get()
.AsNoTracking()
.Include(t => t.Unit)
.Include(t => t.UnitsInTemplate)
.Where(t => allJobIdsInGroup.Contains(t.JobId) &&
t.StatusTypeId == TemplateStatusTypeEnum.Used &&
t.Job!.GroupId == jobGroupId)
.ToListAsync();
foreach (var existingTemplate in allExistingTemplatesInGroup)
{
var key = (existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index ?? -1);
if (!expectedKeys.Contains(key))
{
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, Unit {UnitId}, Index {Index}).",
existingTemplate.Id, existingTemplate.JobId, existingTemplate.UnitId, existingTemplate.Index);
await templateDeactivator.DeactivateTemplateAsync(existingTemplate, initiator);
}
}
}
private async Task UpdateMatchingStatusAsync(Guid jobGroupId, string comment)
{
var status = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = comment
};
await matchingStatusService.SetMatchingStatusAsync(
jobGroupId,
SyncTaskEntityTypeEnum.JobGroup,
new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
TimeSpan.FromMinutes(30)
);
}
}