From ffb15fc1e2d9613487c25a4636302435df067e91 Mon Sep 17 00:00:00 2001 From: Mikhail Kuznetsov Date: Fri, 5 Dec 2025 09:53:09 +1000 Subject: [PATCH] =?UTF-8?q?feat(templateMatcher):=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D1=8B=20=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=BE=D0=B4=D1=8B=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B8=20=D0=BE=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F,=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=82=D1=83=D1=81=D0=B0=20=D0=BD=D0=B0=20Used=20=D0=B5=D1=81?= =?UTF-8?q?=D0=BB=D0=B8=20=D1=81=D1=83=D1=89=D0=B5=D1=81=D1=82=D0=B2=D1=83?= =?UTF-8?q?=D0=B5=D1=82=20=D0=BD=D1=83=D0=B6=D0=BD=D1=8B=D0=B9=20=D1=88?= =?UTF-8?q?=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=20=D0=BF=D1=80=D0=B8=20=D1=81?= =?UTF-8?q?=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.Constants/TemplateMatcherActionEnum.cs | 4 +- PARR.TemplateMatcher/TemplateMatcher.cs | 304 +++++++++++--------- 2 files changed, 163 insertions(+), 145 deletions(-) diff --git a/PARR.Constants/TemplateMatcherActionEnum.cs b/PARR.Constants/TemplateMatcherActionEnum.cs index c9a6d58d..54c0e553 100644 --- a/PARR.Constants/TemplateMatcherActionEnum.cs +++ b/PARR.Constants/TemplateMatcherActionEnum.cs @@ -13,9 +13,7 @@ namespace PARR.Constants /// Sync = 0, /// - /// Обновить существующие шаблоны без сохдания новых(имя шаблона, новая Job и т.д.). - /// Проверить соответствие шаблонов(только Used) измененным фильтрам и выключить расписание если больше не подходит. - /// И проверить соответствие имени шаблона маске, переименовать при необходимости. + /// Только меняет имя шаблона и деактивирует шаблон и расписание, в случае когда ЭК не соответсвует Job+ставит статус Unused /// Update = 1 } diff --git a/PARR.TemplateMatcher/TemplateMatcher.cs b/PARR.TemplateMatcher/TemplateMatcher.cs index 9702ca4c..5113240b 100644 --- a/PARR.TemplateMatcher/TemplateMatcher.cs +++ b/PARR.TemplateMatcher/TemplateMatcher.cs @@ -17,6 +17,8 @@ namespace PARR.TemplateMatcher { internal class TemplateMatcher : ITemplateMatcher { + private const int UnusedCandidateBatchSize = 10; + private readonly ILogger logger; private readonly IUnitFilterService unitFilterService; private readonly MqSettings mqSettings; @@ -52,68 +54,68 @@ namespace PARR.TemplateMatcher { logger.LogDebug("Начало синхронизации шаблонов для JobId {JobId}", jobId); - // 1. Получаем UnitId по фильтру - var units = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId); - if (units == null) - { - logger.LogWarning("Для JobId {JobId} фильтр вернул null. Пропускаем.", jobId); - return; - } - - // Список Id ЭК для которых ожидаемо должны сущестовать шаблоны - var expectedUnitIds = units.ToHashSet(); + var expectedUnitIds = await GetExpectedUnitIdsAsync(jobId); if (!expectedUnitIds.Any()) { logger.LogInformation("Для JobId {JobId} не найдено Unit'ов по фильтрам.", jobId); return; } - // 2. Получаем Job с Group, нужен для определения имени шаблона из маски и расчёта nextRun - var job = await jobService.Get() - .Include(j => j.Group) - .FirstOrDefaultAsync(j => j.Id == jobId); - + var job = await GetJobWithGroupAsync(jobId); if (job == null) { logger.LogError("Job с Id {JobId} не найден.", jobId); return; } - // 3. Получаем ВСЕ шаблоны для Job var existingTemplates = await templateService.Get() - .Where(t => t.JobId == jobId) - .ToListAsync(); + .Where(t => t.JobId == jobId) + .ToListAsync(); logger.LogDebug("JobId {JobId}: {Expected} ожидаемых UnitId, {Existing} существующих шаблонов.", jobId, expectedUnitIds.Count, existingTemplates.Count); - // 4. Деактивация: шаблоны, НЕ в фильтре → Unused + // 4. Шаблоны, НЕ в фильтре → отправляем на деактивацию через worker var templatesToDeactivate = existingTemplates .Where(t => !expectedUnitIds.Contains(t.UnitId)) .ToList(); foreach (var template in templatesToDeactivate) { - logger.LogInformation("Деактивация шаблона {TemplateId} (UnitId {UnitId}) → Unused", + if (template.StatusTypeId == TemplateStatusTypeEnum.Updating) continue; + + logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация через worker.", template.Id, template.UnitId); - template.StatusTypeId = TemplateStatusTypeEnum.Unused; - template.IsActiveSchedule = false; + // Только переводим в Updating — остальное пусть делает worker + template.StatusTypeId = TemplateStatusTypeEnum.Updating; template.DateModified = DateTimeOffset.UtcNow; - } - // Сохраняем деактивацию - if (templatesToDeactivate.Any()) - { if (!await templateService.CommitAsync(initiator)) { - logger.LogError("Не удалось сохранить деактивацию шаблонов. Прерываем синхронизацию."); - return; + logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating для деактивации.", template.Id); + continue; } - logger.LogInformation("Сохранены {Count} шаблонов в статусе Unused.", templatesToDeactivate.Count); + + var updateRequest = new TemplateUpdaterMq + { + TemplateId = template.Id, + JobId = jobId, + UnitId = template.UnitId, + Name = template.Name, + IsActiveTemplate = false, + IsActiveSchedule = false, + LastRun = template.LastRun, + NextRun = template.NextRun, + Index = template.Index, + StatusTypeId = TemplateStatusTypeEnum.Unused, + Initiator = initiator + }; + + await SendTemplateUpdateMessage(updateRequest); } - // 5. Перечитываем шаблоны (на случай, если кто-то параллельно что-то менял) + // 5. Перечитываем шаблоны existingTemplates = await templateService.Get() .Where(t => t.JobId == jobId) .ToListAsync(); @@ -125,7 +127,6 @@ namespace PARR.TemplateMatcher .Where(unitId => !unitToTemplate.ContainsKey(unitId)) .ToList(); - // 7. Получаем Unused-шаблоны var unitIdsToCreateFresh = new List(); foreach (var unitId in unitIdsMissingTemplates) @@ -135,15 +136,15 @@ namespace PARR.TemplateMatcher { logger.LogInformation("Переиспользован шаблон {TemplateId} для UnitId {UnitId}.", reused.Id, unitId); - var referenceDate = job.Group?.ReferenceDate ?? DateTime.UtcNow; - var nextRun = await esppScheduleTransformService.GetNextDateAsync(job.GroupId, referenceDate); + var expectedName = await GetNormalizedTemplateNameAsync(job, unitId); + var nextRun = await GetNextRunAsync(job); // новый шаблон → всегда пересчитываем var updateRequest = new TemplateUpdaterMq { TemplateId = reused.Id, JobId = jobId, UnitId = unitId, - Name = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, unitId, jobId), + Name = expectedName, IsActiveTemplate = false, IsActiveSchedule = false, StatusTypeId = TemplateStatusTypeEnum.Used, @@ -160,31 +161,32 @@ namespace PARR.TemplateMatcher } } - // 9. Обновление имён для существующих шаблонов (в фильтре) - var templatesToCheckName = existingTemplates + // 9. Шаблоны В ФИЛЬТРЕ → обновление имени / реактивация + var templatesInFilter = existingTemplates .Where(t => expectedUnitIds.Contains(t.UnitId)) .ToList(); - foreach (var template in templatesToCheckName) + foreach (var template in templatesInFilter) { - var expectedName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, template.UnitId, jobId); - if (template.Name != expectedName) + var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId); + bool needsUpdate = template.Name != expectedName + || template.StatusTypeId != TemplateStatusTypeEnum.Used; + + if (needsUpdate && template.StatusTypeId != TemplateStatusTypeEnum.Updating) { - logger.LogInformation("Обновление имени шаблона {TemplateId} → '{Name}'.", - template.Id, expectedName); + logger.LogInformation("Шаблон {TemplateId}: требуется обновление имени или реактивация.", template.Id); template.StatusTypeId = TemplateStatusTypeEnum.Updating; - template.Name = expectedName; - template.DateModified = DateTime.UtcNow; + template.DateModified = DateTimeOffset.UtcNow; - // Сначала сохраняем if (!await templateService.CommitAsync(initiator)) { - logger.LogError("Не удалось сохранить обновление имени шаблона {TemplateId}.", template.Id); + logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id); continue; } - // Потом отправляем + var nextRun = await GetNextRunAsync(job, template.NextRun); + var updateRequest = new TemplateUpdaterMq { TemplateId = template.Id, @@ -194,7 +196,7 @@ namespace PARR.TemplateMatcher IsActiveTemplate = template.IsActiveTemplate, IsActiveSchedule = template.IsActiveSchedule, LastRun = template.LastRun, - NextRun = template.NextRun, + NextRun = nextRun, Index = template.Index, StatusTypeId = TemplateStatusTypeEnum.Used, Initiator = initiator @@ -227,84 +229,88 @@ namespace PARR.TemplateMatcher public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator) { - logger.LogDebug("Начало обновления имён шаблонов для JobId {JobId}", jobId); + logger.LogDebug("Начало обновления шаблонов для JobId {JobId}", jobId); - // 1. Получаем все шаблоны для этого Job var existingTemplates = await templateService.Get() - .Where(t => t.JobId == jobId) - .ToListAsync(); + .Where(t => t.JobId == jobId) + .ToListAsync(); - logger.LogDebug("Для JobId {JobId} найдено {Count} существующих шаблонов.", jobId, existingTemplates.Count()); + if (!existingTemplates.Any()) return; - if (!existingTemplates.Any()) - { - logger.LogInformation("Для JobId {JobId} не найдено существующих шаблонов для обновления имени.", jobId); - return; - } + var job = await GetJobWithGroupAsync(jobId); + if (job == null) return; - // 2. Получаем Job, чтобы получить маску имени - var job = await jobService.GetAsync(jobId); - if (job == null) - { - logger.LogError("Job с Id {JobId} не найден.", jobId); - return; - } + var currentUnitIds = await GetExpectedUnitIdsAsync(jobId); + + logger.LogDebug("JobId {JobId}: {Count} UnitId по текущему фильтру.", jobId, currentUnitIds.Count); foreach (var template in existingTemplates) { - logger.LogDebug("Проверяем имя шаблона {TemplateId} для UnitId {UnitId}.", template.Id, template.UnitId); - // Проверяем, нужно ли обновить имя - await CheckAndSendNameUpdateIfRequired(template, job, initiator); - } + logger.LogDebug("Обработка шаблона {TemplateId} (UnitId {UnitId}).", template.Id, template.UnitId); - logger.LogDebug("Окончание обновления имён шаблонов для JobId {JobId}", jobId); - } + bool unitStillInFilter = currentUnitIds.Contains(template.UnitId); + var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId); + TemplateStatusTypeEnum targetStatus; + bool targetIsActiveTemplate; + bool targetIsActiveSchedule; - private async Task CheckAndSendNameUpdateIfRequired(Template template, Job job, HistoryInitiator initiator) - { - var expectedName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, template.UnitId, job.Id); - - logger.LogDebug("Шаблон {TemplateId}: текущее имя '{CurrentName}', ожидаемое имя '{ExpectedName}'", template.Id, template.Name, expectedName); - - if (template.Name != expectedName) - { - logger.LogInformation("Шаблон {TemplateId} для UnitId {UnitId} имеет устаревшее имя. Требуется обновление.", template.Id, template.UnitId); - - // 1. Меняем статус в шаблоне - template.StatusTypeId = TemplateStatusTypeEnum.Updating; - - // 2. Сохраняем изменения в базе через сервис - var saved = await templateService.CommitAsync(initiator); - if (!saved) + if (unitStillInFilter) { - logger.LogError("Не удалось сохранить изменения шаблона {TemplateId} перед отправкой в очередь обновления.", template.Id); - return; // не отправляем сообщение, если не сохранили + targetStatus = TemplateStatusTypeEnum.Used; + targetIsActiveTemplate = template.IsActiveTemplate; + targetIsActiveSchedule = template.IsActiveSchedule; + } + else + { + targetStatus = TemplateStatusTypeEnum.Unused; + targetIsActiveTemplate = false; + targetIsActiveSchedule = false; + logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация.", template.Id, template.UnitId); } - // 3. Подготовим сообщение для отправки в очередь - var updateRequest = new TemplateUpdaterMq - { - TemplateId = template.Id, - JobId = template.JobId, - Name = expectedName, - IsActiveTemplate = template.IsActiveTemplate, - IsActiveSchedule = template.IsActiveSchedule, - LastRun = template.LastRun, - NextRun = template.NextRun, - UnitId = template.UnitId, - Index = template.Index, - StatusTypeId = TemplateStatusTypeEnum.Used, - Initiator = initiator - }; + bool needsUpdate = + template.StatusTypeId != TemplateStatusTypeEnum.Updating && + ( + template.StatusTypeId != targetStatus || + template.Name != expectedName || + template.IsActiveTemplate != targetIsActiveTemplate || + template.IsActiveSchedule != targetIsActiveSchedule + ); - // 4. Отправляем в очередь - await SendTemplateUpdateMessage(updateRequest); - } - else - { - logger.LogDebug("Шаблон {TemplateId} для UnitId {UnitId} имеет актуальное имя.", template.Id, template.UnitId); + if (needsUpdate) + { + template.StatusTypeId = TemplateStatusTypeEnum.Updating; + template.DateModified = DateTimeOffset.UtcNow; + + if (!await templateService.CommitAsync(initiator)) + { + logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating.", template.Id); + continue; + } + + var nextRun = await GetNextRunAsync(job, template.NextRun); + + var updateRequest = new TemplateUpdaterMq + { + TemplateId = template.Id, + JobId = jobId, + UnitId = template.UnitId, + Name = expectedName, + IsActiveTemplate = targetIsActiveTemplate, + IsActiveSchedule = targetIsActiveSchedule, + LastRun = template.LastRun, + NextRun = nextRun, + Index = template.Index, + StatusTypeId = targetStatus, + Initiator = initiator + }; + + await SendTemplateUpdateMessage(updateRequest); + } } + + logger.LogInformation("Обновление шаблонов завершено для JobId {JobId}.", jobId); } @@ -335,82 +341,96 @@ namespace PARR.TemplateMatcher { try { - // Получаем список Unused — свежий каждый раз! var unusedCandidates = await templateService.Get() .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused) - .OrderBy(t => t.DateModified ?? t.DateCreated) // старейшие первыми - .Take(10) // небольшой буфер для избежания повторных запросов + .OrderBy(t => t.DateModified ?? t.DateCreated) + .Take(UnusedCandidateBatchSize) .ToListAsync(); if (!unusedCandidates.Any()) { - logger.LogDebug("Нет Unused-шаблонов для JobId {JobId} (попытка {Attempt}).", jobId, attempt); + logger.LogDebug("Нет Unused-шаблонов (попытка {Attempt}).", 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) + if (await templateService.CommitAsync(initiator)) { - logger.LogInformation( - "Успешно захвачен шаблон {TemplateId} для UnitId {UnitId} (попытка {Attempt}). " + - "Статус изменён на Updating.", + logger.LogInformation("Успешно захвачен шаблон {TemplateId} для UnitId {UnitId} (попытка {Attempt}).", candidate.Id, unitId, attempt); - return candidate; } - else - { - logger.LogWarning( - "Commit вернул false при захвате шаблона {TemplateId} (попытка {Attempt}). Пробуем следующего.", - candidate.Id, attempt); - // Откатываем локальные изменения - candidate.StatusTypeId = originalStatus; - candidate.DateModified = originalModified; - } + + // Откат при неудаче + 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); - - // Откатываем + 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); - } + await Task.Delay(Random.Shared.Next(5, 15) * attempt); } catch (Exception ex) { - logger.LogError(ex, "Ошибка в попытке захвата Unused-шаблона (попытка {Attempt}).", attempt); + logger.LogError(ex, "Ошибка в попытке захвата (попытка {Attempt}).", attempt); if (attempt == maxAttempts) throw; } } return null; } + + + private async Task GetJobWithGroupAsync(Guid jobId) + { + return await jobService.Get() + .Include(j => j.Group) + .FirstOrDefaultAsync(j => j.Id == jobId); + } + + + private async Task> GetExpectedUnitIdsAsync(Guid jobId) + { + var units = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId); + return units?.ToHashSet() ?? new HashSet(); + } + + + private async Task GetNormalizedTemplateNameAsync(Job job, Guid unitId) + { + var rawName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, unitId, job.Id); + return rawName.ToUpper(); + } + + + private async Task GetNextRunAsync(Job job, DateTimeOffset? currentNextRun = null) + { + var now = DateTimeOffset.UtcNow; + if (currentNextRun.HasValue && currentNextRun.Value > now) + { + return currentNextRun.Value; + } + + var referenceDate = job.Group?.ReferenceDate ?? now; + return await esppScheduleTransformService.GetNextDateAsync(job.GroupId, referenceDate); + } } } \ No newline at end of file