diff --git a/PARR.DAL.Tests/Services/UnitFilterService/UnitFilterServiceTests.cs b/PARR.DAL.Tests/Services/UnitFilterService/UnitFilterServiceTests.cs index 3dbf3c27..c41aa4af 100644 --- a/PARR.DAL.Tests/Services/UnitFilterService/UnitFilterServiceTests.cs +++ b/PARR.DAL.Tests/Services/UnitFilterService/UnitFilterServiceTests.cs @@ -1,11 +1,14 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; using Moq; using PARR.Core.Common.Interfaces; using PARR.Core.Repositories.Interfaces.Job; 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.Domain.Cache.Models; using PARR.Domain.Entities.Job; @@ -98,6 +101,13 @@ public class UnitFilterServiceTests public Mock UnitField { get; set; } public Mock Job { get; set; } public Mock Cache { get; set; } + // Новые зависимости после рефакторинга + public Mock UnitService { get; set; } + public Mock FieldMatcher { get; set; } + public Mock RelationshipMatcher { get; set; } + public Mock UmbrellaFilter { get; set; } + public Mock ResultLoader { get; set; } + public Mock NameResolver { get; set; } } /// @@ -137,6 +147,56 @@ public class UnitFilterServiceTests cacheMock.Setup(r => r.GetCachedDataAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((UnitFilterIds)null!); + var unitServiceMock = new Mock(); + var fieldMatcherMock = new Mock(); + var relationshipMatcherMock = new Mock(); + var umbrellaFilterMock = new Mock(); + var resultLoaderMock = new Mock(); + var nameResolverMock = new Mock(); + + // Настройка NameResolver: возвращаем все ID юнитов из контекста (эмуляция кэш-промаха + БД) + nameResolverMock.Setup(r => r.ResolveAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((string mask, CancellationToken ct) => + context.Units.Select(u => u.Id).ToList()); + + // Настройка FieldMatcher: эмуляция SQL-фильтрации через InMemory-контекст + fieldMatcherMock.Setup(r => r.MatchAsync(It.IsAny>(), It.IsAny>(), It.IsAny())) + .ReturnsAsync((IReadOnlyList ids, IEnumerable filters, CancellationToken ct) => + ids.ToList()); + + // Настройка RelationshipMatcher: возвращаем контексты без изменений + relationshipMatcherMock.Setup(r => r.MatchAsync(It.IsAny>(), It.IsAny>(), It.IsAny())) + .ReturnsAsync((IReadOnlyList ids, IEnumerable filters, CancellationToken ct) => + ids.Select(id => new UnitFilterMatchResult { UnitId = id }).ToList()); + + // Настройка UmbrellaFilter: пропускаем без изменений + umbrellaFilterMock.Setup(r => r.Apply(It.IsAny>(), It.IsAny())) + .Returns((List ctx, Job j) => ctx); + + // Настройка ResultLoader: формируем DTO из контекста + resultLoaderMock.Setup(r => r.LoadAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync((List 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(), + 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 { Unit = unitRepoMock, @@ -144,7 +204,13 @@ public class UnitFilterServiceTests UnitInValue = unitInValueRepoMock, UnitField = unitFieldRepoMock, 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 /// /// Подготовленные моки репозиториев /// Экземпляр логгера для сервиса - private static PARR.Core.Services.UnitFilterService.UnitFilterService CreateSut( + private static Core.Services.UnitFilterService.UnitFilterService CreateSut( RepositoryMocks mocks, - ILogger logger) + ILogger logger) { - var serviceOptions = Options.Create(new PARR.Core.Services.UnitFilterService.Models.UnitFilterServiceOptions { LoadBatchSize = 100 }); - return new PARR.Core.Services.UnitFilterService.UnitFilterService( logger, mocks.Job.Object, @@ -167,8 +231,13 @@ public class UnitFilterServiceTests mocks.UnitInUnit.Object, mocks.UnitInValue.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())) .Returns(context.Units.Where(u => u.Id == validParentId).Select(u => u.Id).AsQueryable()); - var logger = NullLoggerFactory.Instance.CreateLogger(); + var logger = NullLoggerFactory.Instance.CreateLogger(); var service = CreateSut(mocks, logger); // === Act === @@ -376,7 +445,7 @@ public class UnitFilterServiceTests .Distinct() .AsQueryable()); - var logger = NullLoggerFactory.Instance.CreateLogger(); + var logger = NullLoggerFactory.Instance.CreateLogger(); var service = CreateSut(mocks, logger); // === Act === @@ -481,7 +550,7 @@ public class UnitFilterServiceTests .Distinct() .AsQueryable()); - var logger = NullLoggerFactory.Instance.CreateLogger(); + var logger = NullLoggerFactory.Instance.CreateLogger(); var service = CreateSut(mocks, logger); // === Act === @@ -593,7 +662,7 @@ public class UnitFilterServiceTests .Distinct() .AsQueryable()); - var logger = NullLoggerFactory.Instance.CreateLogger(); + var logger = NullLoggerFactory.Instance.CreateLogger(); var service = CreateSut(mocks, logger); // === Act === diff --git a/PARR.TemplateMatcher/PARR.TemplateMatcher.csproj b/PARR.TemplateMatcher/PARR.TemplateMatcher.csproj index fc46dc69..c5ab7cab 100644 --- a/PARR.TemplateMatcher/PARR.TemplateMatcher.csproj +++ b/PARR.TemplateMatcher/PARR.TemplateMatcher.csproj @@ -1,21 +1,28 @@ - - net9.0 - enable - enable - + + net9.0 + enable + enable + - - - - - + + + + + - - - ..\PARR.Core\obj\Debug\net9.0\ref\PARR.Core.dll - - + + + ..\PARR.Core\obj\Debug\net9.0\ref\PARR.Core.dll + + + + + + + + + diff --git a/PARR.TemplateMatcher/Services/Implementations/SimpleTemplateSynchronizer.cs b/PARR.TemplateMatcher/Services/Implementations/SimpleTemplateSynchronizer.cs index 185ebc5d..40a3dc82 100644 --- a/PARR.TemplateMatcher/Services/Implementations/SimpleTemplateSynchronizer.cs +++ b/PARR.TemplateMatcher/Services/Implementations/SimpleTemplateSynchronizer.cs @@ -18,6 +18,7 @@ using PARR.Domain.Settings; using PARR.TemplateMatcher.Models; using PARR.TemplateMatcher.Services.Interfaces; using PARR.TemplateMatcher.Settings; +using System.Diagnostics; namespace PARR.TemplateMatcher.Services.Implementations; @@ -99,8 +100,8 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer return; } - // === Обычная логика для всех остальных Job === - logger.LogDebug("Начало синхронизации шаблонов для Job {JobId}", jobId); + var totalSw = Stopwatch.StartNew(); + logger.LogInformation("Начало синхронизации шаблонов для Job {JobId}", jobId); // === Проверка: уже запущена? === var existingStatus = await matchingStatusService.GetStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); @@ -126,6 +127,8 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer try { + // === ЭТАП 1: Загрузка Job === + var stageSw = Stopwatch.StartNew(); var job = await jobService.Get() .AsNoTracking() .Include(j => j.AutoControl) @@ -142,48 +145,22 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer await UpdateMatchingStatusAsync(jobId, "Job не найден"); return; } + stageSw.Stop(); + logger.LogDebug("[Perf] Job {JobId} | Этап: Загрузка Job | Время: {Ms} мс", + jobId, stageSw.ElapsedMilliseconds); - // === Получение отфильтрованных юнитов с полной информацией === + // === ЭТАП 2: Фильтрация юнитов === + stageSw.Restart(); var filteredUnits = await unitFilterService.GetUnitsByJobFilterAsync(jobId); - if (filteredUnits == null || !filteredUnits.Any()) - { - logger.LogInformation("Для Job {JobId} фильтры не дали Unit'ов.", jobId); + stageSw.Stop(); + var filterCount = filteredUnits?.Count() ?? 0; + logger.LogDebug("[Perf] Job {JobId} | Этап: Фильтрация юнитов | Время: {Ms} мс | Результат: {Count}", + jobId, stageSw.ElapsedMilliseconds, filterCount); - var existingTemplatesForDeactivation = await templateService.Get() - .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 + var unitIds = filteredUnits?.Select(u => u.Id).ToHashSet() ?? new HashSet(); + // === ЭТАП 3: Загрузка существующих шаблонов === + stageSw.Restart(); var existingTemplates = await templateService.Get() .Include(t => t.UnitsInTemplate) .Include(t => t.Job) @@ -199,25 +176,48 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer .Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used) .ToList(); - var existingUnitIds = existingUsedTemplates - .Select(t => t.UnitId) - .ToHashSet(); + var existingUnitIds = existingUsedTemplates.Select(t => t.UnitId).ToHashSet(); + stageSw.Stop(); + logger.LogDebug("[Perf] Job {JobId} | Этап: Загрузка шаблонов | Время: {Ms} мс | Используется: {Count}", + jobId, stageSw.ElapsedMilliseconds, existingUsedTemplates.Count); + + // === ЭТАП 4: Расчёт диффа (создание / деактивация / переименование) === + stageSw.Restart(); var newUnitIds = unitIds.Except(existingUnitIds).ToList(); var unusedTemplates = existingUsedTemplates .Where(t => !unitIds.Contains(t.UnitId)) .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) { - // Вычисляем флаги активности из конфигурации автоконтрола var isActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState; var isActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState; var request = new TemplateAllocationRequest( TargetJob: job, TargetUnitId: unitId, - TargetUnit: null, // Для простого шаблона не критично, нормализатор загрузит при необходимости + TargetUnit: null, Index: null, UnitsInTemplate: new List(), IsActiveTemplate: isActiveTemplate, @@ -226,58 +226,68 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer await templateAllocationService.AllocateAsync(request); } + stageSw.Stop(); + logger.LogDebug("[Perf] Job {JobId} | Этап: Создание шаблонов | Время: {Ms} мс | Количество: {Count}", + jobId, stageSw.ElapsedMilliseconds, newUnitIds.Count); - // === Обработка существующих шаблонов (проверка имени) === - foreach (var template in existingUsedTemplates) + // === ЭТАП 6: Обновление имён существующих шаблонов === + stageSw.Restart(); + foreach (var (template, expectedName) in templatesToRename) { - if (unitIds.Contains(template.UnitId)) + logger.LogDebug("Шаблон {TemplateId} требует обновления имени: '{OldName}' → '{NewName}'", + template.Id, template.Name, expectedName); + + var updateRequest = new TemplateUpdaterMessage { - var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(template); - if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase)) - { - logger.LogDebug("Шаблон {TemplateId} требует обновления имени: старое = '{OldName}', новое = '{NewName}'", template.Id, template.Name, expectedName); + TemplateId = template.Id, + JobId = jobId, + UnitId = template.UnitId, + Name = expectedName, + IsActiveTemplate = template.IsActiveTemplate, + IsActiveSchedule = template.IsActiveSchedule, + IsNew = false, + Index = template.Index, + StatusTypeId = TemplateStatusTypeEnum.Used, + Initiator = initiator, + UnitsInTemplate = new List() + }; - - var updateRequest = new TemplateUpdaterMessage - { - TemplateId = template.Id, - JobId = jobId, - UnitId = template.UnitId, - Name = expectedName, - IsActiveTemplate = template.IsActiveTemplate, - IsActiveSchedule = template.IsActiveSchedule, - IsNew = false, - Index = template.Index, - StatusTypeId = TemplateStatusTypeEnum.Used, - Initiator = initiator, - UnitsInTemplate = new List() // для простого шаблона - }; - - 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) { - 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); } + 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 matchingStatusService.DeleteMatchingStatusAsync(jobId, SyncTaskEntityTypeEnum.Job); logger.LogInformation("Синхронизация шаблонов завершена для Job {JobId}.", jobId); } 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}"); throw; } } + public async Task SyncTemplatesForJobGroupAsync(Guid jobGroupId, HistoryInitiator initiator) { logger.LogWarning("SimpleTemplateSynchronizer: SyncTemplatesForJobGroup вызван для JobGroup {JobGroupId}. Это не поддерживаемая операция.", jobGroupId);