fix(templateMatcher): Исправление ошибок в именах шаблонов и рефакторинг синхронизации

- Устранена ошибка, когда при переиспользовании шаблона для шорткодов передавался ЭК "КОМПЛЕКСЫ-[ЗО]" вместо целевого ЭК шаблона.
- Проведен небольшой рефакторинг общих процессов синхронизации различных типов групп работ.
This commit is contained in:
Mikhail Kuznetsov
2026-05-20 17:59:06 +10:00
parent c0148088c1
commit 513e5f869a
26 changed files with 995 additions and 638 deletions

View File

@@ -0,0 +1,17 @@
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.Unit;
namespace PARR.TemplateMatcher.Models
{
public record TemplateAllocationRequest(
Job TargetJob,
Guid TargetUnitId,
Unit? TargetUnit,
int? Index,
List<UnitInTemplateMessage> UnitsInTemplate,
bool IsActiveTemplate,
bool IsActiveSchedule,
HistoryInitiator Initiator);
}

View File

@@ -19,30 +19,26 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
private readonly ILogger<GroupedTemplateProcessor> logger;
private readonly ITemplateRepository templateRepository;
private readonly IUnitRepository unitRepository;
private readonly ITemplateReuser templateReuser;
private readonly ITemplateNameNormalizer templateNameNormalizer;
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
private readonly MqSettings mqSettings;
private readonly IRabbitService mqService;
private readonly ITemplateAllocationService templateAllocationService;
private readonly ITemplateMqPublisher templateMqPublisher;
public GroupedTemplateProcessor(
ILogger<GroupedTemplateProcessor> logger,
ITemplateRepository templateRepository,
IUnitRepository unitRepository,
ITemplateReuser templateReuser,
ITemplateNameNormalizer templateNameNormalizer,
ITemplateUpdaterMqSender templateUpdaterMqSender,
ITemplateAllocationService templateAllocationService,
ITemplateMqPublisher templateMqPublisher,
MqSettings mqSettings,
IRabbitService mqService)
{
this.logger = logger;
this.templateRepository = templateRepository;
this.unitRepository = unitRepository;
this.templateReuser = templateReuser;
this.templateNameNormalizer = templateNameNormalizer;
this.templateUpdaterMqSender = templateUpdaterMqSender;
this.mqSettings = mqSettings;
this.mqService = mqService;
this.templateAllocationService = templateAllocationService;
this.templateMqPublisher = templateMqPublisher;
}
public async Task<HashSet<(Guid JobId, Guid UnitId, int Index)>> ProcessAsync(
@@ -89,7 +85,7 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
}
else
{
await HandleNewOrReusableTemplateAsync(potentialUnitId, unitsInTemplateSubGroup, targetJob, globalIndex, initiator);
await HandleNewOrReusableTemplateAsync(potentialUnitId, unitsInTemplateSubGroup, targetJob, globalIndex, initiator, ct);
}
}
}
@@ -159,7 +155,7 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
UnitFieldValueId = t.UnitFieldValueId
}).ToList()
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
await templateMqPublisher.PublishUpdateAsync(updateRequest);
}
}
else
@@ -174,50 +170,33 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
List<(Guid UnitId, Guid UnitFieldValueId)> unitsInTemplateSubGroup,
Job targetJob,
int globalIndex,
HistoryInitiator initiator)
HistoryInitiator initiator,
CancellationToken ct)
{
var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(targetJob.Id, potentialUnitId, initiator);
// Вычисляем флаги активности
var isActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? false;
var isActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? false;
if (reusableTemplate != null)
{
logger.LogInformation("Переиспользован шаблон {TemplateId}.", reusableTemplate.Id);
var tempTemplateForName = new Template
// Маппим кортежи в сообщения
var unitsInTemplateMsg = unitsInTemplateSubGroup
.Select(e => new UnitInTemplateMessage
{
Id = reusableTemplate.Id,
Name = reusableTemplate.Name,
JobId = targetJob.Id,
UnitId = potentialUnitId,
Index = globalIndex,
Job = targetJob,
Unit = reusableTemplate.Unit,
UnitsInTemplate = unitsInTemplateSubGroup.Select(e => new UnitsInTemplate { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList()
};
UnitId = e.UnitId,
UnitFieldValueId = e.UnitFieldValueId
})
.ToList();
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
var request = new TemplateAllocationRequest(
TargetJob: targetJob,
TargetUnitId: potentialUnitId,
TargetUnit: null,
Index: globalIndex,
UnitsInTemplate: unitsInTemplateMsg,
IsActiveTemplate: isActiveTemplate,
IsActiveSchedule: isActiveSchedule,
Initiator: initiator);
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = reusableTemplate.Id,
JobId = targetJob.Id,
UnitId = potentialUnitId,
Name = expectedName,
IsActiveTemplate = targetJob.AutoControl?.InitUsedTemplateState ?? false,
IsActiveSchedule = targetJob.AutoControl?.InitUsedScheduleState ?? false,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
IsNew = true,
Index = globalIndex,
UnitsInTemplate = unitsInTemplateSubGroup.Select(e => new UnitInTemplateMessage { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList()
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
else
{
logger.LogDebug("Создание нового шаблона.");
await CreateGroupedTemplateAsync(targetJob.Id, potentialUnitId, unitsInTemplateSubGroup, globalIndex, initiator);
}
await templateAllocationService.AllocateAsync(request, ct);
}
private async Task UpdateTemplateUnitsAsync(
@@ -270,32 +249,9 @@ internal class GroupedTemplateProcessor : IGroupedTemplateProcessor
UnitsInTemplate = newUnitEntries.Select(e => new UnitInTemplateMessage { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList()
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
await templateMqPublisher.PublishUpdateAsync(updateRequest);
}
private async Task CreateGroupedTemplateAsync(
Guid jobId,
Guid relationshipUnitId,
List<(Guid UnitId, Guid UnitFieldValueId)> unitsInTemplate,
int index,
HistoryInitiator initiator)
{
logger.LogInformation("Создание нового группового шаблона.");
var mqRequest = new TemplateGeneratorMessage
{
JobId = jobId,
UnitId = relationshipUnitId,
UnitsInTemplate = unitsInTemplate.Select(e => new UnitInTemplateMessage { UnitId = e.UnitId, UnitFieldValueId = e.UnitFieldValueId }).ToList(),
Index = index,
HistoryInitiator = initiator
};
var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new List<object> { mqRequest });
if (!result.IsSuccess)
logger.LogError("Ошибка отправки команды создания шаблона.");
}
private static Job SelectTargetJob(List<Job> jobsInGroup, int subGroupSize, Job maxJob)
{

View File

@@ -15,6 +15,7 @@ using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.Unit;
using PARR.Domain.Enums;
using PARR.Domain.Settings;
using PARR.TemplateMatcher.Models;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings;
@@ -40,10 +41,10 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
private readonly IRabbitService mqService;
private readonly ITemplateRepository templateService;
private readonly IJobRepository jobService;
private readonly ITemplateReuser templateReuser;
private readonly ITemplateDeactivator templateDeactivator;
private readonly ITemplateNameNormalizer templateNameNormalizer;
private readonly ITemplateUpdaterMqSender templateUpdaterMqSender;
private readonly ITemplateAllocationService templateAllocationService;
private readonly ITemplateMqPublisher templateMqPublisher;
private readonly IMatchingStatusService matchingStatusService;
private readonly SettingsFromDb settingsFromDb;
private readonly IOptions<TemplateSettings> templateSettings;
@@ -58,10 +59,10 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
IRabbitService mqService,
ITemplateRepository templateService,
IJobRepository jobService,
ITemplateReuser templateReuser,
ITemplateDeactivator templateDeactivator,
ITemplateNameNormalizer templateNameNormalizer,
ITemplateUpdaterMqSender templateUpdaterMqSender,
ITemplateAllocationService templateAllocationService,
ITemplateMqPublisher templateMqPublisher,
IMatchingStatusService matchingStatusService,
SettingsFromDb settingsFromDb,
IOptions<TemplateSettings> templateSettings,
@@ -76,10 +77,10 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
this.mqService = mqService;
this.templateService = templateService;
this.jobService = jobService;
this.templateReuser = templateReuser;
this.templateDeactivator = templateDeactivator;
this.templateNameNormalizer = templateNameNormalizer;
this.templateUpdaterMqSender = templateUpdaterMqSender;
this.templateAllocationService = templateAllocationService;
this.templateMqPublisher = templateMqPublisher;
this.matchingStatusService = matchingStatusService;
this.settingsFromDb = settingsFromDb;
this.templateSettings = templateSettings;
@@ -209,49 +210,21 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
foreach (var unitId in newUnitIds)
{
var reusableTemplate = await templateReuser.TryReuseOneUnusedTemplateAsync(jobId, unitId, initiator);
if (reusableTemplate != null)
{
logger.LogInformation("Переиспользован шаблон {TemplateId} для Job {JobId}, UnitId {UnitId}.", reusableTemplate.Id, jobId, unitId);
// Вычисляем флаги активности из конфигурации автоконтрола
var isActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState;
var isActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState;
// === Создаём временный Template для нормализации имени ===
var tempTemplateForName = new Template
{
Id = reusableTemplate.Id,
Name = reusableTemplate.Name,
JobId = jobId,
UnitId = unitId,
Index = reusableTemplate.Index,
Job = job, // загруженный job
Unit = reusableTemplate.Unit, // может быть null — нормально
UnitsInTemplate = new List<UnitsInTemplate>() // для простого шаблона
};
var request = new TemplateAllocationRequest(
TargetJob: job,
TargetUnitId: unitId,
TargetUnit: null, // Для простого шаблона не критично, нормализатор загрузит при необходимости
Index: null,
UnitsInTemplate: new List<UnitInTemplateMessage>(),
IsActiveTemplate: isActiveTemplate,
IsActiveSchedule: isActiveSchedule,
Initiator: initiator);
var expectedName = await templateNameNormalizer.GetNormalizedTemplateNameAsync(tempTemplateForName);
//var nextRun = await nextRunService.GetNextRunForTemplateAsync(reusableTemplate.Id, true);
var updateRequest = new TemplateUpdaterMessage
{
TemplateId = reusableTemplate.Id,
JobId = jobId,
UnitId = unitId,
Name = expectedName,
IsActiveTemplate = job.AutoControl?.InitUsedTemplateState ?? DefaultUsedTemplateState,
IsActiveSchedule = job.AutoControl?.InitUsedScheduleState ?? DefaultUsedScheduleState,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
//NextRun = nextRun,
IsNew = true,
UnitsInTemplate = new List<UnitInTemplateMessage>() // для простого шаблона
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
}
else
{
logger.LogDebug("Создание нового шаблона для Job {JobId}, UnitId {UnitId}.", jobId, unitId);
await CreateSimpleTemplateAsync(jobId, unitId, initiator);
}
await templateAllocationService.AllocateAsync(request);
}
// === Обработка существующих шаблонов (проверка имени) ===
@@ -280,7 +253,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
UnitsInTemplate = new List<UnitInTemplateMessage>() // для простого шаблона
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
await templateMqPublisher.PublishUpdateAsync(updateRequest);
}
}
}
@@ -415,7 +388,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
UnitsInTemplate = new List<UnitInTemplateMessage>() // для простого шаблона
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
await templateMqPublisher.PublishUpdateAsync(updateRequest);
}
}
}
@@ -662,7 +635,7 @@ internal class SimpleTemplateSynchronizer : ITemplateSynchronizer
UnitsInTemplate = new List<UnitInTemplateMessage>()
};
await templateUpdaterMqSender.SendTemplateUpdateMessageAsync(updateRequest);
await templateMqPublisher.PublishUpdateAsync(updateRequest);
}

