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:
@@ -15,8 +15,9 @@ using PARR.Domain.Entities.Job;
|
||||
using PARR.Domain.Entities.Unit;
|
||||
using PARR.Domain.Enums;
|
||||
using PARR.Domain.Settings;
|
||||
using PARR.TemplateMatcher.Models;
|
||||
using PARR.TemplateMatcher.Constants;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.TemplateMatcher.Services.SimpleSync;
|
||||
using PARR.TemplateMatcher.Settings;
|
||||
using System.Diagnostics;
|
||||
|
||||
@@ -31,18 +32,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
private const bool DefaultUsedTemplateState = false;
|
||||
private const bool DefaultUsedScheduleState = false;
|
||||
|
||||
// === Константы для логики неиспользуемых шаблонов ===
|
||||
private const string FieldNameResponsibilityArea = "ЗОНА_ОТВЕТСТВЕННОСТИ";
|
||||
private const string FieldNameParrTag = "ПАРР тег";
|
||||
private const string TagValueNotWorking = "ПАРР-НЕИСП";
|
||||
|
||||
private readonly IEnumerable<ISimpleSyncStage> readStages;
|
||||
private readonly IEnumerable<ISimpleSyncWriteStage> writeStages;
|
||||
private readonly ILogger<SimpleTemplateSynchronizer> logger;
|
||||
private readonly IUnitFilterService unitFilterService;
|
||||
private readonly MqSettings mqSettings;
|
||||
private readonly IRabbitService mqService;
|
||||
private readonly ITemplateRepository templateService;
|
||||
private readonly IJobRepository jobService;
|
||||
private readonly ITemplateDeactivator templateDeactivator;
|
||||
private readonly ITemplateNameNormalizer templateNameNormalizer;
|
||||
private readonly ITemplateAllocationService templateAllocationService;
|
||||
private readonly ITemplateMqPublisher templateMqPublisher;
|
||||
@@ -54,13 +51,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
private readonly IUnitRepository unitRepository;
|
||||
|
||||
public SimpleTemplateSynchronizer(
|
||||
IEnumerable<ISimpleSyncStage> readStages,
|
||||
IEnumerable<ISimpleSyncWriteStage> writeStages,
|
||||
ILogger<SimpleTemplateSynchronizer> logger,
|
||||
IUnitFilterService unitFilterService,
|
||||
MqSettings mqSettings,
|
||||
IRabbitService mqService,
|
||||
ITemplateRepository templateService,
|
||||
IJobRepository jobService,
|
||||
ITemplateDeactivator templateDeactivator,
|
||||
ITemplateNameNormalizer templateNameNormalizer,
|
||||
ITemplateAllocationService templateAllocationService,
|
||||
ITemplateMqPublisher templateMqPublisher,
|
||||
@@ -72,13 +70,14 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
IUnitRepository unitRepository
|
||||
)
|
||||
{
|
||||
this.readStages = readStages;
|
||||
this.writeStages = writeStages;
|
||||
this.logger = logger;
|
||||
this.unitFilterService = unitFilterService;
|
||||
this.mqSettings = mqSettings;
|
||||
this.mqService = mqService;
|
||||
this.templateService = templateService;
|
||||
this.jobService = jobService;
|
||||
this.templateDeactivator = templateDeactivator;
|
||||
this.templateNameNormalizer = templateNameNormalizer;
|
||||
this.templateAllocationService = templateAllocationService;
|
||||
this.templateMqPublisher = templateMqPublisher;
|
||||
@@ -92,18 +91,15 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
|
||||
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
||||
{
|
||||
// === Специальная обработка для Job неиспользуемых шаблонов ===
|
||||
if (jobId == settingsFromDb.JobIdForUnusedTemplates)
|
||||
{
|
||||
logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов {JobId}", jobId);
|
||||
logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов '{JobId}'", jobId);
|
||||
await SyncUnusedTemplatesAsync(jobId, initiator);
|
||||
return;
|
||||
}
|
||||
|
||||
var totalSw = Stopwatch.StartNew();
|
||||
logger.LogInformation("Начало синхронизации шаблонов для Job {JobId}", jobId);
|
||||
|
||||
// === Проверка: уже запущена? ===
|
||||
var existingStatus = await matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
|
||||
if (existingStatus.DetailsJobs?.Any() == true)
|
||||
{
|
||||
@@ -111,7 +107,6 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
return;
|
||||
}
|
||||
|
||||
// === Устанавливаем статус "в процессе" ===
|
||||
var initialStatus = new MatchingStatusItemDto
|
||||
{
|
||||
DateStart = DateTimeOffset.UtcNow,
|
||||
@@ -119,169 +114,49 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
Comment = "Начало синхронизации"
|
||||
};
|
||||
await matchingStatusService.SetMatchingStatusAsync(
|
||||
jobId,
|
||||
SyncTaskEntityTypeEnum.Job,
|
||||
jobId, SyncTaskEntityTypeEnum.Job,
|
||||
new MatchingStatusItem { Data = initialStatus, Timestamp = DateTimeOffset.UtcNow, Source = nameof(SimpleTemplateSynchronizer) },
|
||||
TimeSpan.FromMinutes(35)
|
||||
);
|
||||
TimeSpan.FromMinutes(35));
|
||||
|
||||
// Таймер запускается ПОСЛЕ инфраструктурных операций (статус, проверка блокировки)
|
||||
var totalSw = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
// === ЭТАП 1: Загрузка Job ===
|
||||
var stageSw = Stopwatch.StartNew();
|
||||
var job = await jobService.Get()
|
||||
.AsNoTracking()
|
||||
.Include(j => j.AutoControl)
|
||||
.Include(j => j.Tnk)
|
||||
.Include(j => j.Group)
|
||||
.ThenInclude(g => g!.GroupType)
|
||||
.Include(j => j.UnitFilters)
|
||||
.ThenInclude(uf => uf.RelationshipFilters)
|
||||
.FirstOrDefaultAsync(j => j.Id == jobId);
|
||||
var context = new SimpleSyncContext { JobId = jobId, Initiator = initiator };
|
||||
|
||||
if (job == null)
|
||||
foreach (var stage in readStages)
|
||||
{
|
||||
logger.LogWarning("Job {JobId} не найден.", jobId);
|
||||
await UpdateMatchingStatusAsync(jobId, "Job не найден");
|
||||
return;
|
||||
}
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job {JobId} | Этап: Загрузка Job | Время: {Ms} мс",
|
||||
jobId, stageSw.ElapsedMilliseconds);
|
||||
|
||||
// === ЭТАП 2: Фильтрация юнитов ===
|
||||
stageSw.Restart();
|
||||
var filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(jobId);
|
||||
stageSw.Stop();
|
||||
var filterCount = filteredUnits?.Count() ?? 0;
|
||||
logger.LogDebug("[Perf] Job {JobId} | Этап: Фильтрация юнитов | Время: {Ms} мс | Результат: {Count}",
|
||||
jobId, stageSw.ElapsedMilliseconds, filterCount);
|
||||
|
||||
var unitIds = filteredUnits?.Select(u => u.Id).ToHashSet() ?? new HashSet<Guid>();
|
||||
|
||||
// === ЭТАП 3: Загрузка существующих шаблонов ===
|
||||
stageSw.Restart();
|
||||
var existingTemplates = await templateService.Get()
|
||||
.Include(t => t.UnitsInTemplate)
|
||||
.Include(t => t.Job)
|
||||
.ThenInclude(t => t!.Group)
|
||||
.ThenInclude(t => t!.GroupType)
|
||||
.Include(t => t.Job)
|
||||
.ThenInclude(t => t!.Tnk)
|
||||
.Include(t => t.Unit)
|
||||
.Where(t => t.JobId == jobId)
|
||||
.ToListAsync();
|
||||
|
||||
var existingUsedTemplates = existingTemplates
|
||||
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used)
|
||||
.ToList();
|
||||
|
||||
var existingUnitIds = existingUsedTemplates.Select(t => t.UnitId).ToHashSet();
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job {JobId} | Этап: Загрузка шаблонов | Время: {Ms} мс | Используется: {Count}",
|
||||
jobId, stageSw.ElapsedMilliseconds, existingUsedTemplates.Count);
|
||||
|
||||
// === ЭТАП 4: Расчёт диффа (создание / деактивация / переименование) ===
|
||||
stageSw.Restart();
|
||||
|
||||
var newUnitIds = unitIds.Except(existingUnitIds).ToList();
|
||||
var unusedTemplates = existingUsedTemplates
|
||||
.Where(t => !unitIds.Contains(t.UnitId))
|
||||
.ToList();
|
||||
|
||||
// Проверка имён существующих шаблонов
|
||||
var templatesToRename = new List<(Template Template, string ExpectedName)>();
|
||||
foreach (var template in existingUsedTemplates)
|
||||
{
|
||||
if (!unitIds.Contains(template.UnitId))
|
||||
continue;
|
||||
|
||||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template);
|
||||
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
templatesToRename.Add((template, expectedName));
|
||||
}
|
||||
var stageSw = Stopwatch.StartNew();
|
||||
await stage.ExecuteAsync(context);
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job '{JobName}' ({JobId}) | Этап: {Stage} | Время: {Ms} мс",
|
||||
context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job {JobId} | Этап: Расчёт диффа | Время: {Ms} мс | Создать: {Create}, Деактивировать: {Deactivate}, Переименовать: {Rename}",
|
||||
jobId, stageSw.ElapsedMilliseconds, newUnitIds.Count, unusedTemplates.Count, templatesToRename.Count);
|
||||
|
||||
// === ЭТАП 5: Создание новых шаблонов ===
|
||||
stageSw.Restart();
|
||||
foreach (var unitId in newUnitIds)
|
||||
foreach (var stage in writeStages)
|
||||
{
|
||||
var isActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState;
|
||||
var isActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState;
|
||||
|
||||
var request = new TemplateAllocationRequest(
|
||||
TargetJob: job,
|
||||
TargetUnitId: unitId,
|
||||
TargetUnit: null,
|
||||
Index: null,
|
||||
UnitsInTemplate: new List<UnitInTemplateMessage>(),
|
||||
IsActiveTemplate: isActiveTemplate,
|
||||
IsActiveSchedule: isActiveSchedule,
|
||||
Initiator: initiator);
|
||||
|
||||
await templateAllocationService.AllocateAsync(request);
|
||||
var stageSw = Stopwatch.StartNew();
|
||||
await stage.ExecuteAsync(context);
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job '{JobName}' ({JobId}) | Этап: {Stage} | Время: {Ms} мс",
|
||||
context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds);
|
||||
}
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job {JobId} | Этап: Создание шаблонов | Время: {Ms} мс | Количество: {Count}",
|
||||
jobId, stageSw.ElapsedMilliseconds, newUnitIds.Count);
|
||||
|
||||
// === ЭТАП 6: Обновление имён существующих шаблонов ===
|
||||
stageSw.Restart();
|
||||
foreach (var (template, expectedName) in templatesToRename)
|
||||
{
|
||||
logger.LogDebug("Шаблон {TemplateId} требует обновления имени: '{OldName}' → '{NewName}'",
|
||||
template.Id, template.Name, expectedName);
|
||||
|
||||
var updateRequest = new TemplateUpdaterMessage
|
||||
{
|
||||
TemplateId = template.Id,
|
||||
JobId = jobId,
|
||||
UnitId = template.UnitId,
|
||||
Name = expectedName,
|
||||
IsActiveTemplate = template.IsActiveTemplate,
|
||||
IsActiveSchedule = template.IsActiveSchedule,
|
||||
IsNew = false,
|
||||
Index = template.Index,
|
||||
StatusTypeId = TemplateStatusTypeEnum.Used,
|
||||
Initiator = initiator,
|
||||
UnitsInTemplate = new List<UnitInTemplateMessage>()
|
||||
};
|
||||
|
||||
await templateMqPublisher.PublishUpdateAsync(updateRequest);
|
||||
}
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job {JobId} | Этап: Обновление имён | Время: {Ms} мс | Количество: {Count}",
|
||||
jobId, stageSw.ElapsedMilliseconds, templatesToRename.Count);
|
||||
|
||||
// === ЭТАП 7: Деактивация лишних шаблонов ===
|
||||
stageSw.Restart();
|
||||
foreach (var unusedTemplate in unusedTemplates)
|
||||
{
|
||||
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).",
|
||||
unusedTemplate.Id, jobId, unusedTemplate.UnitId);
|
||||
await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator);
|
||||
}
|
||||
stageSw.Stop();
|
||||
logger.LogDebug("[Perf] Job {JobId} | Этап: Деактивация | Время: {Ms} мс | Количество: {Count}",
|
||||
jobId, stageSw.ElapsedMilliseconds, unusedTemplates.Count);
|
||||
|
||||
// === ИТОГО ===
|
||||
totalSw.Stop();
|
||||
logger.LogInformation("[Perf] Job {JobId} | ИТОГО: {TotalMs} мс", jobId, totalSw.ElapsedMilliseconds);
|
||||
logger.LogInformation("[Perf] Job '{JobName}' ({JobId}) | ИТОГО: {TotalMs} мс",
|
||||
context.JobName, jobId, totalSw.ElapsedMilliseconds);
|
||||
|
||||
await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена успешно");
|
||||
await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
|
||||
logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId);
|
||||
logger.LogInformation("Синхронизация шаблонов завершена для Job '{JobName}' ({JobId})",
|
||||
context.JobName, jobId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
totalSw.Stop();
|
||||
logger.LogError(ex, "Ошибка при синхронизации Job {JobId} через {ElapsedMs} мс", jobId, totalSw.ElapsedMilliseconds);
|
||||
logger.LogError(ex, "Ошибка при синхронизации Job '{JobName}' ({JobId}) через {ElapsedMs} мс",
|
||||
string.Empty, jobId, totalSw.ElapsedMilliseconds);
|
||||
await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
@@ -293,6 +168,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId}. Это не поддерживаемая операция.", jobGroupId);
|
||||
}
|
||||
|
||||
|
||||
public async Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
|
||||
{
|
||||
logger.LogDebug("Обновление шаблонов для Job {JobId}", jobId);
|
||||
@@ -444,30 +320,29 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
||||
try
|
||||
{
|
||||
// 1. Находим ID нужных полей
|
||||
var responsableAreaField = await unitFieldService.GetByAihitNameAsync(FieldNameResponsibilityArea);
|
||||
var tagField = await unitFieldService.GetByAihitNameAsync(FieldNameParrTag);
|
||||
var responsableAreaField = await unitFieldService.GetByAihitNameAsync(UnusedTemplateConstants.ResponsibilityAreaFieldName);
|
||||
var tagField = await unitFieldService.GetByAihitNameAsync(UnusedTemplateConstants.ParrTagFieldName);
|
||||
|
||||
if (responsableAreaField == null || tagField == null)
|
||||
{
|
||||
logger.LogError("Не найдены поля '{Field1}' или '{Field2}'. Синхронизация прервана.", FieldNameResponsibilityArea, FieldNameParrTag);
|
||||
logger.LogError("Не найдены поля '{Field1}' или '{Field2}'. Синхронизация прервана.", UnusedTemplateConstants.ResponsibilityAreaFieldName, UnusedTemplateConstants.NotUsedTagValue);
|
||||
await UpdateMatchingStatusAsync(unusedJobId, "Ошибка конфигурации полей");
|
||||
return;
|
||||
}
|
||||
|
||||
var responsableAreaFieldId = responsableAreaField.Id;
|
||||
var tagFieldId = tagField.Id;
|
||||
const string targetTagValue = TagValueNotWorking;
|
||||
|
||||
// 2. Находим ValueId для тега "ПАРР-НЕИСП"
|
||||
var targetTagValueId = await unitInValueService.Get()
|
||||
.AsNoTracking()
|
||||
.Where(uiv => uiv.FieldId == tagFieldId && uiv.Value != null && uiv.Value.Value == targetTagValue)
|
||||
.Where(uiv => uiv.FieldId == tagFieldId && uiv.Value != null && uiv.Value.Value == UnusedTemplateConstants.NotUsedTagValue)
|
||||
.Select(uiv => uiv.ValueId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
if (targetTagValueId == Guid.Empty)
|
||||
{
|
||||
logger.LogWarning("Значение '{TagValue}' для поля '{FieldName}' не найдено в справочнике UnitFieldValue.", targetTagValue, FieldNameParrTag);
|
||||
logger.LogWarning("Значение '{TagValue}' для поля '{FieldName}' не найдено в справочнике UnitFieldValue.", UnusedTemplateConstants.NotUsedTagValue, UnusedTemplateConstants.ParrTagFieldName);
|
||||
}
|
||||
|
||||
var unusedJob = await jobService.Get().AsNoTracking()
|
||||
|
||||
Reference in New Issue
Block a user