feat(api,core,dal): RobotTasks - вынесение логики в Core, оптимизация взятия задания роботом

This commit is contained in:
Mikhail Trubnikov
2026-05-25 16:59:42 +10:00
parent 146e8f6f9a
commit ab563f88b9
8 changed files with 595 additions and 276 deletions

View File

@@ -7,6 +7,8 @@ using PARR.Core.Common.Interfaces;
using PARR.Core.Services.MatchingStatusService;
using PARR.Core.Services.NextRunServices;
using PARR.Core.Services.NextRunServices.Subservices;
using PARR.Core.Services.RobotTask.Implementations;
using PARR.Core.Services.RobotTask.Interfaces;
using PARR.Core.Services.Shortcodes;
using PARR.Core.Services.Shortcodes.Handlers;
using PARR.Core.Services.TaskServices.Handlers;
@@ -78,8 +80,6 @@ namespace PARR.Core
#endregion
#region Services
#region Shortсodes
services.AddScoped<IShortcodeHandler, ConstantsShortcodeHandler>();
@@ -95,10 +95,13 @@ namespace PARR.Core
services.AddScoped<IShortcodesService, ShortcodesService>();
#endregion
#region Services
services.AddTransient<IMatchingStatusService, MatchingStatusService>();
//services.AddScoped<IUserService, UserService>();
services.AddScoped<IRobotTaskService, RobotTaskService>();
//services.AddScoped<IUserService, UserService>();
#endregion

View File

@@ -26,7 +26,7 @@ namespace PARR.Core.Repositories.Interfaces
/// <param name="robotAttemptsNumber"></param>
/// <param name="robotWaitTime"></param>
/// <returns></returns>
Task FindUnfulfilledTaskAndSetRobotErrorStatusAsync(int robotAttemptsNumber, TimeSpan robotWaitTime);
Task MarkExpiredTasksAsFailedAsync(int robotAttemptsNumber, TimeSpan robotWaitTime);
/// <summary>
/// Получить конфигурацию из шаблона по RobotCode. У шаблона обязательно должен быть Include таблицы RobotConfiguration
@@ -42,5 +42,12 @@ namespace PARR.Core.Repositories.Interfaces
/// <param name="configuration"></param>
/// <returns>true - если изменил статус, false - нельзя изменить статус</returns>
bool SetUpdateTaskStatusIfAllow(RobotConfiguration configuration);
/// <summary>
/// Атомарно установить статус задачи "В работе" если текущий статус "В ожидании".
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
Task<bool> SetInProgressStatusAsync(Guid id);
}
}

View File