View File

@@ -0,0 +1,58 @@
using Microsoft.Extensions.Logging;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.TemplateMatcher.Models;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations
{
internal class TemplateAllocationService : ITemplateAllocationService
{
private readonly ILogger<TemplateAllocationService> logger;
private readonly ITemplateReuser reuser;
private readonly ITemplateMqPublisher publisher;
public TemplateAllocationService(
ILogger<TemplateAllocationService> logger,
ITemplateReuser reuser,
ITemplateMqPublisher publisher)
{
this.logger = logger;
this.reuser = reuser;
this.publisher = publisher;
}
public async Task AllocateAsync(TemplateAllocationRequest request, CancellationToken ct = default)
{
// Пробуем переиспользовать
bool reused = await reuser.TryReuseAsync(
request.TargetJob,
request.TargetUnitId,
request.TargetUnit,
request.Index,
request.UnitsInTemplate,
request.IsActiveTemplate,
request.IsActiveSchedule,
request.Initiator,
ct);
if (!reused)
{
logger.LogDebug("Переиспользование не удалось. Запрос на создание шаблона для Job {JobId}, Unit {UnitId}, Index {Index}.",
request.TargetJob.Id, request.TargetUnitId, request.Index);
var createMessage = new TemplateGeneratorMessage
{
JobId = request.TargetJob.Id,
UnitId = request.TargetUnitId,
Index = request.Index,
UnitsInTemplate = request.UnitsInTemplate ?? new List<UnitInTemplateMessage>(),
IsActiveTemplate = request.IsActiveTemplate,
IsActiveSchedule = request.IsActiveSchedule,
HistoryInitiator = request.Initiator
};
await publisher.PublishCreateAsync(createMessage, ct);
}
}
}
}

