Files
parr_api/PARR.TemplateMatcher/Services/Implementations/GroupedTemplateSynchronizer.cs

192 lines
8.5 KiB
C#

using Microsoft.Extensions.Logging;
using PARR.Core.Services.MatchingStatusService;
using PARR.Domain.Cache.Models;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Exceptions;
using PARR.TemplateMatcher.Services.GroupedSync;
using PARR.TemplateMatcher.Services.Implementations.GroupedSync;
using PARR.TemplateMatcher.Services.Interfaces;
using System.Diagnostics;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
{
private readonly IEnumerable<IGroupedSyncStage> _readStages;
private readonly IEnumerable<IGroupedSyncWriteStage> _writeStages;
private readonly IMatchingStatusService _matchingStatusService;
private readonly ILogger<GroupedTemplateSynchronizer> _logger;
public GroupedTemplateSynchronizer(
IEnumerable<IGroupedSyncStage> readStages,
IEnumerable<IGroupedSyncWriteStage> writeStages,
IMatchingStatusService matchingStatusService,
ILogger<GroupedTemplateSynchronizer> logger
)
{
_readStages = readStages;
_writeStages = writeStages;
_matchingStatusService = matchingStatusService;
_logger = logger;
}
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator, bool dryRun = false, CancellationToken ct = default)
{
_logger.LogInformation("Начало синхронизации шаблонов для JobGroup {JobGroupId} (DryRun={DryRun})", jobGroupId, dryRun);
var existingStatus = await _matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
if (existingStatus.DetailsJobGroups?.Count > 0)
{
_logger.LogWarning("Синхронизация для JobGroup {JobGroupId} уже запущена. Пропускаем.", jobGroupId);
return;
}
await SetStatusAsync(jobGroupId, dryRun ? "DryRun: Начало анализа" : "Начало синхронизации");
var totalSw = Stopwatch.StartNew();
try
{
var context = new GroupedSyncContext
{
JobGroupId = jobGroupId,
Initiator = initiator,
DryRun = dryRun
};
// Read-этапы выполняются всегда
foreach (var stage in _readStages)
{
var stageSw = Stopwatch.StartNew();
await stage.ExecuteAsync(context);
stageSw.Stop();
_logger.LogDebug("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | Этап: {Stage} | Время: {Ms} мс",
context.JobGroupName, jobGroupId, stage.StageName, stageSw.ElapsedMilliseconds);
}
// Подробный отчёт по результатам read-этапов
PrintDryRunReport(context);
// Write-этапы пропускаем при DryRun
if (!dryRun)
{
foreach (var stage in _writeStages)
{
var stageSw = Stopwatch.StartNew();
await stage.ExecuteAsync(context);
stageSw.Stop();
_logger.LogDebug("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | Этап: {Stage} | Время: {Ms} мс",
context.JobGroupName, jobGroupId, stage.StageName, stageSw.ElapsedMilliseconds);
}
await SetStatusAsync(jobGroupId, "Синхронизация завершена успешно");
await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
}
else
{
_logger.LogInformation("[DryRun] Write-этапы пропущены. Изменения в БД и MQ не выполнены.");
await SetStatusAsync(jobGroupId, "DryRun: Анализ завершён без изменений");
await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
}
totalSw.Stop();
_logger.LogInformation("[Perf] JobGroup '{JobGroupName}' ({JobGroupId}) | ИТОГО: {TotalMs} мс",
context.JobGroupName, jobGroupId, totalSw.ElapsedMilliseconds);
}
catch (SyncEarlyExitException ex)
{
totalSw.Stop();
_logger.LogInformation("JobGroup {JobGroupId}: {Reason} ({ElapsedMs} мс)",
jobGroupId, ex.Reason, totalSw.ElapsedMilliseconds);
await SetStatusAsync(jobGroupId, ex.Reason);
await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
}
catch (Exception ex)
{
totalSw.Stop();
_logger.LogError(ex, "Ошибка при синхронизации JobGroup {JobGroupId} через {ElapsedMs} мс",
jobGroupId, totalSw.ElapsedMilliseconds);
await SetStatusAsync(jobGroupId, $"Ошибка: {ex.Message}");
throw;
}
}
private void PrintDryRunReport(GroupedSyncContext context)
{
_logger.LogInformation("========== [DryRun] ОТЧЁТ по JobGroup '{JobGroupName}' ({JobGroupId}) ==========",
context.JobGroupName, context.JobGroupId);
_logger.LogInformation("[DryRun] Отфильтровано юнитов: {Count}", context.FilteredUnits.Count);
_logger.LogInformation("[DryRun] Разрешено конфликтов (potentialUnits): {Count}", context.ReverseMapping.Count);
_logger.LogInformation("[DryRun] Сформировано групп: {Count}", context.TemplateGroups.Count);
int totalSubGroups = 0;
int totalUnitsInTemplates = 0;
foreach (var group in context.TemplateGroups.OrderBy(g => g.PotentialUnitId))
{
_logger.LogInformation(
"[DryRun] PotentialUnit {Unit}: {SubGroupCount} подгрупп",
context.FormatUnit(group.PotentialUnitId),
group.SubGroups.Count);
foreach (var subGroup in group.SubGroups.OrderBy(s => s.GlobalIndex))
{
_logger.LogInformation(
"[DryRun] Index={Index}, группа='{Group}', экземпляров: {Count}",
subGroup.GlobalIndex,
subGroup.InnerGroupName,
subGroup.Entries.Count);
foreach (var entry in subGroup.Entries)
{
_logger.LogDebug(
"[DryRun] - {Unit} (ValueId: {ValueId})",
context.FormatUnit(entry.UnitId),
entry.UnitFieldValueId);
}
totalSubGroups++;
totalUnitsInTemplates += subGroup.Entries.Count;
}
}
_logger.LogInformation("[DryRun] ИТОГО: {GroupCount} групп, {SubGroupCount} подгрупп (=шаблонов), {UnitCount} экземпляров в шаблонах",
context.TemplateGroups.Count, totalSubGroups, totalUnitsInTemplates);
_logger.LogInformation("========== [DryRun] КОНЕЦ ОТЧЁТА ==========");
}
public Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator, bool dryRun = false, CancellationToken ct = default)
{
_logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
return Task.CompletedTask;
}
public Task UpdateTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator, CancellationToken ct = default)
{
_logger.LogWarning("GroupedTemplateSynchronizer: UpdateTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);
return Task.CompletedTask;
}
private async Task SetStatusAsync(Guid jobGroupId, string comment)
{
var status = new MatchingStatusItemDto
{
DateStart = DateTimeOffset.UtcNow,
Action = TemplateMatcherActionEnum.Sync,
Comment = comment
};
await _matchingStatusService.SetMatchingStatusAsync(
jobGroupId, SyncTaskEntityTypeEnum.JobGroup,
new MatchingStatusItem { Data = status, Timestamp = DateTimeOffset.UtcNow, Source = nameof(GroupedTemplateSynchronizer) },
TimeSpan.FromMinutes(30));
}
}