Files

667 lines
41 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
}