View File

@@ -23,7 +23,7 @@ internal class TemplateDeactivator : ITemplateDeactivator
private readonly ITemplateRepository templateService;
private readonly IJobRepository jobService;
private readonly ITemplateNameNormalizer namenormalizer;
private readonly ITemplateUpdaterMqSender sender;
private readonly ITemplateMqPublisher templateMqPublisher;
private readonly SettingsFromDb settingsFromDb;
private readonly IOptions<TemplateSettings> templateSettings;
@@ -32,7 +32,7 @@ internal class TemplateDeactivator : ITemplateDeactivator
ITemplateRepository templateService,
IJobRepository jobService,
ITemplateNameNormalizer namenormalizer,
ITemplateUpdaterMqSender sender,
ITemplateMqPublisher templateMqPublisher,
SettingsFromDb settingsFromDb,
IOptions<TemplateSettings> templateSettings
)
@@ -41,7 +41,7 @@ internal class TemplateDeactivator : ITemplateDeactivator
this.templateService = templateService;
this.jobService = jobService;
this.namenormalizer = namenormalizer;
this.sender = sender;
this.templateMqPublisher = templateMqPublisher;
this.settingsFromDb = settingsFromDb;
this.templateSettings = templateSettings;
}
@@ -115,7 +115,7 @@ internal class TemplateDeactivator : ITemplateDeactivator
UnitsInTemplate = new List<UnitInTemplateMessage>()
};
await sender.SendTemplateUpdateMessageAsync(updateRequest);
await templateMqPublisher.PublishUpdateAsync(updateRequest);
return true;
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Common.Interfaces.RabbitServices;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings;
namespace PARR.TemplateMatcher.Services.Implementations
{
internal class TemplateMqPublisher : ITemplateMqPublisher
{
private readonly ILogger<TemplateMqPublisher> logger;
private readonly IRabbitService mqService;
private readonly MqSettings mqSettings;
public TemplateMqPublisher(ILogger<TemplateMqPublisher> logger, IRabbitService mqService, MqSettings mqSettings)
{
this.logger = logger;
this.mqService = mqService;
this.mqSettings = mqSettings;
}
public async Task PublishUpdateAsync(TemplateUpdaterMessage message, CancellationToken ct = default)
{
var result = await mqService.SendAsync(mqSettings.TemplateUpdater, new List<object> { message });
if (!result.IsSuccess)
logger.LogError("Ошибка публикации команды обновления шаблона {TemplateId}.", message.TemplateId);
}
public async Task PublishCreateAsync(TemplateGeneratorMessage message, CancellationToken ct = default)
{
var result = await mqService.SendAsync(mqSettings.TemplateGenerator, new List<object> { message });
if (!result.IsSuccess)
logger.LogError("Ошибка публикации команды создания шаблона для Job {JobId}, Unit {UnitId}.", message.JobId, message.UnitId);
}
}
}

