667 lines
41 KiB
C#
667 lines
41 KiB
C#
using AutoMapper;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Logging;
|
||
using PARR.BLL.Helpers;
|
||
using PARR.Core.Repositories.Interfaces;
|
||
using PARR.Core.Repositories.Interfaces.TemplateRepositories;
|
||
using PARR.Core.Services.NextRunServices;
|
||
using PARR.Core.Services.RobotTask.Interfaces;
|
||
using PARR.Core.Services.RobotTask.Models;
|
||
using PARR.Core.Services.Shortcodes;
|
||
using PARR.Domain.DTOs.RobotTask;
|
||
using PARR.Domain.Entities;
|
||
using PARR.Domain.Entities.Base.History;
|
||
using PARR.Domain.Entities.RobotEntities;
|
||
using PARR.Domain.Enums;
|
||
using PARR.Domain.Exceptions;
|
||
using PARR.Domain.Settings;
|
||
|
||
namespace PARR.Core.Services.RobotTask.Implementations
|
||
{
|
||
internal class RobotTaskService : IRobotTaskService
|
||
{
|
||
/// <summary>
|
||
/// Количество заданий которые рассматриваем для взятия в работу.
|
||
/// Рекомендованное значение, кол-во роботов * 3
|
||
/// </summary>
|
||
private readonly int TakeTasks = 15 * 3;
|
||
|
||
private readonly ILogger<RobotTaskService> _logger;
|
||
private readonly IRobotConfigurationRepository _robotConfigurationRepository;
|
||
private readonly SettingsFromDb _settingsFromDb;
|
||
private readonly IRobotHistoryRepository _robotHistoryRepository;
|
||
private readonly IMapper _mapper;
|
||
private readonly IShortcodesService _shortcodesService;
|
||
private readonly INextRunService _nextRunService;
|
||
private readonly ITemplateRenamePendingRepository _templateRenamePendingRepository;
|
||
|
||
public RobotTaskService(
|
||
ILogger<RobotTaskService> logger,
|
||
IRobotConfigurationRepository robotConfigurationRepository,
|
||
SettingsFromDb settingsFromDb,
|
||
IRobotHistoryRepository robotHistoryRepository,
|
||
IMapper mapper,
|
||
IShortcodesService shortcodesService,
|
||
INextRunService nextRunService,
|
||
ITemplateRenamePendingRepository templateRenamePendingRepository
|
||
)
|
||
{
|
||
_logger = logger;
|
||
_robotConfigurationRepository = robotConfigurationRepository;
|
||
_settingsFromDb = settingsFromDb;
|
||
_robotHistoryRepository = robotHistoryRepository;
|
||
_mapper = mapper;
|
||
_shortcodesService = shortcodesService;
|
||
_nextRunService = nextRunService;
|
||
_templateRenamePendingRepository = templateRenamePendingRepository;
|
||
}
|
||
|
||
|
||
public async Task<RobotTaskTemplate> GetTemplateTaskAsync(TaskStatusEnum taskStatusCode, bool acquireTask, string? robotIp, string? robotId)
|
||
{
|
||
var templateTask = await GetTaskAsync(RobotsEnum.TemplateOrder, taskStatusCode, acquireTask, robotIp, robotId, TimeSpan.Zero);
|
||
|
||
var task = _mapper.Map<RobotTaskTemplate>(templateTask);
|
||
|
||
task = task with { FullDescription = NormalizeLineEndingsToCrlf(await _shortcodesService.ApplyShortcodesAsync(task.FullDescription, templateTask.Template!)) };
|
||
task = task with { ShortDescription = await _shortcodesService.ApplyShortcodesAsync(task.ShortDescription, templateTask.Template!) };
|
||
task = task with { Solution = NormalizeLineEndingsToCrlf(await _shortcodesService.ApplyShortcodesAsync(task.Solution, templateTask.Template!)) };
|
||
task = task with { TnkName = await _shortcodesService.ApplyShortcodesAsync(task.TnkName, templateTask.Template!) };
|
||
task = task with { WorkName = await _shortcodesService.ApplyShortcodesAsync(task.WorkName, templateTask.Template!) };
|
||
task = task with { WorkGroup = await _shortcodesService.ApplyShortcodesAsync(task.WorkGroup, templateTask.Template!) };
|
||
task = task with { ResponseArea = await _shortcodesService.ApplyShortcodesAsync(task.ResponseArea, templateTask.Template!) };
|
||
|
||
task = task with { ClosingCode = _settingsFromDb.ClosingCode };
|
||
task = task with { Initiator = _settingsFromDb.Initiator };
|
||
task = task with { Category = _settingsFromDb.Category };
|
||
|
||
return task;
|
||
}
|
||
|
||
|
||
public async Task<RobotTaskSchedule> GetScheduleTaskAsync(TaskStatusEnum taskStatusCode, bool acquireTask, string? robotIp, string? robotId, IHistoryInitiator historyInitiator, TimeSpan scheduleCooldownDuration)
|
||
{
|
||
var scheduleTask = await GetTaskAsync(RobotsEnum.ScheduleOrder, taskStatusCode, acquireTask, robotIp, robotId, scheduleCooldownDuration);
|
||
|
||
// Проверяем nextRun, lastRun, обновляем их
|
||
|
||
var resultUpdateNextRun = await UpdateNextRunAsync(scheduleTask, historyInitiator);
|
||
if (!resultUpdateNextRun)
|
||
{
|
||
_logger.LogError("Ошибка при расчете NextRun для templateId: {templateId}", scheduleTask.TemplateId);
|
||
throw new NextRunException($"Ошибка при расчете NextRun для templateId: {scheduleTask.TemplateId}");
|
||
}
|
||
|
||
var task = _mapper.Map<RobotTaskSchedule>(scheduleTask);
|
||
|
||
task = task with { Timezone = _settingsFromDb.EsppScheduleTimezone };
|
||
task = task with { WorkGroup = await _shortcodesService.ApplyShortcodesAsync(task.WorkGroup, scheduleTask.Template!) };
|
||
task = task with { ResponseArea = await _shortcodesService.ApplyShortcodesAsync(task.ResponseArea, scheduleTask.Template!) };
|
||
|
||
//nextRun в часовой зоне УЗ Робота ЕСПП
|
||
var nextRunWithRobotTz = scheduleTask.Template!.NextRun.Add(_nextRunService.GetEsppAccountOffset());
|
||
//на всякий случай еще раз проверяем, что дата не устарела и отправляем задание
|
||
if (nextRunWithRobotTz < DateTimeOffset.UtcNow)
|
||
{
|
||
_logger.LogError("Ошибка при расчете NextRun для templateId: {templateId}, итоговое значение для робота, меньше чем сейчас {nextRunWithRobotTz}<{now}",
|
||
task.TemplateId, nextRunWithRobotTz, DateTimeOffset.UtcNow);
|
||
throw new NextRunException($"Ошибка при расчете NextRun для templateId: {scheduleTask.TemplateId}");
|
||
}
|
||
|
||
task = task with { NextStart = EsppScheduleHelpers.GetNextRun(nextRunWithRobotTz) };
|
||
task = task with { GenerationTime = EsppScheduleHelpers.GetGenerationTime(nextRunWithRobotTz) };
|
||
|
||
task = task with { RepeatRange = _settingsFromDb.ScheduleRepeatRange };
|
||
task = task with { };
|
||
|
||
return task;
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Получить задачу для робота.
|
||
/// Метод может генерировать исключения.
|
||
/// </summary>
|
||
/// <param name="robotCode"></param>
|
||
/// <param name="taskStatusCode"></param>
|
||
/// <param name="acquireTask">Взять в работу</param>
|
||
/// <param name="robotIp"></param>
|
||
/// <returns></returns>
|
||
/// <exception cref="NotFoundException"></exception>
|
||
private async Task<RobotConfiguration> GetTaskAsync(RobotsEnum robotCode, TaskStatusEnum taskStatusCode, bool acquireTask, string? robotIp, string? robotId, TimeSpan scheduleCooldownDuration)
|
||
{
|
||
// 1. Ищем все задания с превышенным кол-вом попыток и просроченным временем, ставим им статус ошибки
|
||
await _robotConfigurationRepository.MarkExpiredTasksAsFailedAsync(_settingsFromDb.RobotAttemptsNumber, _settingsFromDb.RobotWaitTime);
|
||
|
||
|
||
// 2. Ищем доступные задания
|
||
var availableTasks = await GetAvailableTasksAsync(robotCode, taskStatusCode, scheduleCooldownDuration);
|
||
|
||
if (availableTasks.Count == 0)
|
||
throw new NotFoundException("Нет доступных заданий для робота");
|
||
|
||
Guid? acquiredTaskId = null;
|
||
|
||
if (acquireTask)
|
||
{
|
||
// Берем задание в работу, устанавливаем ему статус "В работе"
|
||
acquiredTaskId = await AcquireTaskAsync(availableTasks, robotIp, robotId);
|
||
|
||
if (acquiredTaskId == null)
|
||
throw new Exception($"Не удалось взять ни одну из доступных задач ({availableTasks.Count}) в работу");
|
||
}
|
||
else
|
||
{
|
||
// Берем первую задачу из списка доступных
|
||
acquiredTaskId = availableTasks.First();
|
||
_logger.LogDebug("Задача не требует захвата, взята первая из доступных: {TaskId}", acquiredTaskId);
|
||
}
|
||
|
||
|
||
//3. Получаем задачу со всеми нужными инклудами в зависимости от типа робота
|
||
var task = await GetTaskWithAllDataAsync(acquiredTaskId.Value, robotCode);
|
||
|
||
return task;
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Получить список возможных заданий для взятия в работу.
|
||
/// Кол-во заданй ограничено переменной TakeTasks
|
||
/// </summary>
|
||
/// <param name="robotCode"></param>
|
||
/// <param name="taskStatusCode"></param>
|
||
/// <returns></returns>
|
||
private async Task<List<Guid>> GetAvailableTasksAsync(RobotsEnum robotCode, TaskStatusEnum taskStatusCode, TimeSpan scheduleCooldownDuration)
|
||
{
|
||
var query = _robotConfigurationRepository.Get()
|
||
.AsNoTracking()
|
||
.Where(t => t.RobotCode == (int)robotCode);
|
||
|
||
// Если это задание для робота расписаний
|
||
if (robotCode == RobotsEnum.ScheduleOrder)
|
||
{
|
||
// Выбираем только записи с созданными шаблонами (у которых статус 30), а только потом ищем у них расписания
|
||
query = query.Where(t => t.Template!.RobotConfigurations.Any(x => x.RobotCode == (int)RobotsEnum.TemplateOrder && x.TaskStatusCode == (int)TaskStatusEnum.Ok));
|
||
|
||
|
||
// Не берем шаблоны, у которых lastRun + 3 часа < сейчас, и у них последний инициатор был или nextRun (10) или esppSchedule (5), это условие применяется только к активированным расписаниям
|
||
var cooldownThreshold = DateTimeOffset.UtcNow.Add(-scheduleCooldownDuration);
|
||
query = query.Where(t =>
|
||
// Условие кулдауна: проверяем, попадает ли шаблон под ЗАПРЕТ
|
||
!(
|
||
t.Template!.IsActiveSchedule
|
||
&& (t.Template.InitiatorParrComponentId == ParrComponentsEnum.EsppScheduleSync || t.Template.InitiatorParrComponentId == ParrComponentsEnum.NextRun)
|
||
&& t.Template.LastRun >= cooldownThreshold
|
||
)
|
||
);
|
||
}
|
||
|
||
// Сортируем по nextRun, чтобы те, у кого nextRun ближе к текущей, выполнились скорее
|
||
query = query.OrderBy(t => t.Template!.NextRun).ThenBy(t => t.Template!.IsActiveSchedule).ThenBy(t => t.Template!.IsActiveTemplate);
|
||
|
||
// Кандидаты заданий, Id задания и имя шаблона
|
||
//var tasks = new List<Guid>();
|
||
var tasks = new List<RobotTaskDetails>();
|
||
|
||
// Ищем первые TakeTasks заданий в статусе ОЖИДАНИЕ
|
||
tasks = await query
|
||
.Where(t =>
|
||
t.RobotStatusCode == (int)RobotStatusEnum.Wait
|
||
&& t.TaskStatusCode == (int)taskStatusCode
|
||
).Take(TakeTasks)
|
||
//.Select(t => t.Id)
|
||
.Select(t => new RobotTaskDetails(t.Id, t.Template!.Name, t.Template.NextRun))
|
||
.ToListAsync();
|
||
|
||
|
||
_logger.LogDebug("Найдено заданий в статусе 'Ожидание' {Count} шт. Робот '{Robot}'", tasks.Count, robotCode.ToString());
|
||
|
||
if (tasks.Count == 0)
|
||
{
|
||
// Ищем задания в статусе В РАБОТЕ, которые можно перезапустить
|
||
// Поиск по `RobotStatusCode` = 22.
|
||
// Далее проверяется `LastStatusUpdated`, что время последнего смены статуса не превышает допустимого(берется из настроек, поле `RobotWaitTime`)
|
||
// и что текущая попытка не больше разрешенной(берется из настроек, поле `RobotAttemptsNumber`) - если это так, берется эта запись.
|
||
|
||
var endDate = DateTimeOffset.UtcNow.Add(-_settingsFromDb.RobotWaitTime);
|
||
|
||
tasks = await query.Where(t => t.RobotStatusCode == (int)RobotStatusEnum.InProgress
|
||
&& t.TaskStatusCode == (int)taskStatusCode
|
||
&& t.AttemptsNumber < _settingsFromDb.RobotAttemptsNumber
|
||
&& t.LastRobotStatusUpdated < endDate)
|
||
.Take(TakeTasks)
|
||
//.Select(t => t.Id)
|
||
.Select(t => new RobotTaskDetails(t.Id, t.Template!.Name, t.Template.NextRun))
|
||
.ToListAsync();
|
||
|
||
_logger.LogDebug("Найдено заданий в статусе 'В работе' {Count} шт. Робот '{Robot}'", tasks.Count, robotCode.ToString());
|
||
}
|
||
|
||
if (robotCode == RobotsEnum.TemplateOrder)
|
||
{
|
||
// Если запрашиваем шаблоны, смотрим корректируем список заданий в зависимости от статуса переименования.
|
||
// Это не относится к расписаниям, потому что у переименованных расписаний статус Updating, а оно не возьмется в работу, пока не обновится шаблон
|
||
tasks = await ReplaceTemplateTasksForRenameAsync(tasks, robotCode);
|
||
}
|
||
|
||
return tasks.Select(t => t.TaskId).ToList();
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Проверяет наличие шаблонов в процессе переименования и заменяет обычные задания на задания по переименованию.
|
||
/// Если связанный шаблон не переименован, и у него статус ошибки, целевому шаблону устанавливается статус ошибки.
|
||
/// </summary>
|
||
/// <param name="tasks"></param>
|
||
/// <returns></returns>
|
||
private async Task<List<RobotTaskDetails>> ReplaceTemplateTasksForRenameAsync(List<RobotTaskDetails> tasks, RobotsEnum robotCode)
|
||
{
|
||
if (tasks.Count == 0 || robotCode != RobotsEnum.TemplateOrder)
|
||
return tasks;
|
||
|
||
_logger.LogDebug("Исходный пул задач для проверки переименования: {Tasks}",
|
||
string.Join(" | ", tasks.Select(t => $"[Id: {t.TaskId}, Name: '{t.TemplateName}']")));
|
||
|
||
// Ищем есть ли связанные шаблоны с таким имененм на переименование
|
||
var taskTemplateNames = tasks.Select(t => t.TemplateName).Distinct().ToList();
|
||
// Ищем записи в таблице переименований, где OldName совпадает с именами наших новых задач
|
||
var templatesToRename = await _templateRenamePendingRepository.Get()
|
||
.AsNoTracking()
|
||
.Where(t => taskTemplateNames.Contains(t.OldName))
|
||
.ToListAsync();
|
||
|
||
_logger.LogDebug("Найдено записей в TemplateRenamePending для текущих задач: {Count} шт.", templatesToRename.Count);
|
||
|
||
if (templatesToRename.Count == 0)
|
||
return tasks;
|
||
|
||
// Создаем словарь маппинга TemplateId -> OldName.
|
||
var templateIdToOldName = templatesToRename.ToDictionary(t => t.TemplateId, t => t.OldName);
|
||
|
||
// Ищем конфигурации роботов для СТАРЫХ шаблонов (которые переименовываются) по ИД, смотрим, можем ли взять их в работу
|
||
var renameTemplateIds = templatesToRename.Select(t => t.TemplateId).ToList();
|
||
var renameTasks = await _robotConfigurationRepository.Get()
|
||
.AsNoTracking()
|
||
.Include(t => t.Template)
|
||
.Where(t =>
|
||
t.RobotCode == (int)robotCode
|
||
&& renameTemplateIds.Contains(t.TemplateId)
|
||
// Это может быть только обновление. Так как переименования для создаваемого шаблона быть не может
|
||
&& t.TaskStatusCode == (int)TaskStatusEnum.Updating
|
||
).ToListAsync();
|
||
|
||
// =========================================================================
|
||
// БЛОК 1: ОБРАБОТКА ОШИБОК (Правило: если ХОТЯ БЫ ОДНА упала в ошибку -> оригинал в ошибку)
|
||
// =========================================================================
|
||
|
||
// Если старый шаблон в ошибке и лимит попыток исчерпан, ставим ошибку и новому шаблону
|
||
var errorTasks = renameTasks
|
||
.Where(t =>
|
||
t.RobotStatusCode == (int)RobotStatusEnum.Error
|
||
&& t.AttemptsNumber >= _settingsFromDb.RobotAttemptsNumber
|
||
).ToList();
|
||
|
||
var tasksToSetErrorStatus = new List<Guid>();
|
||
if (errorTasks.Count > 0)
|
||
{
|
||
_logger.LogDebug("Найдено связанных заданий на переименование с ошибками: {ErrorCount}. Ставим ошибку целевым (новым) заданиям.", errorTasks.Count);
|
||
|
||
// Собираем ВСЕ OldName, для которых есть хотя бы одна упавшая в ошибку задача.
|
||
// Использование ToHashSet() гарантирует, что если 1 или 10 задач в ошибке, OldName попадет в набор один раз.
|
||
var errorOldNames = errorTasks
|
||
.Where(t => templateIdToOldName.ContainsKey(t.TemplateId))
|
||
.Select(t => templateIdToOldName[t.TemplateId])
|
||
.ToHashSet();
|
||
|
||
// Находим оригинальные задачи, чье имя совпадает с любым из "ошибочных" OldName
|
||
tasksToSetErrorStatus = tasks
|
||
.Where(t => errorOldNames.Contains(t.TemplateName))
|
||
.Select(t => t.TaskId)
|
||
.ToList();
|
||
|
||
if (tasksToSetErrorStatus.Count > 0)
|
||
{
|
||
// Устанавливаем ошибку целевым + пишем комментарий от робота + нажимаем комит
|
||
var logMessage = "[RobotTaskService] Установлен статус ошибки, так как хотя бы одна из связанных задач переименования не была успешно выполнена.";
|
||
await SetErrorStatusAsync(tasksToSetErrorStatus, logMessage);
|
||
}
|
||
}
|
||
|
||
// =========================================================================
|
||
// БЛОК 2: ПОДМЕНА ЗАДАЧ (Правило: берем ПЕРВУЮ валидную задачу для подмены)
|
||
// =========================================================================
|
||
var endDate = DateTimeOffset.UtcNow.Add(-_settingsFromDb.RobotWaitTime);
|
||
|
||
// Фильтруем старые задачи, которые МОЖНО взять в работу. Смотрим статусы роботов, можно взять в работу, только если (RobotStatus == Wait) или (InProgress но которые еще не просрочены)
|
||
var allowedTasks = renameTasks.Where(t =>
|
||
t.RobotStatusCode == (int)RobotStatusEnum.Wait
|
||
|| (t.RobotStatusCode == (int)RobotStatusEnum.InProgress
|
||
&& t.AttemptsNumber < _settingsFromDb.RobotAttemptsNumber
|
||
&& t.LastRobotStatusUpdated < endDate)
|
||
).ToList();
|
||
|
||
// Проверим StatusTypeId у старых шаблонов в процессе переименования
|
||
// 1. Находим задачи переименования, у которых StatusTypeId шаблона НЕ является допустимым (!= Used и != Unused)
|
||
var invalidRenameTasks = allowedTasks
|
||
.Where(t => t.Template != null && t.Template.StatusTypeId != TemplateStatusTypeEnum.Used && t.Template.StatusTypeId != TemplateStatusTypeEnum.Unused)
|
||
.ToList();
|
||
|
||
// 2. Создаем словарь для быстрого поиска и логирования: OldName -> StatusTypeId
|
||
// Так как OldName не уникален, используем GroupBy, чтобы избежать ArgumentException, при наличии нескольких невалидных задач с одинаковым OldName.
|
||
//var invalidOldNamesWithStatus = invalidRenameTasks
|
||
// .Where(t => templateIdToOldName.ContainsKey(t.TemplateId))
|
||
// .Select(t => new { OldName = templateIdToOldName[t.TemplateId], StatusTypeId = t.Template!.StatusTypeId })
|
||
// .ToDictionary(x => x.OldName, x => x.StatusTypeId);
|
||
var invalidOldNamesWithStatus = invalidRenameTasks
|
||
.Where(t => templateIdToOldName.ContainsKey(t.TemplateId))
|
||
.GroupBy(t => templateIdToOldName[t.TemplateId]) // Группируем по OldName
|
||
.ToDictionary(
|
||
g => g.Key, // Ключ = OldName
|
||
g => g.First().Template!.StatusTypeId // Значение = StatusTypeId первой задачи в группе (для лога)
|
||
);
|
||
|
||
// 3. Оставляем для подмены только те задачи, у которых StatusTypeId является допустимым (== Used или == Unused)
|
||
var validAllowedTasks = allowedTasks
|
||
.Where(t => t.Template != null && (t.Template.StatusTypeId == TemplateStatusTypeEnum.Used || t.Template.StatusTypeId == TemplateStatusTypeEnum.Unused))
|
||
.ToList();
|
||
|
||
// Формируем список заданий
|
||
var originalCount = tasks.Count;
|
||
var errorTaskIdsSet = tasksToSetErrorStatus.ToHashSet();
|
||
var errorCount = errorTaskIdsSet.Count;
|
||
|
||
// Создаем словарь подмены ТОЛЬКО из валидных задач (где StatusTypeId == Used или Unused)
|
||
// ГРУППИРУЕМ по OldName и берем .First()!
|
||
// Это реализует правило: "если записей несколько, берем из них первую и подменяем ей оригинальное задание".
|
||
var renameTasksToDictionary = validAllowedTasks
|
||
.Where(t => templateIdToOldName.ContainsKey(t.TemplateId))
|
||
.GroupBy(t => templateIdToOldName[t.TemplateId])
|
||
.ToDictionary(
|
||
g => g.Key, // Ключ = OldName
|
||
g => new RobotTaskDetails(g.First().Id, g.First().Template!.Name, g.First().Template!.NextRun)
|
||
);
|
||
|
||
// Проходим по ИСХОДНОМУ списку, чтобы сохранить порядок сортировки
|
||
var finalTasks = new List<RobotTaskDetails>(tasks.Count);
|
||
int replacedCount = 0;
|
||
int excludedByStatusCount = 0; // Счетчик для логов
|
||
|
||
foreach (var task in tasks)
|
||
{
|
||
// 1. Если задаче нужно поставить ошибку, пропускаем ее
|
||
if (errorTaskIdsSet.Contains(task.TaskId))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 2. Если этот шаблон связан с переименованием, но у старого шаблона StatusTypeId != Used
|
||
if (invalidOldNamesWithStatus.TryGetValue(task.TemplateName, out var badStatusId))
|
||
{
|
||
_logger.LogWarning(
|
||
"Задача для шаблона '{TemplateName}' (TaskId: {TaskId}) ИСКЛЮЧЕНА из выдачи. " +
|
||
"Связанный шаблон в процессе переименования имеет недопустимый StatusTypeId = {StatusTypeId} (ожидалось Used или Unused). " +
|
||
"Исходная задача также не выполняется.",
|
||
task.TemplateName, task.TaskId, badStatusId);
|
||
|
||
excludedByStatusCount++;
|
||
continue; // Не добавляем ни старую, ни новую задачу в итоговый список
|
||
}
|
||
|
||
// 3. Если для этого имени шаблона есть разрешенная задача на переименование (и она валидна) - вставляем ее
|
||
// Подменяем оригинальную задачу на ПЕРВУЮ валидную задачу переименования
|
||
if (renameTasksToDictionary.TryGetValue(task.TemplateName, out var renameTask))
|
||
{
|
||
_logger.LogDebug("ПОДМЕНА ЗАДАЧИ: Исходная [Id: {OriginalId}, Name: '{OriginalName}'] " +
|
||
"-> Заменена на [Id: {NewId}, Name: '{NewName}']",
|
||
task.TaskId, task.TemplateName, renameTask.TaskId, renameTask.TemplateName);
|
||
|
||
finalTasks.Add(renameTask);
|
||
replacedCount++;
|
||
}
|
||
else
|
||
{
|
||
// Иначе оставляем исходную задачу на месте
|
||
finalTasks.Add(task);
|
||
}
|
||
}
|
||
|
||
_logger.LogDebug("Итоговый пул задач после трансформации: {Tasks}",
|
||
string.Join(" | ", finalTasks.Select(t => $"[Id: {t.TaskId}, Name: '{t.TemplateName}']")));
|
||
|
||
_logger.LogInformation(
|
||
"Трансформация пула задач завершена. Исходных: {OriginalCount} шт. " +
|
||
"Отклонено (ошибка): {ErrorCount} шт. Исключено (невалидный StatusTypeId): {ExcludedCount} шт. " +
|
||
"Заменено на старые (взята первая из группы): {ReplacedCount} шт. Итого к выдаче: {FinalCount} шт.",
|
||
originalCount, errorCount, excludedByStatusCount, replacedCount, finalTasks.Count);
|
||
|
||
// Возвращаем без дополнительной сортировки по NextRun. Порядок сохранен начального списка
|
||
return finalTasks;
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Установить статус задания - ошибка
|
||
/// </summary>
|
||
/// <param name="taskIds"></param>
|
||
/// <returns></returns>
|
||
private async Task SetErrorStatusAsync(List<Guid> taskIds, string logMessage)
|
||
{
|
||
if (taskIds == null || taskIds.Count == 0)
|
||
return;
|
||
|
||
var tasks = await _robotConfigurationRepository.Get()
|
||
.Include(t => t.Template)
|
||
.Where(t => taskIds.Contains(t.Id))
|
||
.ToListAsync();
|
||
|
||
if (tasks.Count == 0)
|
||
return;
|
||
|
||
foreach (var task in tasks)
|
||
{
|
||
// Так как это целевой шаблон, то ставим ему сразу максимальное кол-во попыток и ошибку, чтоб больше он не выдавался в заданиях, пока не исправим связанный
|
||
// Устанавливаем статус ошибки
|
||
_robotConfigurationRepository.SetErrorRobotStatusAndMaxAttempts(task);
|
||
|
||
// Пишем в лог роботу
|
||
var history = new RobotHistory
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
HistoryLevel = (int)RobotStatusEnum.Error,
|
||
TaskStatusCode = task.TaskStatusCode,
|
||
RobotConfigurationId = task.Id,
|
||
RobotIp = null,
|
||
RobotId = ParrComponentsEnum.Api.ToString(),
|
||
RobotMessage = logMessage
|
||
};
|
||
|
||
await _robotHistoryRepository.CreateAsync(history);
|
||
|
||
_logger.LogInformation("Для целевого задания {TaskId} (шаблон '{TemplateName}') установлен статус ошибки, " +
|
||
"так как связанное задание со старым шаблоном не было успешно выполнено.",
|
||
task.Id, task.Template!.Name);
|
||
}
|
||
|
||
if (await _robotHistoryRepository.CommitAsync())
|
||
{
|
||
_logger.LogDebug("Установлен статус 'Ошибка', для заданий {TaskCount} шт.", tasks.Count);
|
||
}
|
||
else
|
||
{
|
||
_logger.LogError("Ошибка при установке статуса задания 'Ошибка', для заданий {TaskCount} шт. Транзакция отменена", tasks.Count);
|
||
throw new DbErrorException("Не удалось сохранить изменения статусов заданий при обработке переименования шаблона.");
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Взять задачу в работу
|
||
/// </summary>
|
||
/// <param name="taskId"></param>
|
||
/// <returns></returns>
|
||
private async Task<Guid?> AcquireTaskAsync(List<Guid> tasks, string? robotIp, string? robotId)
|
||
{
|
||
foreach (var taskId in tasks)
|
||
{
|
||
var isChangedStatus = await _robotConfigurationRepository.SetInProgressStatusAsync(taskId);
|
||
if (isChangedStatus)
|
||
{
|
||
_logger.LogDebug("Захвачена задача {TaskId}", taskId);
|
||
|
||
var task = await _robotConfigurationRepository.Get()
|
||
.AsNoTracking()
|
||
.FirstAsync(t => t.Id == taskId);
|
||
|
||
// пишем в историю робота
|
||
var history = new RobotHistory
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
HistoryLevel = (int)RobotHistoryLevelEnum.Start,
|
||
TaskStatusCode = task.TaskStatusCode,
|
||
RobotConfigurationId = taskId,
|
||
RobotIp = robotIp,
|
||
RobotId = robotId
|
||
};
|
||
|
||
if (!await _robotHistoryRepository.CreateAsync(history) || !await _robotHistoryRepository.CommitAsync())
|
||
throw new DbErrorException("Ошибка при добавлении истории робота, при взятии задания в работу.");
|
||
|
||
return taskId;
|
||
}
|
||
else
|
||
{
|
||
_logger.LogDebug("Не удалось захватить задачу {TaskId}", taskId);
|
||
}
|
||
}
|
||
|
||
_logger.LogDebug("Не удалось захватить ни одну из доступных задач для робота");
|
||
|
||
return null;
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Получить задачу со всем необходимыми полями
|
||
/// </summary>
|
||
/// <param name="taskId"></param>
|
||
/// <param name="robotCode"></param>
|
||
/// <returns></returns>
|
||
private async Task<RobotConfiguration> GetTaskWithAllDataAsync(Guid taskId, RobotsEnum robotCode)
|
||
{
|
||
IQueryable<RobotConfiguration> query = _robotConfigurationRepository.Get()
|
||
//.AsNoTracking() // нужно обязательно трекать, так как может измениться nextRun и его нужно будет сохранить
|
||
.AsSingleQuery()
|
||
// Общие инклуды для шаблонов и расписаний
|
||
// Units
|
||
.Include(t => t.Template)
|
||
.ThenInclude(t => t!.Unit)
|
||
.ThenInclude(t => t!.UnitValues)
|
||
.ThenInclude(t => t!.Field)
|
||
.Include(t => t.Template)
|
||
.ThenInclude(t => t!.Unit)
|
||
.ThenInclude(t => t!.UnitValues)
|
||
.ThenInclude(t => t!.Value)
|
||
.Include(t => t.Template)
|
||
.ThenInclude(t => t!.UnitsInTemplate)
|
||
// Группа с типом
|
||
.Include(t => t.Template)
|
||
.ThenInclude(t => t!.Job)
|
||
.ThenInclude(t => t!.Group)
|
||
.ThenInclude(t => t!.GroupType)
|
||
// ТНК
|
||
.Include(t => t.Template)
|
||
.ThenInclude(t => t!.Job)
|
||
.ThenInclude(t => t!.Tnk)
|
||
.ThenInclude(t => t!.Subprocess)
|
||
.ThenInclude(t => t!.Process);
|
||
|
||
if (robotCode == RobotsEnum.ScheduleOrder)
|
||
{
|
||
// Инклуды только для расписаний
|
||
query = query
|
||
// Расписание
|
||
.Include(t => t.Template)
|
||
.ThenInclude(t => t!.Job)
|
||
.ThenInclude(t => t!.Group)
|
||
.ThenInclude(t => t!.EsppSchValues)
|
||
.ThenInclude(t => t!.EsppSchTypeConfig)
|
||
.ThenInclude(t => t!.EsppSchTypeSchedule)
|
||
// Исключения по типу
|
||
.Include(t => t.Template)
|
||
.ThenInclude(t => t!.Job)
|
||
.ThenInclude(t => t!.Group)
|
||
.ThenInclude(t => t!.ScheduleExcludeType)
|
||
// Исключения по календарю
|
||
.Include(t => t.Template)
|
||
.ThenInclude(t => t!.Job)
|
||
.ThenInclude(t => t!.Group)
|
||
.ThenInclude(t => t!.ScheduleExcludeTypeCalendar);
|
||
}
|
||
|
||
return await query.FirstAsync(t => t.Id == taskId);
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Приводит переносы строк в тексте к формату CRLF (\r\n)
|
||
/// </summary>
|
||
/// <param name="text">Исходный текст</param>
|
||
/// <returns>Текст с унифицированными переносами строк</returns>
|
||
private string NormalizeLineEndingsToCrlf(string? text)
|
||
{
|
||
if (string.IsNullOrEmpty(text))
|
||
return string.Empty;
|
||
|
||
// Заменяем любые варианты переносов (\r\n, \r, \n) на единый \r\n
|
||
return System.Text.RegularExpressions.Regex.Replace(text, @"\r\n|\r|\n", "\r\n");
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Обоновить NextRun если он устарел
|
||
/// </summary>
|
||
/// <param name="task"></param>
|
||
/// <returns></returns>
|
||
private async Task<bool> UpdateNextRunAsync(RobotConfiguration task, IHistoryInitiator historyInitiator)
|
||
{
|
||
var template = task.Template!;
|
||
|
||
//var nextRun = await esppScheduleTransformService.GetNextDateAsync(template.Job!.GroupId, template!.Job!.Group!.ReferenceDate);
|
||
var nextRun = await _nextRunService.GetNextRunForTemplateAsync(template.Id, false);
|
||
|
||
if (!nextRun.HasValue)
|
||
{
|
||
_logger.LogError("При обновлении nextRun для шаблона {TemplateId}, расчитанный nextRun=null, ошибка в расчетах.", template.Id);
|
||
return false;
|
||
}
|
||
|
||
if (nextRun.Value < DateTimeOffset.UtcNow)
|
||
{
|
||
_logger.LogError("При обновлении nextRun для шаблона {TemplateId}, расчитанный nextRun<Now [{NextRun}<{Now}], ошибка в расчетах.", template.Id, nextRun.Value, DateTimeOffset.UtcNow);
|
||
return false;
|
||
}
|
||
|
||
if (nextRun != template.NextRun)
|
||
{
|
||
_logger.LogDebug("Для шаблона {TemplateId} обновляю nextRun. Новое: {NewNextRun}, старое: {OldNextRun}", template.Id, nextRun, template.NextRun);
|
||
|
||
template.LastRun = template.NextRun;
|
||
template.NextRun = nextRun.Value;
|
||
|
||
var suffix = "При получении задания роботом, обновил NextRun";
|
||
historyInitiator.InitiatorComment =
|
||
string.IsNullOrEmpty(historyInitiator.InitiatorComment)
|
||
? suffix
|
||
: $"{historyInitiator.InitiatorComment}. {suffix}";
|
||
|
||
if (!await _robotConfigurationRepository.CommitAsync(historyInitiator))
|
||
throw new DbErrorException("Ошибка при сохранении изменения NextRun");
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
}
|
||
}
|