feat(templateMatcher):

- добавлен режим DryRun
- изменен подход к фильтрации шаблонов по рабочей группе - перенесено из этапа основной фильтрации в этап посторение подргупп/шаблонов
- при использовании тегов альтернативных рабочих групп фильтрация по списку пропускается
This commit is contained in:
Mikhail Kuznetsov
2026-09-09 17:09:57 +10:00
parent e02a51f5ed
commit 83c6a5f605
10 changed files with 280 additions and 104 deletions

View File

@@ -30,9 +30,9 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
_logger = logger;
}
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator, bool dryRun = false)
{
_logger.LogInformation("Начало синхронизации шаблонов для JobGroup {JobGroupId}", jobGroupId);
_logger.LogInformation("Начало синхронизации шаблонов для JobGroup {JobGroupId} (DryRun={DryRun})", jobGroupId, dryRun);
var existingStatus = await _matchingStatusService.GetStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
if (existingStatus.DetailsJobGroups?.Count > 0)
@@ -41,14 +41,20 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
return;
}
await SetStatusAsync(jobGroupId, "Начало синхронизации");
await SetStatusAsync(jobGroupId, dryRun ? "DryRun: Начало анализа" : "Начало синхронизации");
var totalSw = Stopwatch.StartNew();
try
{
var context = new GroupedSyncContext { JobGroupId = jobGroupId, Initiator = initiator };
var context = new GroupedSyncContext
{
JobGroupId = jobGroupId,
Initiator = initiator,
DryRun = dryRun
};
// Read-этапы выполняются всегда
foreach (var stage in _readStages)
{
var stageSw = Stopwatch.StartNew();
@@ -58,23 +64,34 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
context.JobGroupName, jobGroupId, stage.StageName, stageSw.ElapsedMilliseconds);
}
foreach (var stage in _writeStages)
// Подробный отчёт по результатам read-этапов
PrintDryRunReport(context);
// Write-этапы пропускаем при DryRun
if (!dryRun)
{
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);
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);
await SetStatusAsync(jobGroupId, "Синхронизация завершена успешно");
await _matchingStatusService.DeleteMatchingStatusAsync(jobGroupId, SyncTaskEntityTypeEnum.JobGroup);
_logger.LogInformation("Синхронизация шаблонов завершена для JobGroup '{JobGroupName}' ({JobGroupId})",
context.JobGroupName, jobGroupId);
}
catch (GroupedSyncEarlyExitException ex)
{
@@ -94,6 +111,52 @@ internal class GroupedTemplateSynchronizer : ITemplateSynchronizer
}
}
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.LogInformation(
"[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)
{
_logger.LogWarning("GroupedTemplateSynchronizer: SyncTemplatesForJob вызван для JobId {JobId}. Это не поддерживаемая операция.", jobId);

View File

@@ -76,12 +76,12 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
this._unitRepository = unitRepository;
}
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator)
public async Task SyncTemplatesForJobAsync(Guid jobId, HistoryInitiator initiator, bool dryRun = false)
{
if (jobId == _settingsFromDb.JobIdForUnusedTemplates)
{
_logger.LogInformation("Обработка синхронизации для Job неиспользуемых шаблонов '{JobId}'", jobId);
await SyncUnusedTemplatesAsync(jobId, initiator);
await SyncUnusedTemplatesAsync(jobId, initiator, dryRun);
return;
}
@@ -120,38 +120,95 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds);
}
foreach (var stage in _writeStages)
// Подробный отчёт по результатам read-этапов
PrintDryRunReport(context);
if (!dryRun)
{
var stageSw = Stopwatch.StartNew();
await stage.ExecuteAsync(context);
stageSw.Stop();
_logger.LogDebug("[Perf] Job '{JobName}' ({JobId}) | Этап: {Stage} | Время: {Ms} мс",
context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds);
foreach (var stage in _writeStages)
{
var stageSw = Stopwatch.StartNew();
await stage.ExecuteAsync(context);
stageSw.Stop();
_logger.LogDebug("[Perf] Job '{JobName}' ({JobId}) | Этап: {Stage} | Время: {Ms} мс",
context.JobName, jobId, stage.StageName, stageSw.ElapsedMilliseconds);
}
await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена успешно");
await _matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
_logger.LogInformation("Синхронизация шаблонов завершена для Job '{JobName}' ({JobId})", context.JobName, jobId);
}
else
{
_logger.LogInformation("[DryRun] Write-этапы пропущены. Изменения в БД и MQ не выполнены.");
await UpdateMatchingStatusAsync(jobId, "DryRun: Анализ завершён без изменений");
await _matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
}
totalSw.Stop();
_logger.LogInformation("[Perf] Job '{JobName}' ({JobId}) | ИТОГО: {TotalMs} мс",
context.JobName, jobId, totalSw.ElapsedMilliseconds);
await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена успешно");
await _matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
_logger.LogInformation("Синхронизация шаблонов завершена для Job '{JobName}' ({JobId})",
context.JobName, jobId);
}
catch (Exception ex)
{
totalSw.Stop();
_logger.LogError(ex, "Ошибка при синхронизации Job '{JobName}' ({JobId}) через {ElapsedMs} мс",
string.Empty, jobId, totalSw.ElapsedMilliseconds);
context.JobName ?? string.Empty, jobId, totalSw.ElapsedMilliseconds);
await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}");
throw;
}
}
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
private void PrintDryRunReport(SimpleSyncContext context)
{
_logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId}. Это не поддерживаемая операция.", jobGroupId);
_logger.LogInformation("========== [DryRun] ОТЧЁТ по Job '{JobName}' ({JobId}) ==========",
context.JobName, context.JobId);
_logger.LogInformation("[DryRun] Отфильтровано юнитов: {Count}", context.FilteredUnitIds.Count);
_logger.LogInformation("[DryRun] Существующих активных шаблонов: {Count}", context.ExistingUsedTemplates.Count);
_logger.LogInformation("[DryRun] Новых юнитов (требуют аллокации): {Count}", context.NewUnitIds.Count);
_logger.LogInformation("[DryRun] Шаблонов для деактивации (Unused): {Count}", context.UnusedTemplates.Count);
_logger.LogInformation("[DryRun] Шаблонов для переименования: {Count}", context.TemplatesToRename.Count);
if (context.NewUnitIds.Any())
{
_logger.LogInformation("[DryRun] --- Новые юниты (будут созданы шаблоны) ---");
foreach (var unitId in context.NewUnitIds)
{
_logger.LogInformation("[DryRun] + {Unit}", context.FormatUnit(unitId));
}
}
if (context.UnusedTemplates.Any())
{
_logger.LogInformation("[DryRun] --- Шаблоны для деактивации ---");
foreach (var template in context.UnusedTemplates)
{
_logger.LogInformation("[DryRun] - {TemplateId} (Unit: {Unit}, Name: '{Name}')",
template.Id, context.FormatUnit(template.UnitId), template.Name);
}
}
if (context.TemplatesToRename.Any())
{
_logger.LogInformation("[DryRun] --- Шаблоны для переименования ---");
foreach (var (template, expectedName) in context.TemplatesToRename)
{
_logger.LogInformation("[DryRun] ~ {TemplateId} (Unit: {Unit}): '{OldName}' -> '{NewName}'",
template.Id, context.FormatUnit(template.UnitId), template.Name, expectedName);
}
}
_logger.LogInformation("========== [DryRun] КОНЕЦ ОТЧЁТА ==========");
}
public Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator, bool dryRun = false)
{
_logger.LogWarning(
"SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId} (DryRun={DryRun}). Это не поддерживаемая операция.",
jobGroupId, dryRun);
return Task.CompletedTask;
}
@@ -278,7 +335,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
}
private async Task SyncUnusedTemplatesAsync(Guid unusedJobId, HistoryInitiator initiator, CancellationToken ct = default)
private async Task SyncUnusedTemplatesAsync(Guid unusedJobId, HistoryInitiator initiator, bool dryRun = false, CancellationToken ct = default)
{
// Проверка отмены в самом начале
ct.ThrowIfCancellationRequested();
@@ -467,9 +524,18 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
continue;
}
await SendUpdateRequest(template, unusedJobId, expectedName, initiator, finalUnitId);
_logger.LogDebug("Отправлен запрос на обновление шаблона {TemplateId}. Изменения: Unit={U}, Job={J}, Name={N}",
template.Id, unitChanged, jobChanged, nameChanged);
// Условие DryRun для отправки в MQ
if (!dryRun)
{
await SendUpdateRequest(template, unusedJobId, expectedName, initiator, finalUnitId);
_logger.LogDebug("Отправлен запрос на обновление шаблона {TemplateId}. Изменения: Unit={U}, Job={J}, Name={N}",
template.Id, unitChanged, jobChanged, nameChanged);
}
else
{
_logger.LogInformation("[DryRun] Пропущена отправка запроса на обновление шаблона {TemplateId}. Изменения: Unit={U}, Job={J}, Name={N}, ExpectedName='{Name}'",
template.Id, unitChanged, jobChanged, nameChanged, expectedName);
}
processed++;
}
@@ -482,13 +548,20 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
totalSw.Stop();
totalSw.Stop();
_logger.LogInformation("[Perf] Job '{JobName}' ({JobId}) | ИТОГО: {TotalMs} мс",
unusedJob.Name, unusedJob.Id, totalSw.ElapsedMilliseconds);
await UpdateMatchingStatusAsync(unusedJobId, "Синхронизация неиспользуемых шаблонов завершена");
if (dryRun)
{
await UpdateMatchingStatusAsync(unusedJobId, "DryRun: Анализ неиспользуемых шаблонов завершён без изменений");
}
else
{
await UpdateMatchingStatusAsync(unusedJobId, "Синхронизация неиспользуемых шаблонов завершена");
}
await _matchingStatusService.DeleteMatchingStatusAsync(unusedJobId, SyncTaskEntityTypeEnum.Job);
totalSw.Stop();
_logger.LogInformation(
"Синхронизация неиспользуемых шаблонов завершена. Обработано {Count} шаблонов",
unusedTemplates.Count);