@@ -0,0 +1,204 @@
using InfluxDB.Client.Api.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Services.RobotTask.Interfaces;
using PARR.Domain.Entities;
using PARR.Domain.Enums;
using PARR.Domain.Exceptions;
using PARR.Domain.Settings;
namespace PARR.Core.Services.RobotTask.Implementations
{
internal class RobotTaskService : IRobotTaskService
{
/// <summary>
/// Количество заданий которые рассматриваем для взятия в работу.
/// </summary>
private readonly int TakeTasks = 10;
private readonly ILogger<RobotTaskService> logger;
private readonly IRobotConfigurationRepository robotConfigurationRepository;
private readonly SettingsFromDb settingsFromDb;
private readonly IRobotHistoryRepository robotHistoryRepository;
public RobotTaskService(
ILogger<RobotTaskService> logger,
IRobotConfigurationRepository robotConfigurationRepository,
SettingsFromDb settingsFromDb,
IRobotHistoryRepository robotHistoryRepository
)
{
this.logger = logger;
this.robotConfigurationRepository = robotConfigurationRepository;
this.settingsFromDb = settingsFromDb;
this.robotHistoryRepository = robotHistoryRepository;
}
public async Task GetTaskAsync(RobotsEnum robotCode, TaskStatusEnum taskStatusCode, bool acquireTask, string? robotIp)
{
// 1. Ищем все задания с превышенным кол-вом попыток и просроченным временем, ставим им статус ошибки
await robotConfigurationRepository.MarkExpiredTasksAsFailedAsync(settingsFromDb.RobotAttemptsNumber, settingsFromDb.RobotWaitTime);
// 2. Ищем доступные задания
var availableTasks = await GetAvailableTasksAsync(robotCode, taskStatusCode);
if (availableTasks.Count == 0)
throw new NotFoundException("Нет доступных заданий для робота");
Guid? acquiredTaskId = null;
if (acquireTask)
{
// Берем задание в работу, устанавливаем ей статус "В работе"
acquiredTaskId = await AcquireTaskAsync(availableTasks, robotIp);
if (acquiredTaskId == null)
throw new NotFoundException($"Не удалось взять ни одну из доступных задач ({availableTasks.Count}) в работу");
}
else
{
// Берем первую задачу из списка доступных
acquiredTaskId = availableTasks.First();
logger.LogDebug("Задача не требует захвата, взята первая из доступных: {TaskId}", acquiredTaskId);
}
//3. Готовим модель ответа
var task = await GetTaskWithAllDataAsync(acquiredTaskId.Value, robotCode);
//todo:
}
/// <summary>
/// Получить список возможных заданий для взятия в работу.
/// Кол-во заданй ограничено переменной TakeTasks
/// </summary>
/// <param name="robotCode"></param>
/// <param name="taskStatusCode"></param>
/// <returns></returns>
private async Task<List<Guid>> GetAvailableTasksAsync(RobotsEnum robotCode, TaskStatusEnum taskStatusCode)
{
var query = robotConfigurationRepository.Get()
.AsNoTracking()
.Where(t => t.RobotCode == (int)robotCode && t.TaskStatusCode == (int)taskStatusCode);
// Если это задание для робота расписаний
if (robotCode == RobotsEnum.ScheduleOrder)
{
// Выбираем только записи с созданными шаблонами (у которых статус 30), а только потом ищем у них расписания
var createdTemplates = robotConfigurationRepository.Get()
.Where(t => t.RobotCode == (int)RobotsEnum.TemplateOrder && t.TaskStatusCode == (int)TaskStatusEnum.Ok)
.Select(t => t.TemplateId);
query = query.Where(t => createdTemplates.Contains(t.TemplateId));
}
// Сортируем по nextRun, чтобы те, у кого nextRun ближе к текущей, выполнились скорее
query = query.OrderBy(t => t.Template!.NextRun).ThenBy(t => t.Template!.IsActiveSchedule).ThenBy(t => t.Template!.IsActiveTemplate);
// Кандидаты заданий
var tasks = new List<Guid>();
// Ещем первые 10 заданий в статусе ОЖИДАНИЕ
tasks = await query.Where(t => t.RobotStatusCode == (int)RobotStatusEnum.Wait).Take(TakeTasks).Select(t => t.Id).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.AttemptsNumber < settingsFromDb.RobotAttemptsNumber
&& t.LastRobotStatusUpdated < endDate)
.Take(TakeTasks)
.Select(t => t.Id)
.ToListAsync();
logger.LogDebug("Найдено заданий в статусе 'В работе' {Count} шт. Робот '{Robot}'", tasks.Count, robotCode.ToString());
}
return tasks;
}
/// <summary>
/// Взять задачу в работу
/// </summary>
/// <param name="taskId"></param>
/// <returns></returns>
private async Task<Guid?> AcquireTaskAsync(List<Guid> tasks, string? robotIp)
{
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
};
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)
{
var query = robotConfigurationRepository.Get()
.AsNoTracking()
.AsSingleQuery()
//todo: тут общие инклуды
;
if(robotCode== RobotsEnum.ScheduleOrder)
{
// тут инклуды только для расписаний
}
return await query.FirstAsync(t => t.Id == taskId);
}
}
}

View File

@@ -0,0 +1,9 @@
namespace PARR.Core.Services.RobotTask.Interfaces
{
/// <summary>
/// Управление заданиями роботам
/// </summary>
public interface IRobotTaskService
{
}
}