Files
parr_api/PARR.TemplateMatcher/TemplateMatcher.cs

449 lines
19 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 const int UnusedCandidateBatchSize = 10;
private const bool DefaultUnusedTemplateState = false;
private const bool DefaultUnusedScheduleState = false;
private const bool DefaultUsedTemplateState = false;
private const bool DefaultUsedScheduleState = false;
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);
var expectedUnitIds = await GetExpectedUnitIdsAsync(jobId);
if (!expectedUnitIds.Any())
{
logger.LogInformation("Для JobId {JobId} не найдено Unit'ов по фильтрам.", jobId);
return;
}
var job = await GetJobWithGroupAndAutoControlAsync(jobId);
if (job == null)
{
logger.LogError("Job с Id {JobId} не найден.", jobId);
return;
}
var existingTemplates = await templateService.Get()
.Where(t => t.JobId == jobId)
.ToListAsync();
logger.LogDebug("JobId {JobId}: {Expected} ожидаемых UnitId, {Existing} существующих шаблонов.",
jobId, expectedUnitIds.Count, existingTemplates.Count);
// 4. Шаблоны, НЕ в фильтре → отправляем на деактивацию через worker
var templatesToDeactivate = existingTemplates
.Where(t => !expectedUnitIds.Contains(t.UnitId))
.ToList();
foreach (var template in templatesToDeactivate)
{
if (template.StatusTypeId == TemplateStatusTypeEnum.Updating) continue;
logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация через worker.",
template.Id, template.UnitId);
// Только переводим в Updating — остальное пусть делает worker
template.StatusTypeId = TemplateStatusTypeEnum.Updating;
template.DateModified = DateTimeOffset.UtcNow;
if (!await templateService.CommitAsync(initiator))
{
logger.LogError("Не удалось перевести шаблон {TemplateId} в Updating для деактивации.", template.Id);
continue;
}
var updateRequest = new TemplateUpdaterMq
{
TemplateId = template.Id,
JobId = jobId,
UnitId = template.UnitId,
Name = GetTemplateNameForUnsed(template.Name),
IsActiveTemplate = DefaultUnusedTemplateState,
IsActiveSchedule = DefaultUnusedScheduleState,
LastRun = template.LastRun,
NextRun = template.NextRun,
Index = template.Index,
StatusTypeId = TemplateStatusTypeEnum.Unused,
Initiator = initiator
};
await SendTemplateUpdateMessage(updateRequest);
}
// 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();
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 expectedName = await GetNormalizedTemplateNameAsync(job, unitId);
var nextRun = await GetNextRunAsync(job); // новый шаблон → всегда пересчитываем
var updateRequest = new TemplateUpdaterMq
{
TemplateId = reused.Id,
JobId = jobId,
UnitId = unitId,
Name = expectedName,
IsActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState,
IsActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
NextRun = nextRun
};
await SendTemplateUpdateMessage(updateRequest);
}
else
{
logger.LogInformation("Нет доступных Unused-шаблонов для UnitId {UnitId} → создадим новый.", unitId);
unitIdsToCreateFresh.Add(unitId);
}
}
// 9. Шаблоны В ФИЛЬТРЕ → обновление имени / реактивация
var templatesInFilter = existingTemplates
.Where(t => expectedUnitIds.Contains(t.UnitId))
.ToList();
foreach (var template in templatesInFilter)
{
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}: требуется обновление имени или реактивация.", template.Id);
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 = template.IsActiveTemplate,
IsActiveSchedule = template.IsActiveSchedule,
LastRun = template.LastRun,
NextRun = 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);
var existingTemplates = await templateService.Get()
.Where(t => t.JobId == jobId)
.ToListAsync();
if (!existingTemplates.Any()) return;
var job = await GetJobWithGroupAndAutoControlAsync(jobId);
if (job == null) 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);
bool unitStillInFilter = currentUnitIds.Contains(template.UnitId);
var expectedName = await GetNormalizedTemplateNameAsync(job, template.UnitId);
TemplateStatusTypeEnum targetStatus;
bool targetIsActiveTemplate;
bool targetIsActiveSchedule;
if (unitStillInFilter)
{
targetStatus = TemplateStatusTypeEnum.Used;
targetIsActiveTemplate = template.IsActiveTemplate;
targetIsActiveSchedule = template.IsActiveSchedule;
}
else
{
targetStatus = TemplateStatusTypeEnum.Unused;
targetIsActiveTemplate = DefaultUnusedTemplateState;
targetIsActiveSchedule = DefaultUnusedScheduleState;
expectedName = GetTemplateNameForUnsed(expectedName);
logger.LogInformation("Шаблон {TemplateId} (UnitId {UnitId}) → деактивация.", template.Id, template.UnitId);
}
bool needsUpdate =
template.StatusTypeId != TemplateStatusTypeEnum.Updating &&
(
template.StatusTypeId != targetStatus ||
template.Name != expectedName ||
template.IsActiveTemplate != targetIsActiveTemplate ||
template.IsActiveSchedule != targetIsActiveSchedule
);
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);
}
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
{
var unusedCandidates = await templateService.Get()
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Unused)
.OrderBy(t => t.DateModified ?? t.DateCreated)
.Take(UnusedCandidateBatchSize)
.ToListAsync();
if (!unusedCandidates.Any())
{
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;
if (await templateService.CommitAsync(initiator))
{
logger.LogInformation("Успешно захвачен шаблон {TemplateId} для UnitId {UnitId} (попытка {Attempt}).",
candidate.Id, unitId, attempt);
return candidate;
}
// Откат при неудаче
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)
await Task.Delay(Random.Shared.Next(5, 15) * attempt);
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка в попытке захвата (попытка {Attempt}).", attempt);
if (attempt == maxAttempts) throw;
}
}
return null;
}
private string GetTemplateNameForUnsed(string templateName)
{
return templateName + "_" + DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
private async Task<Job?> GetJobWithGroupAndAutoControlAsync(Guid jobId)
{
return await jobService.Get()
.Include(j => j.Group)
.Include(j => j.AutoControl)
.FirstOrDefaultAsync(j => j.Id == jobId);
}
private async Task<HashSet<Guid>> GetExpectedUnitIdsAsync(Guid jobId)
{
var units = await unitFilterService.GetUnitsIdByJobFilterAsync(jobId);
return units?.ToHashSet() ?? new HashSet<Guid>();
}
private async Task<string> GetNormalizedTemplateNameAsync(Job job, Guid unitId)
{
var rawName = await shortcodesService.ApplyShortcodesAsync(job.TemplateNameMask, unitId, job.Id);
return rawName.ToUpper();
}
private async Task<DateTimeOffset> 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);
}
}
}