refactor(templateMatcher): Переход на Pipeline-архитектуру для SimpleSync и GroupedSync.
- SimpleTemplateSynchronizer и GroupedTemplateSynchronizer переведены на паттерн Pipeline с разделением на Read/Write этапы - Выделены контракты этапов (ISimpleSyncStage, IGroupedSyncStage) и контексты (SimpleSyncContext, GroupedSyncContext) - Read-этапы безопасны для тестов (не пишут в БД/MQ), Write-этапы изолированы через отдельные интерфейсы - Добавлено [Perf]-логирование каждого этапа с метриками времени выполнения - Логи приведены к человекочитаемому формату 'Имя' (ID) для Job, JobGroup и Unit - Устранено дублирование данных в контекстах (FilteredUnits перезаписывается, TemplateGroups строго типизирован) - Константы неиспользуемых шаблонов вынесены в UnusedTemplateConstants - Структура проекта реорганизована: SimpleSync, GroupedSync, Implementations, Interfaces
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
|
||||
using PARR.TemplateMatcher.Models;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.TemplateMatcher.Services.SimpleSync;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
|
||||
|
||||
internal class AllocateTemplatesStage : ISimpleSyncWriteStage
|
||||
{
|
||||
private readonly ITemplateAllocationService _allocationService;
|
||||
private readonly ILogger<AllocateTemplatesStage> _logger;
|
||||
|
||||
public string StageName => "Создание шаблонов";
|
||||
|
||||
public AllocateTemplatesStage(
|
||||
ITemplateAllocationService allocationService,
|
||||
ILogger<AllocateTemplatesStage> logger)
|
||||
{
|
||||
_allocationService = allocationService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
|
||||
{
|
||||
const bool defaultUsedTemplateState = false;
|
||||
const bool defaultUsedScheduleState = false;
|
||||
|
||||
foreach (var unitId in context.NewUnitIds)
|
||||
{
|
||||
var isActiveTemplate = context.Job.AutoControl?.InitUsedTemplateState ?? defaultUsedTemplateState;
|
||||
var isActiveSchedule = context.Job.AutoControl?.InitUsedScheduleState ?? defaultUsedScheduleState;
|
||||
|
||||
var request = new TemplateAllocationRequest(
|
||||
TargetJob: context.Job,
|
||||
TargetUnitId: unitId,
|
||||
TargetUnit: null,
|
||||
Index: null,
|
||||
UnitsInTemplate: new List<UnitInTemplateMessage>(),
|
||||
IsActiveTemplate: isActiveTemplate,
|
||||
IsActiveSchedule: isActiveSchedule,
|
||||
Initiator: context.Initiator);
|
||||
|
||||
await _allocationService.AllocateAsync(request);
|
||||
|
||||
_logger.LogDebug("Job '{JobName}' ({JobId}): создан шаблон для Unit {Unit}",
|
||||
context.JobName, context.JobId, context.FormatUnit(unitId));
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces;
|
||||
using PARR.Domain.Enums;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.TemplateMatcher.Services.SimpleSync;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
|
||||
|
||||
internal class AnalyzeChangesStage : ISimpleSyncStage
|
||||
{
|
||||
private readonly ITemplateRepository _templateRepository;
|
||||
private readonly ITemplateNameNormalizer _nameNormalizer;
|
||||
private readonly ILogger<AnalyzeChangesStage> _logger;
|
||||
|
||||
public string StageName => "Анализ изменений";
|
||||
|
||||
public AnalyzeChangesStage(
|
||||
ITemplateRepository templateRepository,
|
||||
ITemplateNameNormalizer nameNormalizer,
|
||||
ILogger<AnalyzeChangesStage> logger)
|
||||
{
|
||||
_templateRepository = templateRepository;
|
||||
_nameNormalizer = nameNormalizer;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
|
||||
{
|
||||
var existing = await _templateRepository.Get()
|
||||
.Include(t => t.UnitsInTemplate)
|
||||
.Include(t => t.Job).ThenInclude(t => t!.Group).ThenInclude(t => t!.GroupType)
|
||||
.Include(t => t.Job).ThenInclude(t => t!.Tnk)
|
||||
.Include(t => t.Unit)
|
||||
.Where(t => t.JobId == context.JobId)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var used = existing.Where(t => t.StatusTypeId == TemplateStatusTypeEnum.Used).ToList();
|
||||
var existingUnitIds = used.Select(t => t.UnitId).ToHashSet();
|
||||
|
||||
context.ExistingUsedTemplates = used;
|
||||
context.NewUnitIds = context.FilteredUnitIds.Except(existingUnitIds).ToList();
|
||||
context.UnusedTemplates = used.Where(t => !context.FilteredUnitIds.Contains(t.UnitId)).ToList();
|
||||
|
||||
foreach (var template in used)
|
||||
{
|
||||
if (!context.FilteredUnitIds.Contains(template.UnitId)) continue;
|
||||
|
||||
var expectedName = await _nameNormalizer.GetNormalizedTemplateNameAsync(template);
|
||||
if (!string.Equals(template.Name, expectedName, StringComparison.OrdinalIgnoreCase))
|
||||
context.TemplatesToRename.Add((template, expectedName));
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Job '{JobName}' ({JobId}): существующих шаблонов={Existing}, создать={Create}, деактивировать={Deactivate}, переименовать={Rename}",
|
||||
context.JobName, context.JobId,
|
||||
used.Count, context.NewUnitIds.Count, context.UnusedTemplates.Count, context.TemplatesToRename.Count);
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.TemplateMatcher.Services.SimpleSync;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
|
||||
|
||||
internal class DeactivateTemplatesStage : ISimpleSyncWriteStage
|
||||
{
|
||||
private readonly ITemplateDeactivator _deactivator;
|
||||
private readonly ILogger<DeactivateTemplatesStage> _logger;
|
||||
|
||||
public string StageName => "Деактивация шаблонов";
|
||||
|
||||
public DeactivateTemplatesStage(ITemplateDeactivator deactivator, ILogger<DeactivateTemplatesStage> logger)
|
||||
{
|
||||
_deactivator = deactivator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
|
||||
{
|
||||
foreach (var template in context.UnusedTemplates)
|
||||
{
|
||||
_logger.LogInformation("Job '{JobName}' ({JobId}): деактивация шаблона '{TemplateName}' ({TemplateId}), Unit {Unit}",
|
||||
context.JobName, context.JobId, template.Name, template.Id,
|
||||
context.FormatUnit(template.UnitId));
|
||||
|
||||
await _deactivator.DeactivateTemplateAsync(template, context.Initiator);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
36
PARR.TemplateMatcher/Services/SimpleSync/FilterUnitsStage.cs
Normal file
36
PARR.TemplateMatcher/Services/SimpleSync/FilterUnitsStage.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Services.UnitFilterService;
|
||||
using PARR.TemplateMatcher.Services.SimpleSync;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
|
||||
|
||||
internal class FilterUnitsStage : ISimpleSyncStage
|
||||
{
|
||||
private readonly IUnitFilterService _filterService;
|
||||
private readonly ILogger<FilterUnitsStage> _logger;
|
||||
|
||||
public string StageName => "Фильтрация юнитов";
|
||||
|
||||
public FilterUnitsStage(IUnitFilterService filterService, ILogger<FilterUnitsStage> logger)
|
||||
{
|
||||
_filterService = filterService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
|
||||
{
|
||||
var filtered = await _filterService.GetUnitsByJobFilterAsync(context.JobId, null, ct);
|
||||
|
||||
if (filtered != null)
|
||||
{
|
||||
context.FilteredUnitIds = filtered.Select(u => u.Id).ToHashSet();
|
||||
context.UnitNames = filtered.ToDictionary(u => u.Id, u => u.Name);
|
||||
}
|
||||
|
||||
_logger.LogDebug("Job '{JobName}' ({JobId}): отфильтровано {Count} юнитов",
|
||||
context.JobName, context.JobId, context.FilteredUnitIds.Count);
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
12
PARR.TemplateMatcher/Services/SimpleSync/ISimpleSyncStage.cs
Normal file
12
PARR.TemplateMatcher/Services/SimpleSync/ISimpleSyncStage.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace PARR.TemplateMatcher.Services.SimpleSync
|
||||
{
|
||||
/// <summary>
|
||||
/// Этап синхронизации, который только читает данные и заполняет контекст.
|
||||
/// НЕ выполняет запись в БД, MQ или кэш.
|
||||
/// </summary>
|
||||
public interface ISimpleSyncStage
|
||||
{
|
||||
string StageName { get; }
|
||||
Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace PARR.TemplateMatcher.Services.SimpleSync
|
||||
{
|
||||
/// <summary>
|
||||
/// Этап синхронизации, который выполняет побочные эффекты (запись в БД, MQ).
|
||||
/// В тестах не подключается — тип системы гарантирует безопасность.
|
||||
/// </summary>
|
||||
public interface ISimpleSyncWriteStage : ISimpleSyncStage
|
||||
{
|
||||
}
|
||||
}
|
||||
42
PARR.TemplateMatcher/Services/SimpleSync/LoadJobStage.cs
Normal file
42
PARR.TemplateMatcher/Services/SimpleSync/LoadJobStage.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Job;
|
||||
using PARR.TemplateMatcher.Services.SimpleSync;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
|
||||
|
||||
internal class LoadJobStage : ISimpleSyncStage
|
||||
{
|
||||
private readonly IJobRepository _jobRepository;
|
||||
private readonly ILogger<LoadJobStage> _logger;
|
||||
|
||||
public string StageName => "Загрузка Job";
|
||||
|
||||
public LoadJobStage(IJobRepository jobRepository, ILogger<LoadJobStage> logger)
|
||||
{
|
||||
_jobRepository = jobRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
|
||||
{
|
||||
var job = await _jobRepository.Get()
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Include(j => j.AutoControl)
|
||||
.Include(j => j.Tnk)
|
||||
.Include(j => j.Group).ThenInclude(g => g!.GroupType)
|
||||
.Include(j => j.UnitFilters).ThenInclude(uf => uf.RelationshipFilters)
|
||||
.FirstOrDefaultAsync(j => j.Id == context.JobId, ct);
|
||||
|
||||
if (job == null)
|
||||
throw new InvalidOperationException($"Job '{context.JobId}' не найден");
|
||||
|
||||
context.Job = job;
|
||||
context.JobName = job.Name;
|
||||
|
||||
_logger.LogDebug("Job '{JobName}' ({JobId}) загружен", job.Name, job.Id);
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using PARR.Domain.Entities;
|
||||
using PARR.Domain.Entities.Base.History;
|
||||
using PARR.Domain.Entities.Job;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.SimpleSync
|
||||
{
|
||||
/// <summary>
|
||||
/// Контекст синхронизации простого шаблона. Передаётся между этапами.
|
||||
/// </summary>
|
||||
public class SimpleSyncContext
|
||||
{
|
||||
public Guid JobId { get; init; }
|
||||
public string JobName { get; set; } = string.Empty;
|
||||
public HistoryInitiator Initiator { get; init; } = null!;
|
||||
public Job Job { get; set; } = null!;
|
||||
public HashSet<Guid> FilteredUnitIds { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Имена юнитов для логирования. Заполняется на этапе фильтрации.
|
||||
/// </summary>
|
||||
public Dictionary<Guid, string> UnitNames { get; set; } = new();
|
||||
|
||||
public List<Template> ExistingUsedTemplates { get; set; } = new();
|
||||
public List<Guid> NewUnitIds { get; set; } = new();
|
||||
public List<Template> UnusedTemplates { get; set; } = new();
|
||||
public List<(Template Template, string ExpectedName)> TemplatesToRename { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Возвращает строку вида 'Имя' (ID) для логирования.
|
||||
/// Если имя неизвестно — возвращает только ID.
|
||||
/// </summary>
|
||||
public string FormatUnit(Guid unitId)
|
||||
{
|
||||
return UnitNames.TryGetValue(unitId, out var name)
|
||||
? $"'{name}' ({unitId})"
|
||||
: $"({unitId})";
|
||||
}
|
||||
}
|
||||
}
|
||||
50
PARR.TemplateMatcher/Services/SimpleSync/UpdateNamesStage.cs
Normal file
50
PARR.TemplateMatcher/Services/SimpleSync/UpdateNamesStage.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
|
||||
using PARR.Domain.Enums;
|
||||
using PARR.TemplateMatcher.Services.Interfaces;
|
||||
using PARR.TemplateMatcher.Services.SimpleSync;
|
||||
|
||||
namespace PARR.TemplateMatcher.Services.Implementations.SimpleSync;
|
||||
|
||||
internal class UpdateNamesStage : ISimpleSyncWriteStage
|
||||
{
|
||||
private readonly ITemplateMqPublisher _mqPublisher;
|
||||
private readonly ILogger<UpdateNamesStage> _logger;
|
||||
|
||||
public string StageName => "Обновление имён";
|
||||
|
||||
public UpdateNamesStage(ITemplateMqPublisher mqPublisher, ILogger<UpdateNamesStage> logger)
|
||||
{
|
||||
_mqPublisher = mqPublisher;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<SimpleSyncContext> ExecuteAsync(SimpleSyncContext context, CancellationToken ct = default)
|
||||
{
|
||||
foreach (var (template, expectedName) in context.TemplatesToRename)
|
||||
{
|
||||
var updateRequest = new TemplateUpdaterMessage
|
||||
{
|
||||
TemplateId = template.Id,
|
||||
JobId = context.JobId,
|
||||
UnitId = template.UnitId,
|
||||
Name = expectedName,
|
||||
IsActiveTemplate = template.IsActiveTemplate,
|
||||
IsActiveSchedule = template.IsActiveSchedule,
|
||||
IsNew = false,
|
||||
Index = template.Index,
|
||||
StatusTypeId = TemplateStatusTypeEnum.Used,
|
||||
Initiator = context.Initiator,
|
||||
UnitsInTemplate = new List<UnitInTemplateMessage>()
|
||||
};
|
||||
|
||||
await _mqPublisher.PublishUpdateAsync(updateRequest);
|
||||
|
||||
_logger.LogDebug("Job '{JobName}' ({JobId}): шаблон '{TemplateName}' ({TemplateId}) для Unit {Unit} переименован в '{NewName}'",
|
||||
context.JobName, context.JobId, template.Name, template.Id,
|
||||
context.FormatUnit(template.UnitId), expectedName);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user