< Summary

Information
Class: LGDXRobotCloud.API.Services.Automation.AutoTaskSchedulerService
Assembly: LGDXRobotCloud.API
File(s): /builds/yukaitung/lgdxrobot2-cloud/LGDXRobotCloud.API/Services/Automation/AutoTaskSchedulerService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 355
Coverable lines: 355
Total lines: 488
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 116
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/builds/yukaitung/lgdxrobot2-cloud/LGDXRobotCloud.API/Services/Automation/AutoTaskSchedulerService.cs

#LineLine coverage
 1using LGDXRobotCloud.API.Repositories;
 2using LGDXRobotCloud.API.Services.Common;
 3using LGDXRobotCloud.API.Services.Navigation;
 4using LGDXRobotCloud.Data.DbContexts;
 5using LGDXRobotCloud.Data.Entities;
 6using LGDXRobotCloud.Data.Models.Redis;
 7using LGDXRobotCloud.Protos;
 8using LGDXRobotCloud.Utilities.Enums;
 9using LGDXRobotCloud.Utilities.Helpers;
 10using Microsoft.EntityFrameworkCore;
 11
 12namespace LGDXRobotCloud.API.Services.Automation;
 13
 14public interface IAutoTaskSchedulerService
 15{
 16  Task RunSchedulerNewAutoTaskAsync(int realmId, Guid? robotId);
 17  Task RunSchedulerRobotNewJoinAsync(Guid robotId);
 18  Task<bool> RunSchedulerRobotReadyAsync(Guid robotId);
 19
 20  Task ReleaseRobotAsync(Guid robotId);
 21
 22  Task AutoTaskAbortAsync(Guid robotId, int taskId, string token, AutoTaskAbortReason autoTaskAbortReason);
 23  Task<bool> AutoTaskAbortApiAsync(int taskId);
 24  Task AutoTaskNextAsync(Guid robotId, int taskId, string token);
 25  Task<bool> AutoTaskNextApiAsync(Guid robotId, int taskId, string token);
 26}
 27
 028public class AutoTaskSchedulerService(
 029    IAutoTaskPathPlannerService autoTaskPathPlanner,
 030    IAutoTaskRepository autoTaskRepository,
 031    IEmailService emailService,
 032    IOnlineRobotsService onlineRobotsService,
 033    IRobotService robotService,
 034    ITriggerService triggerService,
 035    LgdxContext context
 036  ) : IAutoTaskSchedulerService
 37{
 038  private readonly IAutoTaskPathPlannerService _autoTaskPathPlanner = autoTaskPathPlanner ?? throw new ArgumentNullExcep
 039  private readonly IAutoTaskRepository _autoTaskRepository = autoTaskRepository ?? throw new ArgumentNullException(nameo
 040  private readonly IEmailService _emailService = emailService ?? throw new ArgumentNullException(nameof(emailService));
 041  private readonly IOnlineRobotsService _onlineRobotsService = onlineRobotsService ?? throw new ArgumentNullException(na
 042  private readonly IRobotService _robotService = robotService ?? throw new ArgumentNullException(nameof(robotService));
 043  private readonly ITriggerService _triggerService = triggerService ?? throw new ArgumentNullException(nameof(triggerSer
 044  private readonly LgdxContext _context = context ?? throw new ArgumentNullException(nameof(context));
 45
 46  private async Task AddAutoTaskJourney(AutoTask autoTask)
 047  {
 048    var autoTaskJourney = new AutoTaskJourney
 049    {
 050      AutoTaskId = autoTask.Id,
 051      CurrentProgressId = autoTask.CurrentProgressId
 052    };
 053    await _context.AutoTasksJourney.AddAsync(autoTaskJourney);
 054    await _context.SaveChangesAsync();
 055  }
 56
 57  private async Task<RobotClientsAutoTask?> GenerateTaskDetail(AutoTask task, bool continueAutoTask = false)
 058  {
 059    var progress = await _context.Progresses.AsNoTracking()
 060      .Where(p => p.Id == task.CurrentProgressId)
 061      .Select(p => new { p.Name })
 062      .FirstOrDefaultAsync();
 063    if (!continueAutoTask)
 064    {
 65      // Notify the updated task
 066      var flowName = await _context.Flows.AsNoTracking()
 067        .Where(f => f.Id == task.FlowId)
 068        .Select(f => f.Name)
 069        .FirstOrDefaultAsync();
 070      string? robotName = null;
 071      if (task.AssignedRobotId != null)
 072      {
 073        robotName = await _context.Robots.AsNoTracking()
 074          .Where(r => r.Id == task.AssignedRobotId)
 075          .Select(r => r.Name)
 076          .FirstOrDefaultAsync();
 077      }
 78
 79      // Send the updated to the Redis queue
 080      var data = new AutoTaskUpdate
 081      {
 082        Id = task.Id,
 083        Name = task.Name,
 084        Priority = task.Priority,
 085        FlowId = task.FlowId ?? 0,
 086        FlowName = flowName ?? "Deleted Flow",
 087        RealmId = task.RealmId,
 088        AssignedRobotId = task.AssignedRobotId,
 089        AssignedRobotName = robotName,
 090        CurrentProgressId = task.CurrentProgressId,
 091        CurrentProgressName = progress!.Name,
 092      };
 093      await _autoTaskRepository.AutoTaskHasUpdateAsync(task.RealmId, data);
 094    }
 95
 096    if (task.CurrentProgressId == (int)ProgressState.Completed || task.CurrentProgressId == (int)ProgressState.Aborted)
 097    {
 98      // Return immediately if the task is completed / aborted
 099      return new RobotClientsAutoTask
 0100      {
 0101        TaskId = task.Id,
 0102        TaskName = task.Name ?? string.Empty,
 0103        TaskProgressId = task.CurrentProgressId,
 0104        TaskProgressName = progress!.Name ?? string.Empty,
 0105        Paths = { },
 0106        NextToken = string.Empty,
 0107      };
 108    }
 109
 0110    var flowDetail = await _context.FlowDetails.AsNoTracking()
 0111      .Where(fd => fd.FlowId == task.FlowId && fd.Order == (int)task.CurrentProgressOrder!)
 0112      .FirstOrDefaultAsync();
 0113    if (!continueAutoTask && flowDetail!.TriggerId != null)
 0114    {
 115      // Fire the trigger
 0116      await _triggerService.InitialiseTriggerAsync(task, flowDetail);
 0117    }
 118
 0119    List<RobotClientsPath> paths = [];
 120    try
 0121    {
 0122      paths = await _autoTaskPathPlanner.GeneratePath(task);
 0123    }
 0124    catch (Exception)
 0125    {
 0126      await AutoTaskAbortSqlAsync(task.Id);
 0127      await _emailService.SendAutoTaskAbortEmailAsync(task.Id, AutoTaskAbortReason.PathPlanner);
 0128      await AddAutoTaskJourney(task);
 0129      return null;
 130    }
 131
 0132    string nextToken = task.NextToken ?? string.Empty;
 0133    if (flowDetail?.AutoTaskNextControllerId != (int)AutoTaskNextController.Robot)
 0134    {
 135      // API has the control
 0136      nextToken = string.Empty;
 0137    }
 138
 0139    return new RobotClientsAutoTask
 0140    {
 0141      TaskId = task.Id,
 0142      TaskName = task.Name ?? string.Empty,
 0143      TaskProgressId = task.CurrentProgressId,
 0144      TaskProgressName = progress!.Name ?? string.Empty,
 0145      Paths = { paths },
 0146      NextToken = nextToken,
 0147    };
 0148  }
 149
 150  public async Task RunSchedulerNewAutoTaskAsync(int realmId, Guid? robotId)
 0151  {
 152    // Find any robot that is idle
 0153    if (robotId == null)
 0154    {
 0155      robotId = await _autoTaskRepository.SchedulerHoldAnyRobotAsync(realmId);
 0156      if (robotId != null)
 0157      {
 0158        await RunSchedulerRobotReadyAsync(robotId.Value);
 159        // Release the robot when it obtains the task
 0160      }
 0161    }
 162    else
 0163    {
 0164      if (await _autoTaskRepository.SchedulerHoldRobotAsync(realmId, robotId.Value))
 0165      {
 0166        await RunSchedulerRobotReadyAsync(robotId.Value);
 167        // Release the robot when it obtains the task
 0168      }
 0169    }
 0170  }
 171
 172  private async Task<AutoTask?> GetRunningAutoTaskSqlAsync(Guid robotId)
 0173  {
 0174    return await _context.AutoTasks.AsNoTracking()
 0175      .Include(t => t.AutoTaskDetails)
 0176      .Where(t => t.AssignedRobotId == robotId)
 0177      .Where(t => !LgdxHelper.AutoTaskStaticStates.Contains(t.CurrentProgressId))
 0178      .OrderByDescending(t => t.Priority) // In case the robot has multiple running task by mistake
 0179      .ThenByDescending(t => t.AssignedRobotId)
 0180      .ThenBy(t => t.Id)
 0181      .FirstOrDefaultAsync();
 0182  }
 183
 184  public async Task RunSchedulerRobotNewJoinAsync(Guid robotId)
 0185  {
 0186    if (await _onlineRobotsService.GetPauseAutoTaskAssignmentAsync(robotId))
 0187    {
 0188      return;
 189    }
 190
 0191    var runningAutoTask = await GetRunningAutoTaskSqlAsync(robotId);
 0192    if (runningAutoTask != null)
 0193    {
 194      // Send the running task to the robot
 0195      var task = await GenerateTaskDetail(runningAutoTask, true);
 0196      if (task != null)
 0197      {
 0198        var realmId = await _robotService.GetRobotRealmIdAsync(robotId) ?? 0;
 0199        await _autoTaskRepository.AddAutoTaskAsync(realmId, robotId, task);
 0200      }
 0201      return;
 202    }
 203    // Assign the task to the robot
 0204    await RunSchedulerRobotReadyAsync(robotId);
 0205  }
 206
 207  private async Task<AutoTask?> GetNextAutoTaskSqlAsync(Guid robotId, int realmId)
 0208  {
 0209    AutoTask? task = null;
 0210    using var transaction = await _context.Database.BeginTransactionAsync();
 211    try
 0212    {
 213      // Get waiting task
 0214      task = await _context.AutoTasks.FromSql(
 0215        $@"SELECT * FROM ""Automation.AutoTasks"" AS T
 0216            WHERE T.""CurrentProgressId"" = {(int)ProgressState.Waiting} AND (T.""AssignedRobotId"" = {robotId} OR T.""A
 0217            ORDER BY T.""Priority"" DESC, T.""AssignedRobotId"" DESC, T.""Id""
 0218            LIMIT 1 FOR UPDATE SKIP LOCKED"
 0219      ).FirstOrDefaultAsync();
 0220      if (task == null)
 0221      {
 0222        return null;
 223      }
 224
 225      // Get flow detail
 0226      var flowDetail = await _context.FlowDetails
 0227        .Where(f => f.FlowId == task!.FlowId)
 0228        .Select(f => new
 0229        {
 0230          f.ProgressId,
 0231          f.Order
 0232        })
 0233        .OrderBy(f => f.Order)
 0234        .FirstOrDefaultAsync();
 235
 236      // Update task
 0237      task!.AssignedRobotId = robotId;
 0238      task.CurrentProgressId = flowDetail!.ProgressId;
 0239      task.CurrentProgressOrder = flowDetail.Order;
 0240      task.NextToken = LgdxHelper.GenerateMd5Hash($"{robotId} {task.Id} {task.CurrentProgressId} {DateTime.UtcNow}");
 0241      await _context.SaveChangesAsync();
 0242      await transaction.CommitAsync();
 0243      return task;
 244    }
 0245    catch (Exception)
 0246    {
 0247      await transaction.RollbackAsync();
 0248    }
 0249    return null;
 0250  }
 251
 252  public async Task<bool> RunSchedulerRobotReadyAsync(Guid robotId)
 0253  {
 0254    if (await _onlineRobotsService.GetPauseAutoTaskAssignmentAsync(robotId))
 0255    {
 0256      return false;
 257    }
 258
 0259    var realmId = await _robotService.GetRobotRealmIdAsync(robotId) ?? 0;
 0260    var newTask = await GetNextAutoTaskSqlAsync(robotId, realmId);
 0261    if (newTask != null)
 0262    {
 0263      await AddAutoTaskJourney(newTask);
 264      // Send the running task to the robot
 0265      var task = await GenerateTaskDetail(newTask);
 0266      if (task != null)
 0267      {
 0268        await _autoTaskRepository.AddAutoTaskAsync(realmId, robotId, task);
 0269      }
 270      // Has new task
 0271      return true;
 272    }
 0273    return false;
 0274  }
 275
 276  public async Task ReleaseRobotAsync(Guid robotId)
 0277  {
 0278    var realmId = _robotService.GetRobotRealmIdAsync(robotId).Result ?? 0;
 0279    await _autoTaskRepository.SchedulerReleaseRobotAsync(realmId, robotId);
 0280  }
 281
 282  private async Task<AutoTask?> AutoTaskAbortSqlAsync(int taskId, Guid? robotId = null, string? token = null)
 0283  {
 0284    AutoTask? task = null;
 0285    using var transaction = await _context.Database.BeginTransactionAsync();
 286    try
 0287    {
 288      // Get task
 0289      if (robotId == null && string.IsNullOrWhiteSpace(token))
 0290      {
 291        // From API
 0292        task = await _context.AutoTasks.FromSql(
 0293          $@"SELECT * FROM ""Automation.AutoTasks"" AS T
 0294            WHERE T.""Id"" = {taskId}
 0295            LIMIT 1 FOR UPDATE NOWAIT"
 0296        ).FirstOrDefaultAsync();
 0297      }
 298      else
 0299      {
 0300        task = await _context.AutoTasks.FromSql(
 0301          $@"SELECT * FROM ""Automation.AutoTasks"" AS T
 0302              WHERE T.""Id"" = {taskId} AND T.""AssignedRobotId"" = {robotId} AND T.""NextToken"" = {token}
 0303              LIMIT 1 FOR UPDATE NOWAIT"
 0304        ).FirstOrDefaultAsync();
 0305      }
 0306      if (task == null)
 0307      {
 0308        return null;
 309      }
 310
 311      // Update task
 0312      task!.CurrentProgressId = (int)ProgressState.Aborted;
 0313      task.CurrentProgressOrder = null;
 0314      task.NextToken = null;
 0315      await _context.SaveChangesAsync();
 0316      await transaction.CommitAsync();
 0317    }
 0318    catch (Exception)
 0319    {
 0320      await transaction.RollbackAsync();
 0321    }
 0322    return task;
 0323  }
 324
 325  private async Task DeleteTriggerRetries(int taskId)
 0326  {
 0327    var count = await _context.TriggerRetries.Where(tr => tr.AutoTaskId == taskId).CountAsync();
 0328    if (count > 0)
 0329    {
 0330      await _context.TriggerRetries.Where(tr => tr.AutoTaskId == taskId).ExecuteDeleteAsync();
 0331    }
 0332  }
 333
 334  public async Task AutoTaskAbortAsync(Guid robotId, int taskId, string token, AutoTaskAbortReason autoTaskAbortReason)
 0335  {
 0336    var task = await AutoTaskAbortSqlAsync(taskId, robotId, token);
 0337    if (task == null)
 0338    {
 0339      return;
 340    }
 341
 0342    await DeleteTriggerRetries(taskId);
 0343    await _emailService.SendAutoTaskAbortEmailAsync(taskId, autoTaskAbortReason);
 0344    await AddAutoTaskJourney(task);
 345    // Allow update the task to rabbitmq
 0346    var sendTask = await GenerateTaskDetail(task);
 0347    if (!await RunSchedulerRobotReadyAsync(robotId))
 0348    {
 349      // No new task, send the aborted task to idle the robot
 0350      if (sendTask != null)
 0351      {
 0352        var realmId = await _robotService.GetRobotRealmIdAsync(robotId) ?? 0;
 0353        await _autoTaskRepository.AddAutoTaskAsync(realmId, robotId, sendTask);
 0354      }
 0355    }
 0356  }
 357
 358  public async Task<bool> AutoTaskAbortApiAsync(int taskId)
 0359  {
 0360    var task = await AutoTaskAbortSqlAsync(taskId);
 0361    if (task == null)
 0362    {
 0363      return false;
 364    }
 365
 0366    await DeleteTriggerRetries(taskId);
 0367    await _emailService.SendAutoTaskAbortEmailAsync(taskId, AutoTaskAbortReason.UserApi);
 0368    await AddAutoTaskJourney(task);
 369    // Allow update the task to rabbitmq
 0370    var sendTask = await GenerateTaskDetail(task);
 0371    if (task.AssignedRobotId != null && !await RunSchedulerRobotReadyAsync(task.AssignedRobotId.Value))
 0372    {
 373      // No new task, send the aborted task to idle the robot
 0374      if (sendTask != null)
 0375      {
 0376        var realmId = await _robotService.GetRobotRealmIdAsync(task.AssignedRobotId!.Value) ?? 0;
 0377        await _autoTaskRepository.AddAutoTaskAsync(realmId, task.AssignedRobotId!.Value, sendTask);
 0378      }
 0379    }
 0380    return true;
 0381  }
 382
 383  private async Task<AutoTask?> AutoTaskNextSqlAsync(Guid robotId, int taskId, string token)
 0384  {
 0385    AutoTask? task = null;
 0386    using var transaction = await _context.Database.BeginTransactionAsync();
 387    try
 0388    {
 389      // Get waiting task
 0390      task = await _context.AutoTasks.FromSql(
 0391        $@"SELECT * FROM ""Automation.AutoTasks"" AS T
 0392            WHERE T.""Id"" = {taskId} AND T.""AssignedRobotId"" = {robotId} AND T.""NextToken"" = {token}
 0393            LIMIT 1 FOR UPDATE NOWAIT"
 0394      ).FirstOrDefaultAsync();
 0395      if (task == null)
 0396      {
 0397        return null;
 398      }
 399
 400      // Get flow detail
 0401      var flowDetail = await _context.FlowDetails.AsNoTracking()
 0402        .Where(f => f.FlowId == task!.FlowId)
 0403        .Where(f => f.Order > task!.CurrentProgressOrder)
 0404        .Select(f => new
 0405        {
 0406          f.ProgressId,
 0407          f.Order
 0408        })
 0409        .OrderBy(f => f.Order)
 0410        .FirstOrDefaultAsync();
 411
 412      // Update task
 0413      if (flowDetail != null)
 0414      {
 0415        task!.CurrentProgressId = flowDetail!.ProgressId;
 0416        task.CurrentProgressOrder = flowDetail.Order;
 0417        task.NextToken = LgdxHelper.GenerateMd5Hash($"{robotId} {task.Id} {task.CurrentProgressId} {DateTime.UtcNow}");
 0418      }
 419      else
 0420      {
 0421        task!.CurrentProgressId = (int)ProgressState.Completed;
 0422        task.CurrentProgressOrder = null;
 0423        task.NextToken = null;
 0424      }
 425
 0426      await _context.SaveChangesAsync();
 0427      await transaction.CommitAsync();
 0428    }
 0429    catch (Exception)
 0430    {
 0431      await transaction.RollbackAsync();
 0432    }
 0433    return task;
 0434  }
 435
 436  public async Task AutoTaskNextAsync(Guid robotId, int taskId, string token)
 0437  {
 0438    var task = await AutoTaskNextSqlAsync(robotId, taskId, token);
 0439    if (task == null)
 0440    {
 0441      return;
 442    }
 443
 0444    await AddAutoTaskJourney(task);
 0445    if (task.CurrentProgressId == (int)ProgressState.Completed && await RunSchedulerRobotReadyAsync(robotId))
 0446    {
 447      // Allow update the task to rabbitmq
 0448      await GenerateTaskDetail(task);
 449      // Completed task, and new task available
 0450      return;
 451    }
 452
 453    // Send the running task / complete task to the robot
 0454    var sendTask = await GenerateTaskDetail(task);
 0455    if (sendTask != null)
 0456    {
 0457      var realmId = await _robotService.GetRobotRealmIdAsync(robotId) ?? 0;
 0458      await _autoTaskRepository.AddAutoTaskAsync(realmId, robotId, sendTask);
 0459    }
 0460  }
 461
 462  public async Task<bool> AutoTaskNextApiAsync(Guid robotId, int taskId, string token)
 0463  {
 0464    var task = await AutoTaskNextSqlAsync(robotId, taskId, token);
 0465    if (task == null)
 0466    {
 0467      return false;
 468    }
 469
 0470    await AddAutoTaskJourney(task);
 0471    if (task.CurrentProgressId == (int)ProgressState.Completed && await RunSchedulerRobotReadyAsync(robotId))
 0472    {
 473      // Allow update the task to rabbitmq
 0474      await GenerateTaskDetail(task);
 475      // Completed task, and new task available
 0476      return true;
 477    }
 478
 479    // Send the running task / complete task to the robot
 0480    var sendTask = await GenerateTaskDetail(task);
 0481    if (sendTask != null)
 0482    {
 0483      var realmId = await _robotService.GetRobotRealmIdAsync(robotId) ?? 0;
 0484      await _autoTaskRepository.AddAutoTaskAsync(realmId, robotId, sendTask);
 0485    }
 0486    return true;
 0487  }
 488}