View File

@@ -1,8 +1,12 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.Domain.Entities;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.Unit;
using PARR.Domain.Enums;
using PARR.TemplateMatcher.Services.Interfaces;
namespace PARR.TemplateMatcher.Services.Implementations;
@@ -10,71 +14,105 @@ namespace PARR.TemplateMatcher.Services.Implementations;
internal class TemplateReuser : ITemplateReuser
{
private readonly ILogger<TemplateReuser> logger;
private readonly ITemplateRepository templateService;
private readonly ITemplateRepository templateRepository;
private readonly ITemplateNameNormalizer nameNormalizer;
private readonly ITemplateMqPublisher mqPublisher;
public TemplateReuser(
ILogger<TemplateReuser> logger,
ITemplateRepository templateService)
ITemplateRepository templateRepository,
ITemplateNameNormalizer nameNormalizer,
ITemplateMqPublisher mqPublisher)
{
this.logger = logger;
this.templateService = templateService;
this.templateRepository = templateRepository;
this.nameNormalizer = nameNormalizer;
this.mqPublisher = mqPublisher;
}
public async Task<Template?> TryReuseOneUnusedTemplateAsync(
Guid jobId,
Guid unitId,
public async Task<bool> TryReuseAsync(
Job targetJob,
Guid targetUnitId,
Unit? targetUnit,
int? index,
List<UnitInTemplateMessage> unitsInTemplate,
bool isActiveTemplate,
bool isActiveSchedule,
HistoryInitiator initiator,
int maxAttempts = 3)
CancellationToken ct = default)
{
const int maxAttempts = 3;
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
ct.ThrowIfCancellationRequested();
try
{
// Атомарно резервируем один шаблон через DAL
var templateId = await templateService.ReserveUnusedTemplateAsync(unitId, initiator);
// 1. Атомарный захват шаблона
var templateId = await templateRepository.ReserveUnusedTemplateAsync(targetUnitId, initiator);
if (templateId == null)
{
logger.LogDebug("Нет доступных Unused-шаблонов для переиспользования (попытка {Attempt}).", attempt);
return null;
logger.LogDebug("Нет доступных Unused-шаблонов (попытка {Attempt}).", attempt);
return false; // Шаблоны закончились
}
// Загружаем зарезервированный шаблон
var template = await templateService.Get()
// Загружаем захваченный шаблон
var template = await templateRepository.Get()
.AsNoTracking()
.Include(t => t.Unit)
.Include(t => t.Job)
.ThenInclude(t => t!.Tnk)
.Include(t => t.Job)
.ThenInclude(t => t!.Group)
.ThenInclude(t => t!.GroupType)
.FirstOrDefaultAsync(t => t.Id == templateId);
.FirstOrDefaultAsync(t => t.Id == templateId, ct);
if (template == null)
{
logger.LogWarning("Зарезервированный шаблон {TemplateId} не найден при загрузке.", templateId);
continue;
logger.LogWarning("Зарезервированный шаблон {TemplateId} не найден.", templateId);
continue; // Попробовать еще раз
}
logger.LogInformation(
"Успешно захвачен шаблон {TemplateId} (старый Job {OldJobId}) для нового Job {NewJobId}, Unit {UnitId} (попытка {Attempt}).",
template.Id, template.JobId, jobId, unitId, attempt);
logger.LogInformation("Шаблон {TemplateId} захвачен для переиспользования.", templateId);
return template;
// 2. Нормализация имени
var templateForName = new Template
{
Id = template.Id,
Name = template.Name,
JobId = targetJob.Id,
UnitId = targetUnitId,
Index = index,
Job = targetJob,
Unit = targetUnit,
UnitsInTemplate = unitsInTemplate.Select(m => new UnitsInTemplate { UnitId = m.UnitId, UnitFieldValueId = m.UnitFieldValueId }).ToList()
};
var expectedName = await nameNormalizer.GetNormalizedTemplateNameAsync(templateForName);
// 3. Отправка команды в MQ
var message = new TemplateUpdaterMessage
{
TemplateId = template.Id,
JobId = targetJob.Id,
UnitId = targetUnitId,
Name = expectedName,
IsActiveTemplate = isActiveTemplate,
IsActiveSchedule = isActiveSchedule,
StatusTypeId = TemplateStatusTypeEnum.Used,
Initiator = initiator,
IsNew = true,
Index = index ?? template.Index,
UnitsInTemplate = unitsInTemplate
};
await mqPublisher.PublishUpdateAsync(message, ct);
return true; // Успех
}
catch (Exception ex)
{
logger.LogError(ex, "Ошибка при попытке захвата шаблона (попытка {Attempt}).", attempt);
if (attempt == maxAttempts)
throw;
// Небольшая задержка перед повтором
await Task.Delay(Random.Shared.Next(10, 50));
logger.LogError(ex, "Ошибка при переиспользовании (попытка {Attempt}).", attempt);
if (attempt == maxAttempts) return false; // После 3 попыток сдаемся
await Task.Delay(Random.Shared.Next(10, 50), ct);
}
}
return null;
return false;
}
}

