Files
parr_api/PARR.TemplateMatcher/TemplateMatcher.cs

416 lines
20 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 Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.BLL.Domain.Mq;
using PARR.BLL.Services.Interfaces;
using PARR.Common.Domain;
using PARR.Constants;
using PARR.DAL.DomainServices.Interfaces;
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;
namespace PARR.TemplateMatcher
{
internal class TemplateMatcher : ITemplateMatcher
{
private readonly ILogger<TemplateMatcher> logger;
private readonly IUnitFilterService unitFilterService;
private readonly MqSettings mqSettings;
private readonly IMqService mqService;
private readonly ITemplateService templateService;
private readonly IJobService jobService;
private readonly IShortcodesService shortcodesService;
private readonly IEsppScheduleTransformService esppScheduleTransformService;
public TemplateMatcher(
ILogger<TemplateMatcher> logger,
IUnitFilterService unitFilterService,
MqSettings mqSettings,
IMqService mqService,
ITemplateService templateService,
IJobService jobService,
IShortcodesService shortcodesService,
IEsppScheduleTransformService esppScheduleTransformService
)
{
this.logger = logger;
this.unitFilterService = unitFilterService;
this.mqSettings = mqSettings;
this.mqService = mqService;
this.templateService = templateService;
this.jobService = jobService;
this.shortcodesService = shortcodesService;
this.esppScheduleTransformService = esppScheduleTransformService;
}
public async Task SyncTemplatesForJob(Guid jobId, HistoryInitiator initiator)
{
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();
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);
if (job == null)
{
logger.LogError("Job с Id {JobId} не найден.", jobId);
return;
}
// 3. Получаем ВСЕ шаблоны для Job
var existingTemplates = await templateService.Get()
.Where(t => t.JobId == jobId)
.ToListAsync();
logger.LogDebug("JobId {JobId}: {Expected} ожидаемых UnitId, {Existing} существующих шаблонов.",
jobId, expectedUnitIds.Count, existingTemplates.Count);
// 4. Деактивация: шаблоны, НЕ в фильтре → Unused
var templatesToDeactivate = existingTemplates
.Where(t => !expectedUnitIds.Contains(t.UnitId))
.ToList();
foreach (var template in templatesToDeactivate)
{
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<Guid>();
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,
UnitId = unitId,
HistoryInitiator = initiator
};
var msg = JsonSerializer.Serialize(mqRequest);
var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new[] { msg });
if (!result.IsSuccess)
logger.LogError("Ошибка отправки команды создания шаблона для UnitId {UnitId}.", unitId);
}
logger.LogInformation("Синхронизация завершена для JobId {JobId}.", jobId);
}
public async Task UpdateTemplatesForJob(Guid jobId, HistoryInitiator initiator)
{
logger.LogDebug("Начало обновления имён шаблонов для JobId {JobId}", jobId);
// 1. Получаем все шаблоны для этого Job
var existingTemplates = await templateService.Get()
.Where(t => t.JobId == jobId)
.ToListAsync();
logger.LogDebug("Для JobId {JobId} найдено {Count} существующих шаблонов.", jobId, existingTemplates.Count());
if (!existingTemplates.Any())
{
logger.LogInformation("Для JobId {JobId} не найдено существующих шаблонов для обновления имени.", jobId);
return;
}
// 2. Получаем Job, чтобы получить маску имени
var job = await jobService.GetAsync(jobId);
if (job == null)
{
logger.LogError("Job с Id {JobId} не найден.", jobId);
return;
}
foreach (var template in existingTemplates)
{
logger.LogDebug("Проверяем имя шаблона {TemplateId} для UnitId {UnitId}.", template.Id, template.UnitId);
// Проверяем, нужно ли обновить имя
await CheckAndSendNameUpdateIfRequired(template, job, initiator);
}
logger.LogDebug("Окончание обновления имён шаблонов для JobId {JobId}", jobId);
}
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)
{
logger.LogError("Не удалось сохранить изменения шаблона {TemplateId} перед отправкой в очередь обновления.", template.Id);
return; // не отправляем сообщение, если не сохранили
}
// 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
};
// 4. Отправляем в очередь
await SendTemplateUpdateMessage(updateRequest);
}
else
{
logger.LogDebug("Шаблон {TemplateId} для UnitId {UnitId} имеет актуальное имя.", template.Id, template.UnitId);
}
}
private async Task SendTemplateUpdateMessage(TemplateUpdaterMq updateRequest)
{
logger.LogDebug("Отправка сообщения в очередь '{Queue}' для шаблона {TemplateId}",
mqSettings.TemplateUpdater.QueueName, updateRequest.TemplateId);
var msg = JsonSerializer.Serialize(updateRequest);
var result = await mqService.SendAsync(mqSettings.TemplateUpdater, new[] { msg });
if (result.IsSuccess)
{
logger.LogInformation("Отправлен запрос на обновление шаблона {TemplateId}", updateRequest.TemplateId);
}
else
{
logger.LogError("Ошибка при отправке запроса на обновление шаблона {TemplateId}: {Message}",
updateRequest.TemplateId, msg);
}
}
private async Task<Template?> 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;
}
}
}