From 336cf90deb3061223e760915d01d0a283be8394e Mon Sep 17 00:00:00 2001 From: Mikhail Kuznetsov Date: Thu, 4 Dec 2025 14:24:42 +1000 Subject: [PATCH] =?UTF-8?q?feat(templateMatcher):=20=D1=80=D0=B5=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=20=D1=84=D1=83=D0=BD?= =?UTF-8?q?=D0=BA=D1=86=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BD=D0=B5=D0=B8=D1=81=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D1=83=D0=B5=D0=BC=D1=8B=D1=85=20=D1=88=D0=B0?= =?UTF-8?q?=D0=B1=D0=BB=D0=BE=D0=BD=D0=BE=D0=B2,=20=D0=BF=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B1=D0=B5=D0=B7=20=D0=BE=D0=B1=D0=B5=D0=B7=D0=BB?= =?UTF-8?q?=D0=B8=D1=87=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B8=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=D0=B0?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Implementations/UnitFilterService.cs | 37 +-- .../Interfaces/IUnitFilterService.cs | 13 - .../Implementations/TemplateService.cs | 15 - PARR.TemplateMatcher/TemplateMatcher.cs | 302 +++++++++++++----- 4 files changed, 227 insertions(+), 140 deletions(-) diff --git a/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs b/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs index 2c7eeb17..eeda69ee 100644 --- a/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs +++ b/PARR.DAL/DomainServices/Implementations/UnitFilterService.cs @@ -3,10 +3,8 @@ using Microsoft.Extensions.Logging; using PARR.DAL.Contracts; using PARR.DAL.DomainServices.Interfaces; using PARR.DAL.Models.Job; -using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.Services.Interfaces.Unit; -using System.Net.NetworkInformation; using System.Text.RegularExpressions; @@ -17,19 +15,16 @@ namespace PARR.DAL.DomainServices.Implementations private readonly ILogger logger; private readonly IJobService jobService; private readonly IUnitService unitService; - private readonly ITemplateService templateService; public UnitFilterService( ILogger logger, IJobService jobService, - IUnitService unitService, - ITemplateService templateService + IUnitService unitService ) { this.logger = logger; this.jobService = jobService; this.unitService = unitService; - this.templateService = templateService; } @@ -287,35 +282,5 @@ namespace PARR.DAL.DomainServices.Implementations return result; } - - - public async Task> GetUnitsIdByJobFilterWithTemplateStatusAsync( - Guid jobId, - bool mustHaveTemplate, - int? takeCount = null - ) - { - var unitIds = await GetUnitsIdByJobFilterAsync(jobId, takeCount); - if (unitIds == null || !unitIds.Any()) - return Array.Empty(); - - var unitIdSet = unitIds.ToHashSet(); - - // Получаем UnitId, для которых уже есть шаблоны по этому jobId - var existingUnitIds = await templateService - .Get() - .AsNoTracking() - .Where(template => - template.JobId == jobId && - unitIdSet.Contains(template.UnitId)) - .Select(template => template.UnitId) - .ToListAsync(); - - var existingSet = existingUnitIds.ToHashSet(); - - return mustHaveTemplate - ? existingSet - : unitIdSet.Except(existingSet); - } } } diff --git a/PARR.DAL/DomainServices/Interfaces/IUnitFilterService.cs b/PARR.DAL/DomainServices/Interfaces/IUnitFilterService.cs index 4ddb00e1..0735763b 100644 --- a/PARR.DAL/DomainServices/Interfaces/IUnitFilterService.cs +++ b/PARR.DAL/DomainServices/Interfaces/IUnitFilterService.cs @@ -18,18 +18,5 @@ namespace PARR.DAL.DomainServices.Interfaces /// Job обязательно должен содержать UnitFilter, Group и GroupType /// Task?> GetUnitsIdByJobFilterAsync(Job job, int? takeCount = null); - - - /// - /// Получить список Id ЭК в зависимости от параметра mustHaveTemplate - /// - /// - /// - /// true - ЭК, которые имеют связанные шаблоны - /// false - ЭК, для которых шаблоны не созданы - /// - /// - /// Лист Id ЭК - Task> GetUnitsIdByJobFilterWithTemplateStatusAsync(Guid jobId, bool mustHaveTemplate, int? takeCount = null); } } diff --git a/PARR.DAL/Services/Implementations/TemplateService.cs b/PARR.DAL/Services/Implementations/TemplateService.cs index 82ed3c7f..3e55b0f4 100644 --- a/PARR.DAL/Services/Implementations/TemplateService.cs +++ b/PARR.DAL/Services/Implementations/TemplateService.cs @@ -46,21 +46,6 @@ namespace PARR.DAL.Services.Implementations .ThenInclude(p => p!.Process) .Include(t => t.Job) .ThenInclude(t => t!.Group); - - - //return Get() - // .Include(h => h.Host) - // .ThenInclude(t => t!.ResponseArea) - // .Include(h => h.Host) - // .ThenInclude(t => t!.WorkGroup) - // .ThenInclude(t => t!.ResponseArea) - // .Include(h => h.Host) - // .ThenInclude(t => t!.EkStatus) - // .Include(a => a.ApplicationsInWork) - // .ThenInclude(w => w!.Work) - // .ThenInclude(t => t!.Tnk) - // .ThenInclude(s => s!.Subprocess) - // .ThenInclude(p => p!.Process); } public override Task CreateAsync(Template obj) diff --git a/PARR.TemplateMatcher/TemplateMatcher.cs b/PARR.TemplateMatcher/TemplateMatcher.cs index 187c87ca..9702ca4c 100644 --- a/PARR.TemplateMatcher/TemplateMatcher.cs +++ b/PARR.TemplateMatcher/TemplateMatcher.cs @@ -9,6 +9,7 @@ using PARR.DAL.Models; using PARR.DAL.Models.Job; using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces.Job; +using PARR.DAL.TransformServices; using PARR.TemplateMatcher.Settings; using System.Text.Json; @@ -23,6 +24,7 @@ namespace PARR.TemplateMatcher private readonly ITemplateService templateService; private readonly IJobService jobService; private readonly IShortcodesService shortcodesService; + private readonly IEsppScheduleTransformService esppScheduleTransformService; public TemplateMatcher( ILogger logger, @@ -31,7 +33,8 @@ namespace PARR.TemplateMatcher IMqService mqService, ITemplateService templateService, IJobService jobService, - IShortcodesService shortcodesService + IShortcodesService shortcodesService, + IEsppScheduleTransformService esppScheduleTransformService ) { this.logger = logger; @@ -41,6 +44,7 @@ namespace PARR.TemplateMatcher this.templateService = templateService; this.jobService = jobService; this.shortcodesService = shortcodesService; + this.esppScheduleTransformService = esppScheduleTransformService; } @@ -48,48 +52,162 @@ namespace PARR.TemplateMatcher { logger.LogDebug("Начало синхронизации шаблонов для JobId {JobId}", jobId); - // 1. Получаем UnitId, которые должны иметь шаблоны (по фильтрам Job) + // 1. Получаем UnitId по фильтру var units = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId); if (units == null) { logger.LogWarning("Для JobId {JobId} фильтр вернул null. Пропускаем.", jobId); return; } + + // Список Id ЭК для которых ожидаемо должны сущестовать шаблоны var expectedUnitIds = units.ToHashSet(); - - logger.LogDebug("Для JobId {JobId} найдено {Count} Unit'ов, соответствующих фильтрам.", jobId, expectedUnitIds.Count); - if (!expectedUnitIds.Any()) { - logger.LogInformation("Для JobId {JobId} не найдено Unit'ов, соответствующих фильтрам.", jobId); + logger.LogInformation("Для JobId {JobId} не найдено Unit'ов по фильтрам.", jobId); return; } - // 2. Получаем все шаблоны для этого Job - var existingTemplates = await templateService.Get() - .Where(t => t.JobId == jobId) - .ToListAsync(); + // 2. Получаем Job с Group, нужен для определения имени шаблона из маски и расчёта nextRun + var job = await jobService.Get() + .Include(j => j.Group) + .FirstOrDefaultAsync(j => j.Id == jobId); - logger.LogDebug("Для JobId {JobId} найдено {Count} существующих шаблонов.", jobId, existingTemplates.Count()); - - // 3. Получаем Job, чтобы получить маску имени - var job = await jobService.GetAsync(jobId); if (job == null) { logger.LogError("Job с Id {JobId} не найден.", jobId); return; } - // 4. Находим UnitId, для которых нет шаблонов (нужно создать) - var existingUnitIds = existingTemplates.Select(t => t.UnitId).ToHashSet(); - var unitIdsToCreate = expectedUnitIds.Except(existingUnitIds); + // 3. Получаем ВСЕ шаблоны для Job + var existingTemplates = await templateService.Get() + .Where(t => t.JobId == jobId) + .ToListAsync(); - logger.LogDebug("Для JobId {JobId} найдено {Count} Unit'ов без шаблонов (требуется создание).", jobId, unitIdsToCreate.Count()); + logger.LogDebug("JobId {JobId}: {Expected} ожидаемых UnitId, {Existing} существующих шаблонов.", + jobId, expectedUnitIds.Count, existingTemplates.Count); - foreach (var unitId in unitIdsToCreate) + // 4. Деактивация: шаблоны, НЕ в фильтре → Unused + var templatesToDeactivate = existingTemplates + .Where(t => !expectedUnitIds.Contains(t.UnitId)) + .ToList(); + + foreach (var template in templatesToDeactivate) { - logger.LogInformation("Для UnitId {UnitId} нет шаблона, запрашиваем создание.", unitId); + logger.LogInformation("Деактивация шаблона {TemplateId} (UnitId {UnitId}) → Unused", + template.Id, template.UnitId); + template.StatusTypeId = TemplateStatusTypeEnum.Unused; + template.IsActiveSchedule = false; + template.DateModified = DateTimeOffset.UtcNow; + } + + // Сохраняем деактивацию + if (templatesToDeactivate.Any()) + { + if (!await templateService.CommitAsync(initiator)) + { + logger.LogError("Не удалось сохранить деактивацию шаблонов. Прерываем синхронизацию."); + return; + } + logger.LogInformation("Сохранены {Count} шаблонов в статусе Unused.", templatesToDeactivate.Count); + } + + // 5. Перечитываем шаблоны (на случай, если кто-то параллельно что-то менял) + existingTemplates = await templateService.Get() + .Where(t => t.JobId == jobId) + .ToListAsync(); + + var unitToTemplate = existingTemplates.ToDictionary(t => t.UnitId, t => t); + + // 6. UnitId без шаблона + var unitIdsMissingTemplates = expectedUnitIds + .Where(unitId => !unitToTemplate.ContainsKey(unitId)) + .ToList(); + + // 7. Получаем Unused-шаблоны + var unitIdsToCreateFresh = new List(); + + foreach (var unitId in unitIdsMissingTemplates) + { + var reused = await TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator); + if (reused != null) + { + logger.LogInformation("Переиспользован шаблон {TemplateId} для UnitId {UnitId}.", reused.Id, unitId); + + var referenceDate = job.Group?.ReferenceDate ?? DateTime.UtcNow; + var nextRun = await esppScheduleTransformService.GetNextDateAsync(job.GroupId, referenceDate); + + var updateRequest = new TemplateUpdaterMq + { + TemplateId = reused.Id, + JobId = jobId, + UnitId = unitId, + Name = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, unitId, jobId), + IsActiveTemplate = false, + IsActiveSchedule = false, + StatusTypeId = TemplateStatusTypeEnum.Used, + Initiator = initiator, + NextRun = nextRun + }; + + await SendTemplateUpdateMessage(updateRequest); + } + else + { + logger.LogInformation("Нет доступных Unused-шаблонов для UnitId {UnitId} → создадим новый.", unitId); + unitIdsToCreateFresh.Add(unitId); + } + } + + // 9. Обновление имён для существующих шаблонов (в фильтре) + var templatesToCheckName = existingTemplates + .Where(t => expectedUnitIds.Contains(t.UnitId)) + .ToList(); + + foreach (var template in templatesToCheckName) + { + var expectedName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, template.UnitId, jobId); + if (template.Name != expectedName) + { + logger.LogInformation("Обновление имени шаблона {TemplateId} → '{Name}'.", + template.Id, expectedName); + + template.StatusTypeId = TemplateStatusTypeEnum.Updating; + template.Name = expectedName; + template.DateModified = DateTime.UtcNow; + + // Сначала сохраняем + if (!await templateService.CommitAsync(initiator)) + { + logger.LogError("Не удалось сохранить обновление имени шаблона {TemplateId}.", template.Id); + continue; + } + + // Потом отправляем + var updateRequest = new TemplateUpdaterMq + { + TemplateId = template.Id, + JobId = jobId, + UnitId = template.UnitId, + Name = expectedName, + IsActiveTemplate = template.IsActiveTemplate, + IsActiveSchedule = template.IsActiveSchedule, + LastRun = template.LastRun, + NextRun = template.NextRun, + Index = template.Index, + StatusTypeId = TemplateStatusTypeEnum.Used, + Initiator = initiator + }; + + await SendTemplateUpdateMessage(updateRequest); + } + } + + // 10. Создание новых шаблонов + foreach (var unitId in unitIdsToCreateFresh) + { + logger.LogInformation("Создание нового шаблона для UnitId {UnitId}.", unitId); var mqRequest = new TemplateGeneratorWorkerMq { JobId = jobId, @@ -100,66 +218,13 @@ namespace PARR.TemplateMatcher var msg = JsonSerializer.Serialize(mqRequest); var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new[] { msg }); - if (result.IsSuccess) - logger.LogInformation("Отправлен запрос на создание шаблона для UnitId {UnitId} в JobId {JobId}", unitId, jobId); - else - logger.LogError("Ошибка при отправке запроса на создание шаблона для UnitId {UnitId} в JobId {JobId}: {Message}", unitId, jobId, msg); + if (!result.IsSuccess) + logger.LogError("Ошибка отправки команды создания шаблона для UnitId {UnitId}.", unitId); } - // 5. Находим шаблоны, которые больше не соответствуют фильтрам (нужно отключить/обновить) - var unitIdsToRemove = existingUnitIds.Except(expectedUnitIds); - - logger.LogDebug("Для JobId {JobId} найдено {Count} Unit'ов с шаблонами, которые больше не соответствуют фильтрам (требуется обновление/отключение).", jobId, unitIdsToRemove.Count()); - - foreach (var template in existingTemplates) - { - var unitId = template.UnitId; - if (unitIdsToRemove.Contains(template.UnitId)) - { - logger.LogInformation("Шаблон {TemplateId} для UnitId {UnitId} больше не соответствует фильтрам Job. Требуется обновление статуса.", template.Id, template.UnitId); - - // 1. Меняем статус - template.StatusTypeId = TemplateStatusTypeEnum.Updating; - - // 2. Сохраняем изменения в базе через сервис - var saved = await templateService.CommitAsync(initiator); - if (!saved) - { - logger.LogError(null, "Не удалось сохранить изменения шаблона {TemplateId} перед отправкой в очередь обновления.", template.Id); - continue; // пропускаем этот шаблон - } - - // 3. Подготовим сообщение - var updateRequest = new TemplateUpdaterMq - { - TemplateId = template.Id, - JobId = template.JobId, - Name = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, template.UnitId, jobId), - IsActiveTemplate = template.IsActiveTemplate, - IsActiveSchedule = false, - LastRun = template.LastRun, - NextRun = template.NextRun, - UnitId = template.UnitId, - Index = template.Index, - StatusTypeId = TemplateStatusTypeEnum.Unused, // после обновления статус должен быть Unused - Initiator = initiator - }; - - // 4. Отправляем в очередь - await SendTemplateUpdateMessage(updateRequest); - } - else - { - // Unit всё ещё соответствует фильтрам — проверим, нужно ли обновить имя - logger.LogDebug("Шаблон {TemplateId} для UnitId {UnitId} соответствует фильтрам. Проверяем имя.", template.Id, unitId); - await CheckAndSendNameUpdateIfRequired(template, job, initiator); - } - } - - logger.LogDebug("Окончание синхронизации шаблонов для JobId {JobId}", jobId); + logger.LogInformation("Синхронизация завершена для JobId {JobId}.", jobId); } - public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator) { logger.LogDebug("Начало обновления имён шаблонов для JobId {JobId}", jobId); @@ -262,5 +327,90 @@ namespace PARR.TemplateMatcher updateRequest.TemplateId, msg); } } + + + private async Task TryReuseOneUnusedTemplateAsync(Guid jobId, Guid unitId, HistoryInitiator initiator, int maxAttempts = 3) + { + for (int attempt = 1; attempt <= maxAttempts; attempt++) + { + try + { + // Получаем список Unused — свежий каждый раз! + var unusedCandidates = await templateService.Get() + .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused) + .OrderBy(t => t.DateModified ?? t.DateCreated) // старейшие первыми + .Take(10) // небольшой буфер для избежания повторных запросов + .ToListAsync(); + + if (!unusedCandidates.Any()) + { + logger.LogDebug("Нет Unused-шаблонов для JobId {JobId} (попытка {Attempt}).", jobId, attempt); + return null; + } + + foreach (var candidate in unusedCandidates) + { + // Сохраняем оригинальное состояние для отладки и отката + var originalStatus = candidate.StatusTypeId; + var originalModified = candidate.DateModified; + + try + { + // Меняем ТОЛЬКО статус и дату — минимум изменений + candidate.StatusTypeId = TemplateStatusTypeEnum.Updating; + candidate.DateModified = DateTimeOffset.UtcNow; + + // Пробуем зафиксировать захват + var saved = await templateService.CommitAsync(initiator); + + if (saved) + { + logger.LogInformation( + "Успешно захвачен шаблон {TemplateId} для UnitId {UnitId} (попытка {Attempt}). " + + "Статус изменён на Updating.", + candidate.Id, unitId, attempt); + + return candidate; + } + else + { + logger.LogWarning( + "Commit вернул false при захвате шаблона {TemplateId} (попытка {Attempt}). Пробуем следующего.", + candidate.Id, attempt); + // Откатываем локальные изменения + candidate.StatusTypeId = originalStatus; + candidate.DateModified = originalModified; + } + } + catch (Exception ex) when ( + ex is DbUpdateException || + ex.InnerException?.Message.Contains("deadlock", StringComparison.OrdinalIgnoreCase) == true || + ex.InnerException?.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase) == true) + { + logger.LogWarning(ex, + "Конфликт при захвате шаблона {TemplateId} (попытка {Attempt}). Пробуем следующего.", + candidate.Id, attempt); + + // Откатываем + candidate.StatusTypeId = originalStatus; + candidate.DateModified = originalModified; + } + } + + if (attempt < maxAttempts) + { + var delayMs = Random.Shared.Next(5, 15) * attempt; + await Task.Delay(delayMs); + } + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка в попытке захвата Unused-шаблона (попытка {Attempt}).", attempt); + if (attempt == maxAttempts) throw; + } + } + + return null; + } } } \ No newline at end of file