View File

@@ -1,43 +0,0 @@
using Microsoft.Extensions.Logging;
using PARR.Core.Common.Interfaces.RabbitServices;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.TemplateMatcher.Services.Interfaces;
using PARR.TemplateMatcher.Settings;
namespace PARR.TemplateMatcher.Services.Implementations;
internal class TemplateUpdaterMqSender : ITemplateUpdaterMqSender
{
private readonly ILogger<TemplateUpdaterMqSender> logger;
private readonly IRabbitService mqService;
private readonly MqSettings mqSettings;
public TemplateUpdaterMqSender(
ILogger<TemplateUpdaterMqSender> logger,
IRabbitService mqService,
MqSettings mqSettings
)
{
this.logger = logger;
this.mqService = mqService;
this.mqSettings = mqSettings;
}
public async Task SendTemplateUpdateMessageAsync(TemplateUpdaterMessage updateRequest)
{
logger.LogDebug("Отправка сообщения в очередь '{Queue}' для шаблона {TemplateId}",
mqSettings.TemplateUpdater.QueueName, updateRequest.TemplateId);
var result = await mqService.SendAsync(mqSettings.TemplateUpdater, new List<object> { updateRequest });
if (result.IsSuccess)
{
logger.LogInformation("Отправлен запрос на обновление шаблона {TemplateId}", updateRequest.TemplateId);
}
else
{
logger.LogError("Ошибка при отправке запроса на обновление шаблона {TemplateId} в очередь '{Queue}'.",
updateRequest.TemplateId, mqSettings.TemplateUpdater.QueueName);
}
}
}

