feat(templateMatcher): Для синхронизации негруппированных работ добавлены метрики для мониторинга основных этапов.
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Moq;
|
using Moq;
|
||||||
using PARR.Core.Common.Interfaces;
|
using PARR.Core.Common.Interfaces;
|
||||||
using PARR.Core.Repositories.Interfaces.Job;
|
using PARR.Core.Repositories.Interfaces.Job;
|
||||||
using PARR.Core.Repositories.Interfaces.Unit;
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
|
using PARR.Core.Services.UnitFilterService;
|
||||||
|
using PARR.Core.Services.UnitFilterService.Matchers.Interfaces;
|
||||||
|
using PARR.Core.Services.UnitFilterService.Models;
|
||||||
|
using PARR.Core.Services.UnitService.Interfaces;
|
||||||
using PARR.DAL.Context;
|
using PARR.DAL.Context;
|
||||||
using PARR.Domain.Cache.Models;
|
using PARR.Domain.Cache.Models;
|
||||||
using PARR.Domain.Entities.Job;
|
using PARR.Domain.Entities.Job;
|
||||||
@@ -98,6 +101,13 @@ public class UnitFilterServiceTests
|
|||||||
public Mock<IUnitFieldRepository> UnitField { get; set; }
|
public Mock<IUnitFieldRepository> UnitField { get; set; }
|
||||||
public Mock<IJobRepository> Job { get; set; }
|
public Mock<IJobRepository> Job { get; set; }
|
||||||
public Mock<IRedisCacheService> Cache { get; set; }
|
public Mock<IRedisCacheService> Cache { get; set; }
|
||||||
|
// Новые зависимости после рефакторинга
|
||||||
|
public Mock<IUnitService> UnitService { get; set; }
|
||||||
|
public Mock<IUnitFieldMatcher> FieldMatcher { get; set; }
|
||||||
|
public Mock<IUnitRelationshipMatcher> RelationshipMatcher { get; set; }
|
||||||
|
public Mock<IUmbrellaFilter> UmbrellaFilter { get; set; }
|
||||||
|
public Mock<IUnitFilterResultLoader> ResultLoader { get; set; }
|
||||||
|
public Mock<IUnitNameResolver> NameResolver { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -137,6 +147,56 @@ public class UnitFilterServiceTests
|
|||||||
cacheMock.Setup(r => r.GetCachedDataAsync<UnitFilterIds>(It.IsAny<string>(), It.IsAny<bool>()))
|
cacheMock.Setup(r => r.GetCachedDataAsync<UnitFilterIds>(It.IsAny<string>(), It.IsAny<bool>()))
|
||||||
.ReturnsAsync((UnitFilterIds)null!);
|
.ReturnsAsync((UnitFilterIds)null!);
|
||||||
|
|
||||||
|
var unitServiceMock = new Mock<IUnitService>();
|
||||||
|
var fieldMatcherMock = new Mock<IUnitFieldMatcher>();
|
||||||
|
var relationshipMatcherMock = new Mock<IUnitRelationshipMatcher>();
|
||||||
|
var umbrellaFilterMock = new Mock<IUmbrellaFilter>();
|
||||||
|
var resultLoaderMock = new Mock<IUnitFilterResultLoader>();
|
||||||
|
var nameResolverMock = new Mock<IUnitNameResolver>();
|
||||||
|
|
||||||
|
// Настройка NameResolver: возвращаем все ID юнитов из контекста (эмуляция кэш-промаха + БД)
|
||||||
|
nameResolverMock.Setup(r => r.ResolveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync((string mask, CancellationToken ct) =>
|
||||||
|
context.Units.Select(u => u.Id).ToList());
|
||||||
|
|
||||||
|
// Настройка FieldMatcher: эмуляция SQL-фильтрации через InMemory-контекст
|
||||||
|
fieldMatcherMock.Setup(r => r.MatchAsync(It.IsAny<IReadOnlyList<Guid>>(), It.IsAny<IEnumerable<JobFieldFilter>>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync((IReadOnlyList<Guid> ids, IEnumerable<JobFieldFilter> filters, CancellationToken ct) =>
|
||||||
|
ids.ToList());
|
||||||
|
|
||||||
|
// Настройка RelationshipMatcher: возвращаем контексты без изменений
|
||||||
|
relationshipMatcherMock.Setup(r => r.MatchAsync(It.IsAny<IReadOnlyList<Guid>>(), It.IsAny<IEnumerable<JobRelationshipFilter>>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync((IReadOnlyList<Guid> ids, IEnumerable<JobRelationshipFilter> filters, CancellationToken ct) =>
|
||||||
|
ids.Select(id => new UnitFilterMatchResult { UnitId = id }).ToList());
|
||||||
|
|
||||||
|
// Настройка UmbrellaFilter: пропускаем без изменений
|
||||||
|
umbrellaFilterMock.Setup(r => r.Apply(It.IsAny<List<UnitFilterMatchResult>>(), It.IsAny<Job>()))
|
||||||
|
.Returns((List<UnitFilterMatchResult> ctx, Job j) => ctx);
|
||||||
|
|
||||||
|
// Настройка ResultLoader: формируем DTO из контекста
|
||||||
|
resultLoaderMock.Setup(r => r.LoadAsync(It.IsAny<List<UnitFilterMatchResult>>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync((List<UnitFilterMatchResult> contexts, CancellationToken ct) =>
|
||||||
|
{
|
||||||
|
var unitIds = contexts.Select(c => c.UnitId).ToHashSet();
|
||||||
|
var units = context.Units.Where(u => unitIds.Contains(u.Id)).ToList();
|
||||||
|
return units.Select(u => new UnitFilterResultDto
|
||||||
|
{
|
||||||
|
Id = u.Id,
|
||||||
|
Name = u.Name,
|
||||||
|
Values = u.UnitValues?.Select(v => new UnitValueDto
|
||||||
|
{
|
||||||
|
FieldId = v.FieldId,
|
||||||
|
Value = v.Value?.Value
|
||||||
|
}).ToList() ?? new List<UnitValueDto>(),
|
||||||
|
Parents = contexts.First(c => c.UnitId == u.Id).ValidParentIds
|
||||||
|
.Select(pid => new RelatedUnitDto { UnitId = pid })
|
||||||
|
.ToList(),
|
||||||
|
Children = contexts.First(c => c.UnitId == u.Id).ValidChildIds
|
||||||
|
.Select(cid => new RelatedUnitDto { UnitId = cid })
|
||||||
|
.ToList()
|
||||||
|
}).ToList();
|
||||||
|
});
|
||||||
|
|
||||||
return new RepositoryMocks
|
return new RepositoryMocks
|
||||||
{
|
{
|
||||||
Unit = unitRepoMock,
|
Unit = unitRepoMock,
|
||||||
@@ -144,7 +204,13 @@ public class UnitFilterServiceTests
|
|||||||
UnitInValue = unitInValueRepoMock,
|
UnitInValue = unitInValueRepoMock,
|
||||||
UnitField = unitFieldRepoMock,
|
UnitField = unitFieldRepoMock,
|
||||||
Job = jobRepoMock,
|
Job = jobRepoMock,
|
||||||
Cache = cacheMock
|
Cache = cacheMock,
|
||||||
|
UnitService = unitServiceMock,
|
||||||
|
FieldMatcher = fieldMatcherMock,
|
||||||
|
RelationshipMatcher = relationshipMatcherMock,
|
||||||
|
UmbrellaFilter = umbrellaFilterMock,
|
||||||
|
ResultLoader = resultLoaderMock,
|
||||||
|
NameResolver = nameResolverMock
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,12 +220,10 @@ public class UnitFilterServiceTests
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="mocks">Подготовленные моки репозиториев</param>
|
/// <param name="mocks">Подготовленные моки репозиториев</param>
|
||||||
/// <param name="logger">Экземпляр логгера для сервиса</param>
|
/// <param name="logger">Экземпляр логгера для сервиса</param>
|
||||||
private static PARR.Core.Services.UnitFilterService.UnitFilterService CreateSut(
|
private static Core.Services.UnitFilterService.UnitFilterService CreateSut(
|
||||||
RepositoryMocks mocks,
|
RepositoryMocks mocks,
|
||||||
ILogger<PARR.Core.Services.UnitFilterService.UnitFilterService> logger)
|
ILogger<Core.Services.UnitFilterService.UnitFilterService> logger)
|
||||||
{
|
{
|
||||||
var serviceOptions = Options.Create(new PARR.Core.Services.UnitFilterService.Models.UnitFilterServiceOptions { LoadBatchSize = 100 });
|
|
||||||
|
|
||||||
return new PARR.Core.Services.UnitFilterService.UnitFilterService(
|
return new PARR.Core.Services.UnitFilterService.UnitFilterService(
|
||||||
logger,
|
logger,
|
||||||
mocks.Job.Object,
|
mocks.Job.Object,
|
||||||
@@ -167,8 +231,13 @@ public class UnitFilterServiceTests
|
|||||||
mocks.UnitInUnit.Object,
|
mocks.UnitInUnit.Object,
|
||||||
mocks.UnitInValue.Object,
|
mocks.UnitInValue.Object,
|
||||||
mocks.Cache.Object,
|
mocks.Cache.Object,
|
||||||
serviceOptions,
|
mocks.UnitField.Object,
|
||||||
mocks.UnitField.Object
|
mocks.UnitService.Object,
|
||||||
|
mocks.FieldMatcher.Object,
|
||||||
|
mocks.RelationshipMatcher.Object,
|
||||||
|
mocks.UmbrellaFilter.Object,
|
||||||
|
mocks.ResultLoader.Object,
|
||||||
|
mocks.NameResolver.Object
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +333,7 @@ public class UnitFilterServiceTests
|
|||||||
mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(fieldId, It.IsAny<string>()))
|
mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(fieldId, It.IsAny<string>()))
|
||||||
.Returns(context.Units.Where(u => u.Id == validParentId).Select(u => u.Id).AsQueryable());
|
.Returns(context.Units.Where(u => u.Id == validParentId).Select(u => u.Id).AsQueryable());
|
||||||
|
|
||||||
var logger = NullLoggerFactory.Instance.CreateLogger<PARR.Core.Services.UnitFilterService.UnitFilterService>();
|
var logger = NullLoggerFactory.Instance.CreateLogger<Core.Services.UnitFilterService.UnitFilterService>();
|
||||||
var service = CreateSut(mocks, logger);
|
var service = CreateSut(mocks, logger);
|
||||||
|
|
||||||
// === Act ===
|
// === Act ===
|
||||||
@@ -376,7 +445,7 @@ public class UnitFilterServiceTests
|
|||||||
.Distinct()
|
.Distinct()
|
||||||
.AsQueryable());
|
.AsQueryable());
|
||||||
|
|
||||||
var logger = NullLoggerFactory.Instance.CreateLogger<PARR.Core.Services.UnitFilterService.UnitFilterService>();
|
var logger = NullLoggerFactory.Instance.CreateLogger<Core.Services.UnitFilterService.UnitFilterService>();
|
||||||
var service = CreateSut(mocks, logger);
|
var service = CreateSut(mocks, logger);
|
||||||
|
|
||||||
// === Act ===
|
// === Act ===
|
||||||
@@ -481,7 +550,7 @@ public class UnitFilterServiceTests
|
|||||||
.Distinct()
|
.Distinct()
|
||||||
.AsQueryable());
|
.AsQueryable());
|
||||||
|
|
||||||
var logger = NullLoggerFactory.Instance.CreateLogger<PARR.Core.Services.UnitFilterService.UnitFilterService>();
|
var logger = NullLoggerFactory.Instance.CreateLogger<Core.Services.UnitFilterService.UnitFilterService>();
|
||||||
var service = CreateSut(mocks, logger);
|
var service = CreateSut(mocks, logger);
|
||||||
|
|
||||||
// === Act ===
|
// === Act ===
|
||||||
@@ -593,7 +662,7 @@ public class UnitFilterServiceTests
|
|||||||
.Distinct()
|
.Distinct()
|
||||||
.AsQueryable());
|
.AsQueryable());
|
||||||
|
|
||||||
var logger = NullLoggerFactory.Instance.CreateLogger<PARR.Core.Services.UnitFilterService.UnitFilterService>();
|
var logger = NullLoggerFactory.Instance.CreateLogger<Core.Services.UnitFilterService.UnitFilterService>();
|
||||||
var service = CreateSut(mocks, logger);
|
var service = CreateSut(mocks, logger);
|
||||||
|
|
||||||
// === Act ===
|
// === Act ===
|
||||||
|
|||||||
@@ -18,4 +18,11 @@
|
|||||||
</Reference>
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Разрешаем тестовой сборке видеть internal классы PARR.Core -->
|
||||||
|
<InternalsVisibleTo Include="PARR.Test" />
|
||||||
|
<!-- Разрешаем Castle DynamicProxy (Moq / NSubstitute) видеть internal классы PARR.Core -->
|
||||||
|
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ using PARR.Domain.Settings;
|
|||||||
using PARR.TemplateMatcher.Models;
|
using PARR.TemplateMatcher.Models;
|
||||||
using PARR.TemplateMatcher.Services.Interfaces;
|
using PARR.TemplateMatcher.Services.Interfaces;
|
||||||
using PARR.TemplateMatcher.Settings;
|
using PARR.TemplateMatcher.Settings;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace PARR.TemplateMatcher.Services.Implementations;
|
namespace PARR.TemplateMatcher.Services.Implementations;
|
||||||
|
|
||||||
@@ -99,8 +100,8 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Обычная логика для всех остальных Job ===
|
var totalSw = Stopwatch.StartNew();
|
||||||
logger.LogDebug("Начало синхронизации шаблонов для Job {JobId}", jobId);
|
logger.LogInformation("Начало синхронизации шаблонов для Job {JobId}", jobId);
|
||||||
|
|
||||||
// === Проверка: уже запущена? ===
|
// === Проверка: уже запущена? ===
|
||||||
var existingStatus = await matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
|
var existingStatus = await matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
|
||||||
@@ -126,6 +127,8 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// === ЭТАП 1: Загрузка Job ===
|
||||||
|
var stageSw = Stopwatch.StartNew();
|
||||||
var job = await jobService.Get()
|
var job = await jobService.Get()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(j => j.AutoControl)
|
.Include(j => j.AutoControl)
|
||||||
@@ -142,48 +145,22 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
await UpdateMatchingStatusAsync(jobId, "Job не найден");
|
await UpdateMatchingStatusAsync(jobId, "Job не найден");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
stageSw.Stop();
|
||||||
|
logger.LogDebug("[Perf] Job {JobId} | Этап: Загрузка Job | Время: {Ms} мс",
|
||||||
|
jobId, stageSw.ElapsedMilliseconds);
|
||||||
|
|
||||||
// === Получение отфильтрованных юнитов с полной информацией ===
|
// === ЭТАП 2: Фильтрация юнитов ===
|
||||||
|
stageSw.Restart();
|
||||||
var filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(jobId);
|
var filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(jobId);
|
||||||
if (filteredUnits == null || !filteredUnits.Any())
|
stageSw.Stop();
|
||||||
{
|
var filterCount = filteredUnits?.Count() ?? 0;
|
||||||
logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId);
|
logger.LogDebug("[Perf] Job {JobId} | Этап: Фильтрация юнитов | Время: {Ms} мс | Результат: {Count}",
|
||||||
|
jobId, stageSw.ElapsedMilliseconds, filterCount);
|
||||||
|
|
||||||
var existingTemplatesForDeactivation = await templateService.Get()
|
var unitIds = filteredUnits?.Select(u => u.Id).ToHashSet() ?? new HashSet<Guid>();
|
||||||
.AsNoTracking()
|
|
||||||
.Include(t => t.UnitsInTemplate)
|
|
||||||
.Where(t => t.JobId == jobId && t.StatusTypeId == TemplateStatusTypeEnum.Used)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
await UpdateMatchingStatusAsync(jobId, $"Нет Unit'ов. Деактивация {existingTemplatesForDeactivation.Count} шаблонов...");
|
|
||||||
|
|
||||||
foreach (var unusedTemplate in existingTemplatesForDeactivation)
|
|
||||||
{
|
|
||||||
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId);
|
|
||||||
await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator);
|
|
||||||
}
|
|
||||||
|
|
||||||
await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена: нет Unit'ов");
|
|
||||||
await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
|
|
||||||
logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Извлекаем ID юнитов для последующих операций
|
|
||||||
var unitIds = filteredUnits.Select(u => u.Id).ToList();
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
// Отладка: проверить, есть ли юнит в unitIds
|
|
||||||
if (unitIds.Contains(targetUnitId))
|
|
||||||
{
|
|
||||||
logger.LogDebug("Юнит {TargetUnitId} найден в unitIds.", targetUnitId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
logger.LogDebug("Юнит {TargetUnitId} НЕ найден в unitIds.", targetUnitId);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
// === ЭТАП 3: Загрузка существующих шаблонов ===
|
||||||
|
stageSw.Restart();
|
||||||
var existingTemplates = await templateService.Get()
|
var existingTemplates = await templateService.Get()
|
||||||
.Include(t => t.UnitsInTemplate)
|
.Include(t => t.UnitsInTemplate)
|
||||||
.Include(t => t.Job)
|
.Include(t => t.Job)
|
||||||
@@ -199,25 +176,48 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used)
|
.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var existingUnitIds = existingUsedTemplates
|
var existingUnitIds = existingUsedTemplates.Select(t => t.UnitId).ToHashSet();
|
||||||
.Select(t => t.UnitId)
|
stageSw.Stop();
|
||||||
.ToHashSet();
|
logger.LogDebug("[Perf] Job {JobId} | Этап: Загрузка шаблонов | Время: {Ms} мс | Используется: {Count}",
|
||||||
|
jobId, stageSw.ElapsedMilliseconds, existingUsedTemplates.Count);
|
||||||
|
|
||||||
|
// === ЭТАП 4: Расчёт диффа (создание / деактивация / переименование) ===
|
||||||
|
stageSw.Restart();
|
||||||
|
|
||||||
var newUnitIds = unitIds.Except(existingUnitIds).ToList();
|
var newUnitIds = unitIds.Except(existingUnitIds).ToList();
|
||||||
var unusedTemplates = existingUsedTemplates
|
var unusedTemplates = existingUsedTemplates
|
||||||
.Where(t => !unitIds.Contains(t.UnitId))
|
.Where(t => !unitIds.Contains(t.UnitId))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
// Проверка имён существующих шаблонов
|
||||||
|
var templatesToRename = new List<(Template Template, string ExpectedName)>();
|
||||||
|
foreach (var template in existingUsedTemplates)
|
||||||
|
{
|
||||||
|
if (!unitIds.Contains(template.UnitId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template);
|
||||||
|
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
templatesToRename.Add((template, expectedName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stageSw.Stop();
|
||||||
|
logger.LogDebug("[Perf] Job {JobId} | Этап: Расчёт диффа | Время: {Ms} мс | Создать: {Create}, Деактивировать: {Deactivate}, Переименовать: {Rename}",
|
||||||
|
jobId, stageSw.ElapsedMilliseconds, newUnitIds.Count, unusedTemplates.Count, templatesToRename.Count);
|
||||||
|
|
||||||
|
// === ЭТАП 5: Создание новых шаблонов ===
|
||||||
|
stageSw.Restart();
|
||||||
foreach (var unitId in newUnitIds)
|
foreach (var unitId in newUnitIds)
|
||||||
{
|
{
|
||||||
// Вычисляем флаги активности из конфигурации автоконтрола
|
|
||||||
var isActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState;
|
var isActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState;
|
||||||
var isActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState;
|
var isActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState;
|
||||||
|
|
||||||
var request = new TemplateAllocationRequest(
|
var request = new TemplateAllocationRequest(
|
||||||
TargetJob: job,
|
TargetJob: job,
|
||||||
TargetUnitId: unitId,
|
TargetUnitId: unitId,
|
||||||
TargetUnit: null, // Для простого шаблона не критично, нормализатор загрузит при необходимости
|
TargetUnit: null,
|
||||||
Index: null,
|
Index: null,
|
||||||
UnitsInTemplate: new List<UnitInTemplateMessage>(),
|
UnitsInTemplate: new List<UnitInTemplateMessage>(),
|
||||||
IsActiveTemplate: isActiveTemplate,
|
IsActiveTemplate: isActiveTemplate,
|
||||||
@@ -226,17 +226,16 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
|
|
||||||
await templateAllocationService.AllocateAsync(request);
|
await templateAllocationService.AllocateAsync(request);
|
||||||
}
|
}
|
||||||
|
stageSw.Stop();
|
||||||
|
logger.LogDebug("[Perf] Job {JobId} | Этап: Создание шаблонов | Время: {Ms} мс | Количество: {Count}",
|
||||||
|
jobId, stageSw.ElapsedMilliseconds, newUnitIds.Count);
|
||||||
|
|
||||||
// === Обработка существующих шаблонов (проверка имени) ===
|
// === ЭТАП 6: Обновление имён существующих шаблонов ===
|
||||||
foreach (var template in existingUsedTemplates)
|
stageSw.Restart();
|
||||||
|
foreach (var (template, expectedName) in templatesToRename)
|
||||||
{
|
{
|
||||||
if (unitIds.Contains(template.UnitId))
|
logger.LogDebug("Шаблон {TemplateId} требует обновления имени: '{OldName}' → '{NewName}'",
|
||||||
{
|
template.Id, template.Name, expectedName);
|
||||||
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template);
|
|
||||||
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", template.Id, template.Name, expectedName);
|
|
||||||
|
|
||||||
|
|
||||||
var updateRequest = new TemplateUpdaterMessage
|
var updateRequest = new TemplateUpdaterMessage
|
||||||
{
|
{
|
||||||
@@ -250,34 +249,45 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
|
|||||||
Index = template.Index,
|
Index = template.Index,
|
||||||
StatusTypeId = TemplateStatusTypeEnum.Used,
|
StatusTypeId = TemplateStatusTypeEnum.Used,
|
||||||
Initiator = initiator,
|
Initiator = initiator,
|
||||||
UnitsInTemplate = new List<UnitInTemplateMessage>() // для простого шаблона
|
UnitsInTemplate = new List<UnitInTemplateMessage>()
|
||||||
};
|
};
|
||||||
|
|
||||||
await templateMqPublisher.PublishUpdateAsync(updateRequest);
|
await templateMqPublisher.PublishUpdateAsync(updateRequest);
|
||||||
}
|
}
|
||||||
}
|
stageSw.Stop();
|
||||||
}
|
logger.LogDebug("[Perf] Job {JobId} | Этап: Обновление имён | Время: {Ms} мс | Количество: {Count}",
|
||||||
|
jobId, stageSw.ElapsedMilliseconds, templatesToRename.Count);
|
||||||
|
|
||||||
// === Деактивация лишних шаблонов ===
|
// === ЭТАП 7: Деактивация лишних шаблонов ===
|
||||||
|
stageSw.Restart();
|
||||||
foreach (var unusedTemplate in unusedTemplates)
|
foreach (var unusedTemplate in unusedTemplates)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).", unusedTemplate.Id, jobId, unusedTemplate.UnitId);
|
logger.LogInformation("Деактивация лишнего шаблона {TemplateId} (Job {JobId}, UnitId {UnitId}).",
|
||||||
|
unusedTemplate.Id, jobId, unusedTemplate.UnitId);
|
||||||
await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator);
|
await templateDeactivator.DeactivateTemplateAsync(unusedTemplate, initiator);
|
||||||
}
|
}
|
||||||
|
stageSw.Stop();
|
||||||
|
logger.LogDebug("[Perf] Job {JobId} | Этап: Деактивация | Время: {Ms} мс | Количество: {Count}",
|
||||||
|
jobId, stageSw.ElapsedMilliseconds, unusedTemplates.Count);
|
||||||
|
|
||||||
|
// === ИТОГО ===
|
||||||
|
totalSw.Stop();
|
||||||
|
logger.LogInformation("[Perf] Job {JobId} | ИТОГО: {TotalMs} мс", jobId, totalSw.ElapsedMilliseconds);
|
||||||
|
|
||||||
// === Успешное завершение ===
|
|
||||||
await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена успешно");
|
await UpdateMatchingStatusAsync(jobId, "Синхронизация завершена успешно");
|
||||||
await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
|
await matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job);
|
||||||
logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId);
|
logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Ошибка при синхронизации Job {JobId}", jobId);
|
totalSw.Stop();
|
||||||
|
logger.LogError(ex, "Ошибка при синхронизации Job {JobId} через {ElapsedMs} мс", jobId, totalSw.ElapsedMilliseconds);
|
||||||
await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}");
|
await UpdateMatchingStatusAsync(jobId, $"Ошибка: {ex.Message}");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
|
public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator)
|
||||||
{
|
{
|
||||||
logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId}. Это не поддерживаемая операция.", jobGroupId);
|
logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId}. Это не поддерживаемая операция.", jobGroupId);
|
||||||
|
|||||||
Reference in New Issue
Block a user