< 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
96%
Covered lines: 285
Uncovered lines: 9
Coverable lines: 294
Total lines: 393
Line coverage: 96.9%
Branch coverage
81%
Covered branches: 73
Total branches: 90
Branch coverage: 81.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%1616100%
GetIgnoreRobotsKey(...)100%11100%
AddAutoTaskJourney()100%11100%
GenerateTaskDetail()76.66%303092.4%
ResetIgnoreRobot(...)100%11100%
GetRunningAutoTaskSqlAsync()100%11100%
AssignAutoTaskSqlAsync()100%44100%
GetAutoTaskAsync()93.75%1616100%
AutoTaskAbortSqlAsync()100%88100%
DeleteTriggerRetries()50%2257.14%
AutoTaskAbortAsync()100%22100%
AutoTaskAbortApiAsync()100%22100%
AutoTaskNextSqlAsync()100%66100%
AutoTaskNextAsync()100%22100%
AutoTaskNextApiAsync()100%22100%
AutoTaskNextConstructAsync()100%11100%

File(s)

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

#LineLine coverage
 1using LGDXRobotCloud.API.Services.Common;
 2using LGDXRobotCloud.API.Services.Navigation;
 3using LGDXRobotCloud.Data.Contracts;
 4using LGDXRobotCloud.Data.DbContexts;
 5using LGDXRobotCloud.Data.Entities;
 6using LGDXRobotCloud.Protos;
 7using LGDXRobotCloud.Utilities.Enums;
 8using LGDXRobotCloud.Utilities.Helpers;
 9using MassTransit;
 10using Microsoft.EntityFrameworkCore;
 11using Microsoft.Extensions.Caching.Distributed;
 12using Microsoft.Extensions.Caching.Memory;
 13
 14namespace LGDXRobotCloud.API.Services.Automation;
 15
 16public interface IAutoTaskSchedulerService
 17{
 18  void ResetIgnoreRobot(int realmId);
 19  Task<RobotClientsAutoTask?> GetAutoTaskAsync(Guid robotId);
 20  Task<RobotClientsAutoTask?> AutoTaskAbortAsync(Guid robotId, int taskId, string token, AutoTaskAbortReason autoTaskAbo
 21  Task<bool> AutoTaskAbortApiAsync(int taskId);
 22  Task<RobotClientsAutoTask?> AutoTaskNextAsync(Guid robotId, int taskId, string token);
 23  Task<AutoTask?> AutoTaskNextApiAsync(Guid robotId, int taskId, string token);
 24  Task<RobotClientsAutoTask?> AutoTaskNextConstructAsync(AutoTask autoTask);
 25}
 26
 2027public class AutoTaskSchedulerService(
 2028    IAutoTaskPathPlannerService autoTaskPathPlanner,
 2029    IBus bus,
 2030    IEmailService emailService,
 2031    IMemoryCache memoryCache,
 2032    IOnlineRobotsService onlineRobotsService,
 2033    IRobotService robotService,
 2034    ITriggerService triggerService,
 2035    LgdxContext context
 2036  ) : IAutoTaskSchedulerService
 37{
 2038  private readonly IAutoTaskPathPlannerService _autoTaskPathPlanner = autoTaskPathPlanner ?? throw new ArgumentNullExcep
 2039  private readonly IBus _bus = bus ?? throw new ArgumentNullException(nameof(bus));
 2040  private readonly IEmailService _emailService = emailService ?? throw new ArgumentNullException(nameof(emailService));
 2041  private readonly IMemoryCache _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
 2042  private readonly IOnlineRobotsService _onlineRobotsService = onlineRobotsService ?? throw new ArgumentNullException(na
 2043  private readonly IRobotService _robotService = robotService ?? throw new ArgumentNullException(nameof(robotService));
 2044  private readonly ITriggerService _triggerService = triggerService ?? throw new ArgumentNullException(nameof(triggerSer
 2045  private readonly LgdxContext _context = context ?? throw new ArgumentNullException(nameof(context));
 1146  private static string GetIgnoreRobotsKey(int realmId) => $"AutoTaskSchedulerService_IgnoreRobot_{realmId}";
 47
 48  private async Task AddAutoTaskJourney(AutoTask autoTask)
 749  {
 750    var autoTaskJourney = new AutoTaskJourney
 751    {
 752      AutoTaskId = autoTask.Id,
 753      CurrentProgressId = autoTask.CurrentProgressId
 754    };
 755    await _context.AutoTasksJourney.AddAsync(autoTaskJourney);
 756    await _context.SaveChangesAsync();
 757  }
 58
 59  private async Task<RobotClientsAutoTask?> GenerateTaskDetail(AutoTask? task, bool continueAutoTask = false)
 1360  {
 1361    if (task == null)
 362    {
 363      return null;
 64    }
 65
 1066    var progress = await _context.Progresses.AsNoTracking()
 1067      .Where(p => p.Id == task.CurrentProgressId)
 1068      .Select(p => new { p.Name })
 1069      .FirstOrDefaultAsync();
 1070    if (!continueAutoTask)
 671    {
 72      // Notify the updated task
 673      var flowName = await _context.Flows.AsNoTracking()
 674        .Where(f => f.Id == task.FlowId)
 675        .Select(f => f.Name)
 676        .FirstOrDefaultAsync();
 677      string? robotName = null;
 678      if (task.AssignedRobotId != null)
 679      {
 680        robotName = await _context.Robots.AsNoTracking()
 681          .Where(r => r.Id == task.AssignedRobotId)
 682          .Select(r => r.Name)
 683          .FirstOrDefaultAsync();
 684      }
 685      await _bus.Publish(new AutoTaskUpdateContract
 686      {
 687        Id = task.Id,
 688        Name = task.Name,
 689        Priority = task.Priority,
 690        FlowId = task.FlowId ?? 0,
 691        FlowName = flowName ?? "Deleted Flow",
 692        RealmId = task.RealmId,
 693        AssignedRobotId = task.AssignedRobotId,
 694        AssignedRobotName = robotName,
 695        CurrentProgressId = task.CurrentProgressId,
 696        CurrentProgressName = progress!.Name,
 697      });
 698    }
 99
 10100    if (task.CurrentProgressId == (int)ProgressState.Completed || task.CurrentProgressId == (int)ProgressState.Aborted)
 2101    {
 102      // Return immediately if the task is completed / aborted
 2103      return new RobotClientsAutoTask
 2104      {
 2105        TaskId = task.Id,
 2106        TaskName = task.Name ?? string.Empty,
 2107        TaskProgressId = task.CurrentProgressId,
 2108        TaskProgressName = progress!.Name ?? string.Empty,
 2109        Paths = { },
 2110        NextToken = string.Empty,
 2111      };
 112    }
 113
 8114    var flowDetail = await _context.FlowDetails.AsNoTracking()
 8115      .Where(fd => fd.FlowId == task.FlowId && fd.Order == (int)task.CurrentProgressOrder!)
 8116      .FirstOrDefaultAsync();
 8117    if (!continueAutoTask && flowDetail!.TriggerId != null)
 1118    {
 119      // Fire the trigger
 1120      await _triggerService.InitialiseTriggerAsync(task, flowDetail);
 1121    }
 122
 8123    List<RobotClientsPath> paths = [];
 124    try
 8125    {
 8126      paths = await _autoTaskPathPlanner.GeneratePath(task);
 8127    }
 0128    catch (Exception)
 0129    {
 0130      await AutoTaskAbortSqlAsync(task.Id);
 0131      await _emailService.SendAutoTaskAbortEmailAsync(task.Id, AutoTaskAbortReason.PathPlanner);
 0132      await AddAutoTaskJourney(task);
 0133      return null;
 134    }
 135
 8136    string nextToken = task.NextToken ?? string.Empty;
 8137    if (flowDetail?.AutoTaskNextControllerId != (int)AutoTaskNextController.Robot)
 1138    {
 139      // API has the control
 1140      nextToken = string.Empty;
 1141    }
 142
 8143    return new RobotClientsAutoTask
 8144    {
 8145      TaskId = task.Id,
 8146      TaskName = task.Name ?? string.Empty,
 8147      TaskProgressId = task.CurrentProgressId,
 8148      TaskProgressName = progress!.Name ?? string.Empty,
 8149      Paths = { paths },
 8150      NextToken = nextToken,
 8151    };
 13152  }
 153
 154  public void ResetIgnoreRobot(int realmId)
 1155  {
 1156    _memoryCache.Remove(GetIgnoreRobotsKey(realmId));
 1157  }
 158
 159  private async Task<AutoTask?> GetRunningAutoTaskSqlAsync(Guid robotId)
 8160  {
 8161    return await _context.AutoTasks.AsNoTracking()
 8162      .Include(t => t.AutoTaskDetails)
 8163      .Where(t => t.AssignedRobotId == robotId)
 8164      .Where(t => !LgdxHelper.AutoTaskStaticStates.Contains(t.CurrentProgressId))
 8165      .OrderByDescending(t => t.Priority) // In case the robot has multiple running task by mistake
 8166      .ThenByDescending(t => t.AssignedRobotId)
 8167      .ThenBy(t => t.Id)
 8168      .FirstOrDefaultAsync();
 8169  }
 170
 171  private async Task<AutoTask?> AssignAutoTaskSqlAsync(Guid robotId)
 3172  {
 3173    AutoTask? task = null;
 3174    using var transaction = await _context.Database.BeginTransactionAsync();
 175    try
 3176    {
 177      // Get waiting task
 3178      task = await _context.AutoTasks.FromSql(
 3179        $@"SELECT * FROM ""Automation.AutoTasks"" AS T
 3180            WHERE T.""CurrentProgressId"" = {(int)ProgressState.Waiting} AND (T.""AssignedRobotId"" = {robotId} OR T.""A
 3181            ORDER BY T.""Priority"" DESC, T.""AssignedRobotId"" DESC, T.""Id""
 3182            LIMIT 1 FOR UPDATE SKIP LOCKED"
 3183      ).FirstOrDefaultAsync();
 184
 185      // Get flow detail
 3186      var flowDetail = await _context.FlowDetails
 3187        .Where(f => f.FlowId == task!.FlowId)
 3188        .Select(f => new {
 3189          f.ProgressId,
 3190          f.Order
 3191        })
 3192        .OrderBy(f => f.Order)
 3193        .FirstOrDefaultAsync();
 194
 195      // Update task
 2196      task!.AssignedRobotId = robotId;
 2197      task.CurrentProgressId = flowDetail!.ProgressId;
 2198      task.CurrentProgressOrder = flowDetail.Order;
 2199      task.NextToken = LgdxHelper.GenerateMd5Hash($"{robotId} {task.Id} {task.CurrentProgressId} {DateTime.UtcNow}");
 2200      await _context.SaveChangesAsync();
 2201      await transaction.CommitAsync();
 2202      return task;
 203    }
 1204    catch (Exception)
 1205    {
 1206      await transaction.RollbackAsync();
 1207    }
 1208    return null;
 3209  }
 210
 211  public async Task<RobotClientsAutoTask?> GetAutoTaskAsync(Guid robotId)
 9212  {
 9213    var realmId = await _robotService.GetRobotRealmIdAsync(robotId) ?? 0;
 9214    _memoryCache.TryGetValue(GetIgnoreRobotsKey(realmId), out HashSet<Guid>? ignoreRobotIds);
 9215    if (ignoreRobotIds != null && (ignoreRobotIds?.Contains(robotId) ?? false))
 1216    {
 1217      return null;
 218    }
 219
 8220    var currentTask = await GetRunningAutoTaskSqlAsync(robotId);
 8221    bool continueAutoTask = currentTask != null;
 8222    if (currentTask == null)
 4223    {
 4224      if (!_onlineRobotsService.GetPauseAutoTaskAssignment(robotId))
 3225      {
 3226        currentTask = await AssignAutoTaskSqlAsync(robotId);
 3227        if (currentTask != null)
 2228        {
 2229          await AddAutoTaskJourney(currentTask);
 2230        }
 3231      }
 232      else
 1233      {
 234        // If pause auto task assignment is true, new task will not be assigned.
 1235        return null;
 236      }
 3237    }
 238
 7239    if (currentTask == null)
 1240    {
 241      // No task for this robot, pause database access.
 1242      ignoreRobotIds ??= [];
 1243      ignoreRobotIds.Add(robotId);
 1244      _memoryCache.Set(GetIgnoreRobotsKey(realmId), ignoreRobotIds, TimeSpan.FromMinutes(5));
 1245    }
 7246    return await GenerateTaskDetail(currentTask, continueAutoTask);
 9247  }
 248
 249  private async Task<AutoTask?> AutoTaskAbortSqlAsync(int taskId, Guid? robotId = null, string? token = null)
 4250  {
 4251    AutoTask? task = null;
 4252    using var transaction = await _context.Database.BeginTransactionAsync();
 253    try
 4254    {
 255      // Get task
 4256      if (robotId == null && string.IsNullOrWhiteSpace(token))
 2257      {
 258        // From API
 2259        task = await _context.AutoTasks.FromSql(
 2260          $@"SELECT * FROM ""Automation.AutoTasks"" AS T
 2261            WHERE T.""Id"" = {taskId}
 2262            LIMIT 1 FOR UPDATE NOWAIT"
 2263        ).FirstOrDefaultAsync();
 2264      }
 265      else
 2266      {
 2267        task = await _context.AutoTasks.FromSql(
 2268          $@"SELECT * FROM ""Automation.AutoTasks"" AS T
 2269              WHERE T.""Id"" = {taskId} AND T.""AssignedRobotId"" = {robotId} AND T.""NextToken"" = {token}
 2270              LIMIT 1 FOR UPDATE NOWAIT"
 2271        ).FirstOrDefaultAsync();
 2272      }
 273
 274      // Update task
 4275      task!.CurrentProgressId = (int)ProgressState.Aborted;
 2276      task.CurrentProgressOrder = null;
 2277      task.NextToken = null;
 2278      await _context.SaveChangesAsync();
 2279      await transaction.CommitAsync();
 2280    }
 2281    catch (Exception)
 2282    {
 2283      await transaction.RollbackAsync();
 2284    }
 4285    return task;
 4286  }
 287
 288  private async Task DeleteTriggerRetries(int taskId)
 2289  {
 2290    var count = await _context.TriggerRetries.Where(tr => tr.AutoTaskId == taskId).CountAsync();
 2291    if (count > 0)
 0292    {
 0293      await _context.TriggerRetries.Where(tr => tr.AutoTaskId == taskId).ExecuteDeleteAsync();
 0294    }
 2295  }
 296
 297  public async Task<RobotClientsAutoTask?> AutoTaskAbortAsync(Guid robotId, int taskId, string token, AutoTaskAbortReaso
 2298  {
 2299    var task = await AutoTaskAbortSqlAsync(taskId, robotId, token);
 2300    if (task != null)
 1301    {
 1302      await DeleteTriggerRetries(taskId);
 1303      await _emailService.SendAutoTaskAbortEmailAsync(taskId, autoTaskAbortReason);
 1304      await AddAutoTaskJourney(task);
 1305    }
 2306    return await GenerateTaskDetail(task);
 2307  }
 308
 309  public async Task<bool> AutoTaskAbortApiAsync(int taskId)
 2310  {
 2311    var task = await AutoTaskAbortSqlAsync(taskId);
 2312    if (task == null)
 1313      return false;
 314
 1315    await DeleteTriggerRetries(taskId);
 1316    await _emailService.SendAutoTaskAbortEmailAsync(taskId, AutoTaskAbortReason.UserApi);
 1317    await AddAutoTaskJourney(task);
 1318    return true;
 2319  }
 320
 321  private async Task<AutoTask?> AutoTaskNextSqlAsync(Guid robotId, int taskId, string token)
 5322  {
 5323    AutoTask? task = null;
 5324    using var transaction = await _context.Database.BeginTransactionAsync();
 325    try
 5326    {
 327      // Get waiting task
 5328      task = await _context.AutoTasks.FromSql(
 5329        $@"SELECT * FROM ""Automation.AutoTasks"" AS T
 5330            WHERE T.""Id"" = {taskId} AND T.""AssignedRobotId"" = {robotId} AND T.""NextToken"" = {token}
 5331            LIMIT 1 FOR UPDATE NOWAIT"
 5332      ).FirstOrDefaultAsync();
 333
 334      // Get flow detail
 5335      var flowDetail = await _context.FlowDetails.AsNoTracking()
 5336        .Where(f => f.FlowId == task!.FlowId)
 5337        .Where(f => f.Order > task!.CurrentProgressOrder)
 5338        .Select(f => new {
 5339          f.ProgressId,
 5340          f.Order
 5341        })
 5342        .OrderBy(f => f.Order)
 5343        .FirstOrDefaultAsync();
 344
 345      // Update task
 3346      if (flowDetail != null)
 2347      {
 2348        task!.CurrentProgressId = flowDetail!.ProgressId;
 2349        task.CurrentProgressOrder = flowDetail.Order;
 2350        task.NextToken = LgdxHelper.GenerateMd5Hash($"{robotId} {task.Id} {task.CurrentProgressId} {DateTime.UtcNow}");
 2351      }
 352      else
 1353      {
 1354        task!.CurrentProgressId = (int)ProgressState.Completed;
 1355        task.CurrentProgressOrder = null;
 1356        task.NextToken = null;
 1357      }
 358
 3359      await _context.SaveChangesAsync();
 3360      await transaction.CommitAsync();
 3361    }
 2362    catch (Exception)
 2363    {
 2364      await transaction.RollbackAsync();
 2365    }
 5366    return task;
 5367  }
 368
 369  public async Task<RobotClientsAutoTask?> AutoTaskNextAsync(Guid robotId, int taskId, string token)
 3370  {
 3371    var task = await AutoTaskNextSqlAsync(robotId, taskId, token);
 3372    if (task != null)
 2373    {
 2374      await AddAutoTaskJourney(task);
 2375    }
 3376    return await GenerateTaskDetail(task);
 3377  }
 378
 379  public async Task<AutoTask?> AutoTaskNextApiAsync(Guid robotId, int taskId, string token)
 2380  {
 2381    var task = await AutoTaskNextSqlAsync(robotId, taskId, token);
 2382    if (task != null)
 1383    {
 1384      await AddAutoTaskJourney(task);
 1385    }
 2386    return task;
 2387  }
 388
 389  public async Task<RobotClientsAutoTask?> AutoTaskNextConstructAsync(AutoTask autoTask)
 1390  {
 1391    return await GenerateTaskDetail(autoTask);
 1392  }
 393}