View File

@@ -0,0 +1,12 @@
using PARR.TemplateMatcher.Models;
namespace PARR.TemplateMatcher.Services.Interfaces
{
/// <summary>
/// Оркестрирует выделение шаблона: попытка переиспользования или создание нового.
/// </summary>
public interface ITemplateAllocationService
{
Task AllocateAsync(TemplateAllocationRequest request, CancellationToken ct = default);
}
}

View File

@@ -0,0 +1,13 @@
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
namespace PARR.TemplateMatcher.Services.Interfaces
{
/// <summary>
/// Публикует команды создания и обновления шаблонов в RabbitMQ.
/// </summary>
public interface ITemplateMqPublisher
{
Task PublishUpdateAsync(TemplateUpdaterMessage message, CancellationToken ct = default);
Task PublishCreateAsync(TemplateGeneratorMessage message, CancellationToken ct = default);
}
}

View File

@@ -1,27 +1,31 @@
using PARR.Domain.Entities;
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
using PARR.Domain.Entities.Base.History;
using PARR.Domain.Entities.Job;
using PARR.Domain.Entities.Unit;
namespace PARR.TemplateMatcher.Services.Interfaces
{
/// <summary>
/// Отвечает за полный цикл переиспользования Unused-шаблона:
/// 1. Атомарный захват в БД (с retry).
/// 2. Нормализация имени.
/// 3. Отправка команды на обновление (активацию) в MQ.
/// </summary>
public interface ITemplateReuser
{
/// <summary>
/// Пытается переиспользовать неиспользуемый шаблон для указанной работы и ЭК.
/// При резервировании шаблона для правильного расчета NextRun Id работы
/// и ЭК устанавливается при захвате.
/// Пытается переиспользовать шаблон. Возвращает true, если успешно захвачен и отправлена команда.
/// </summary>
/// <param name="jobId">Id работы.</param>
/// <param name="unitId">Id ЭК.</param>
/// <param name="initiator">Данные инициатора операции.</param>
/// <param name="maxAttempts">Максимальное количество попыток (по умолчанию 3).</param>
/// <returns>
/// Зарезервированный шаблон или <see langword="null"/>,
/// если не удалось получить шаблон после всех попыток.
/// </returns>
Task<Template?> TryReuseOneUnusedTemplateAsync(
Guid jobId,
Guid unitId,
Task<bool> TryReuseAsync(
Job targetJob,
Guid targetUnitId,
Unit? targetUnit,
int? index,
List<UnitInTemplateMessage> unitsInTemplate,
bool isActiveTemplate,
bool isActiveSchedule,
HistoryInitiator initiator,
int maxAttempts = 3);
CancellationToken ct = default);
}
}

