refactor(templateMatcher): Переход на Pipeline-архитектуру для SimpleSync и GroupedSync.

- SimpleTemplateSynchronizer и GroupedTemplateSynchronizer переведены на паттерн Pipeline с разделением на Read/Write этапы
- Выделены контракты этапов (ISimpleSyncStage, IGroupedSyncStage) и контексты (SimpleSyncContext, GroupedSyncContext)
- Read-этапы безопасны для тестов (не пишут в БД/MQ), Write-этапы изолированы через отдельные интерфейсы
- Добавлено [Perf]-логирование каждого этапа с метриками времени выполнения
- Логи приведены к человекочитаемому формату 'Имя' (ID) для Job, JobGroup и Unit
- Устранено дублирование данных в контекстах (FilteredUnits перезаписывается, TemplateGroups строго типизирован)
- Константы неиспользуемых шаблонов вынесены в UnusedTemplateConstants
- Структура проекта реорганизована: SimpleSync, GroupedSync, Implementations, Interfaces
This commit is contained in:
Mikhail Kuznetsov
2026-06-11 15:09:46 +10:00
parent 16f4944a62
commit a438f18480
44 changed files with 1124 additions and 525 deletions

View File

@@ -1,260 +1,110 @@
using System.Diagnostics;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.Job;
using Microsoft.Extensions.Logging;
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.Implementations.GroupedSync;
using PARR.TemplateMatcher.Services.Interfaces;
using System.Diagnostics;
namespace PARR.TemplateMatcher.Services.Implementations;
namespace PARR.TemplateMatcher.Services.GroupedSync;
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;
private readonly IEnumerable<IGroupedSyncStage> _readStages;
private readonly IEnumerable<IGroupedSyncWriteStage> _writeStages;
private readonly IMatchingStatusService _matchingStatusService;
private readonly ILogger<GroupedTemplateSynchronizer> _logger;
public GroupedTemplateSynchronizer(
ILogger<GroupedTemplateSynchronizer> logger,
IJobGroupRepository jobGroupService,
IUnitFilterService unitFilterService,
IGroupedTemplateUnitFilter groupedTemplateUnitFilter,
IUnitInTemplateConflictMapper unitInTemplateConflictMapper,
IGroupedTemplateBuilder groupedTemplateBuilder,
IGroupedTemplateProcessor groupedTemplateProcessor,
ITemplateRepository templateService,
ITemplateDeactivator templateDeactivator,
IMatchingStatusService matchingStatusService)
IEnumerable<IGroupedSyncStage> readStages,
IEnumerable<IGroupedSyncWriteStage> writeStages,
IMatchingStatusService matchingStatusService,
ILogger<GroupedTemplateSynchronizer> logger)
{
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);
_readStages = readStages;
_writeStages = writeStages;
_matchingStatusService = matchingStatusService;
_logger = logger;
}
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
{
var totalSw = Stopwatch.StartNew();
logger.LogInformation("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
_logger.LogInformation("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
// === Проверка: уже запущена? ===
var existingStatus = await matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
var existingStatus = await _matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
if (existingStatus.DetailsJobGroups?.Any() == true)
{
logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId);
_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)
);
await SetStatusAsync(jobGroupId, "Начало синхронизации");
var totalSw = Stopwatch.StartNew();
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);
var context = new GroupedSyncContext { JobGroupId = jobGroupId, Initiator = initiator };
if (jobGroup == null || jobGroup.Jobs == null || !jobGroup.Jobs.Any())
foreach (var stage in _readStages)
{
logger.LogWarning("JobGroup {JobGroupId} не найден или не содержит Job'ов.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "JobGroup не найден или пуст");
return;
var stageSw = Stopwatch.StartNew();
await stage.ExecuteAsync(context);
stageSw.Stop();
_logger.LogDebug("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | Этап: {Stage} | Время: {Ms} мс",
context.JobGroupName, jobGroupId, stage.StageName, stageSw.ElapsedMilliseconds);
}
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)
foreach (var stage in _writeStages)
{
logger.LogWarning("В JobGroup {JobGroupId} не найдено Job с установленным MaxValueRelationships.", jobGroupId);
await UpdateMatchingStatusAsync(jobGroupId, "Не найден Job с MaxValueRelationships");
return;
var stageSw = Stopwatch.StartNew();
await stage.ExecuteAsync(context);
stageSw.Stop();
_logger.LogDebug("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | Этап: {Stage} | Время: {Ms} мс",
context.JobGroupName, jobGroupId, stage.StageName, stageSw.ElapsedMilliseconds);
}
logger.LogDebug("Используется Job {JobId} с максимальным MaxValueRelationships ({MaxValue}).", maxJob.Id, maxJob.MaxValueRelationships);
totalSw.Stop();
_logger.LogInformation("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | ИТОГО: {TotalMs} мс",
context.JobGroupName, jobGroupId, totalSw.ElapsedMilliseconds);
// === ЭТАП 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);
await SetStatusAsync(jobGroupId, "Синхронизация завершена успешно");
await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
_logger.LogInformation("Синхронизация шаблонов завершена для JobGroup '{JobGroupName}' ({JobGroupId})",
context.JobGroupName, jobGroupId);
}
catch (GroupedSyncEarlyExitException ex)
{
totalSw.Stop();
_logger.LogInformation("JobGroup {JobGroupId}: {Reason} ({ElapsedMs} мс)",
jobGroupId, ex.Reason, totalSw.ElapsedMilliseconds);
await SetStatusAsync(jobGroupId, ex.Reason);
await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
}
catch (Exception ex)
{
totalSw.Stop();
logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId} через {ElapsedMs} мс", jobGroupId, totalSw.ElapsedMilliseconds);
await UpdateMatchingStatusAsync(jobGroupId, $"Ошибка: {ex.Message}");
_logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId} через {ElapsedMs} мс",
jobGroupId, totalSw.ElapsedMilliseconds);
await SetStatusAsync(jobGroupId, $"Ошибка: {ex.Message}");
throw;
}
}
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
public Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
{
logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция. Используйте SyncTemplatesForJobGroup для обновления.", jobId);
_logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
return Task.CompletedTask;
}
private async Task DeactivateUnusedTemplatesAsync(
HashSet<(Guid JobId, Guid UnitId, int Index)> expectedKeys,
Guid jobGroupId,
List<Job> jobsInGroup,
HistoryInitiator initiator)
public Task UpdateTemplatesForJobAsync(Guid jobId, 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);
}
}
_logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
return Task.CompletedTask;
}
private async Task UpdateMatchingStatusAsync(Guid jobGroupId, string comment)
private async Task SetStatusAsync(Guid jobGroupId, string comment)
{
var status = new MatchingStatusItemDto
{
@@ -262,12 +112,9 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
Action = TemplateMatcherActionEnum.Sync,
Comment = comment
};
await matchingStatusService.SetMatchingStatusAsync(
jobGroupId,
SyncTaskEntityTypeEnum.JobGroup,
await _matchingStatusService.SetMatchingStatusAsync(
jobGroupId, SyncTaskEntityTypeEnum.JobGroup,
new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
TimeSpan.FromMinutes(30)
);
TimeSpan.FromMinutes(30));
}
}