View File

@@ -1,9 +0,0 @@
using PARR.Domain.Common.Rabbit.Messages.TemplateMatching;
namespace PARR.TemplateMatcher.Services.Interfaces
{
public interface ITemplateUpdaterMqSender
{
Task SendTemplateUpdateMessageAsync(TemplateUpdaterMessage updateRequest);
}
}

View File

@@ -26,20 +26,34 @@ namespace PARR.TemplateMatcher
configuration.GetSection(nameof(TemplateSettings)).Bind(templateSettings);
services.AddSingleton(templateSettings);
services.AddTransient<IMqTemplateMatcher, MqTemplateMatcher>();
services.AddTransient<IJobValidatorService, JobValidatorService>();
services.AddTransient<IJobGroupValidatorService, JobGroupValidatorService>();
services.AddTransient<ITemplateMatcher, TemplateMatcher>();
services.AddTransient<ITemplateReuser, TemplateReuser>();
// === 1. Singleton: Долгоживущие сервисы и обработчики очередей ===
services.AddSingleton<IMqTemplateMatcher, MqTemplateMatcher>();
// === 2. Scoped: Бизнес-логика и работа с БД (DbContext) ===
// Создаются заново для каждого сообщения из очереди (внутри CreateAsyncScope)
services.AddScoped<ITemplateMatcher, TemplateMatcher>();
services.AddScoped<ITemplateSynchronizer, SimpleTemplateSynchronizer>();
services.AddScoped<ITemplateSynchronizer, GroupedTemplateSynchronizer>();
// Пайплайн аллокации шаблонов
services.AddScoped<ITemplateAllocationService, TemplateAllocationService>();
services.AddScoped<ITemplateReuser, TemplateReuser>();
services.AddScoped<ITemplateDeactivator, TemplateDeactivator>();
// Пайплайн групповых шаблонов
services.AddScoped<IGroupedTemplateUnitFilter, GroupedTemplateUnitFilter>();
services.AddScoped<IUnitInTemplateConflictMapper, UnitInTemplateConflictMapper>();
services.AddScoped<IGroupedTemplateBuilder, GroupedTemplateBuilder>();
services.AddScoped<IGroupedTemplateProcessor, GroupedTemplateProcessor>();
// Валидаторы
services.AddScoped<IJobValidatorService, JobValidatorService>();
services.AddScoped<IJobGroupValidatorService, JobGroupValidatorService>();
// === 3. Transient: Stateless утилиты и инфраструктура ===
// Легковесные сервисы без состояния, создаются по требованию
services.AddTransient<ITemplateMqPublisher, TemplateMqPublisher>();
services.AddTransient<ITemplateNameNormalizer, TemplateNameNormalizer>();
services.AddTransient<ITemplateUpdaterMqSender, TemplateUpdaterMqSender>();
services.AddTransient<ITemplateDeactivator, TemplateDeactivator>();
services.AddTransient<IUnitInTemplateConflictMapper, UnitInTemplateConflictMapper>();
services.AddTransient<IGroupedTemplateUnitFilter, GroupedTemplateUnitFilter>();
services.AddTransient<IGroupedTemplateBuilder, GroupedTemplateBuilder>();
services.AddTransient<IGroupedTemplateProcessor, GroupedTemplateProcessor>();
services.AddTransient<ITemplateSynchronizer, SimpleTemplateSynchronizer>();
services.AddTransient<ITemplateSynchronizer, GroupedTemplateSynchronizer>();
}
public static IConfigurationBuilder AddTemplateMatcherConfigurations(this IConfigurationBuilder builder, IServiceCollection services)