< 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
99%
Covered lines: 319
Uncovered lines: 3
Coverable lines: 322
Total lines: 424
Line coverage: 99%
Branch coverage
85%
Covered branches: 94
Total branches: 110
Branch coverage: 85.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%1414100%
GetIgnoreRobotsKey(...)100%11100%
AddAutoTaskJourney()100%11100%
GenerateWaypoint(...)100%1414100%
GenerateTaskDetail()81.57%3838100%
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    IBus bus,
 2029    IEmailService emailService,
 2030    IMemoryCache memoryCache,
 2031    IOnlineRobotsService onlineRobotsService,
 2032    IRobotService robotService,
 2033    ITriggerService triggerService,
 2034    LgdxContext context
 2035  ) : IAutoTaskSchedulerService
 36{
 2037  private readonly IBus _bus = bus ?? throw new ArgumentNullException(nameof(bus));
 2038  private readonly IEmailService _emailService = emailService ?? throw new ArgumentNullException(nameof(emailService));
 2039  private readonly IMemoryCache _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
 2040  private readonly IOnlineRobotsService _onlineRobotsService = onlineRobotsService ?? throw new ArgumentNullException(na
 2041  private readonly IRobotService _robotService = robotService ?? throw new ArgumentNullException(nameof(robotService));
 2042  private readonly ITriggerService _triggerService = triggerService ?? throw new ArgumentNullException(nameof(triggerSer
 2043  private readonly LgdxContext _context = context ?? throw new ArgumentNullException(nameof(context));
 1144  private static string GetIgnoreRobotsKey(int realmId) => $"AutoTaskSchedulerService_IgnoreRobot_{realmId}";
 45
 46  private async Task AddAutoTaskJourney(AutoTask autoTask)
 747  {
 748    var autoTaskJourney = new AutoTaskJourney
 749    {
 750      AutoTaskId = autoTask.Id,
 751      CurrentProgressId = autoTask.CurrentProgressId
 752    };
 753    await _context.AutoTasksJourney.AddAsync(autoTaskJourney);
 754    await _context.SaveChangesAsync();
 755  }
 56
 57  private static RobotClientsDof GenerateWaypoint(AutoTaskDetail taskDetail)
 858  {
 859    if (taskDetail.Waypoint != null)
 460    {
 461      var waypoint = new RobotClientsDof
 462      { X = taskDetail.Waypoint.X, Y = taskDetail.Waypoint.Y, Rotation = taskDetail.Waypoint.Rotation };
 463      if (taskDetail.CustomX != null)
 164        waypoint.X = (double)taskDetail.CustomX;
 465      if (taskDetail.CustomY != null)
 166        waypoint.X = (double)taskDetail.CustomY;
 467      if (taskDetail.CustomRotation != null)
 168        waypoint.X = (double)taskDetail.CustomRotation;
 469      return waypoint;
 70    }
 71    else
 472    {
 473      return new RobotClientsDof
 474      {
 475        X = taskDetail.CustomX != null ? (double)taskDetail.CustomX : 0,
 476        Y = taskDetail.CustomY != null ? (double)taskDetail.CustomY : 0,
 477        Rotation = taskDetail.CustomRotation != null ? (double)taskDetail.CustomRotation : 0
 478      };
 79    }
 880  }
 81
 82  private async Task<RobotClientsAutoTask?> GenerateTaskDetail(AutoTask? task, bool continueAutoTask = false)
 1383  {
 1384    if (task == null)
 385    {
 386      return null;
 87    }
 88
 1089    var progress = await _context.Progresses.AsNoTracking()
 1090      .Where(p => p.Id == task.CurrentProgressId)
 1091      .Select(p => new { p.Name })
 1092      .FirstOrDefaultAsync();
 1093    if (!continueAutoTask)
 694    {
 95      // Notify the updated task
 696      var flowName = await _context.Flows.AsNoTracking()
 697        .Where(f => f.Id == task.FlowId)
 698        .Select(f => f.Name)
 699        .FirstOrDefaultAsync();
 6100      string? robotName = null;
 6101      if (task.AssignedRobotId != null)
 6102      {
 6103        robotName = await _context.Robots.AsNoTracking()
 6104          .Where(r => r.Id == task.AssignedRobotId)
 6105          .Select(r => r.Name)
 6106          .FirstOrDefaultAsync();
 6107      }
 6108      await _bus.Publish(new AutoTaskUpdateContract{
 6109        Id = task.Id,
 6110        Name = task.Name,
 6111        Priority = task.Priority,
 6112        FlowId = task.FlowId ?? 0,
 6113        FlowName = flowName ?? "Deleted Flow",
 6114        RealmId = task.RealmId,
 6115        AssignedRobotId = task.AssignedRobotId,
 6116        AssignedRobotName = robotName,
 6117        CurrentProgressId = task.CurrentProgressId,
 6118        CurrentProgressName = progress!.Name,
 6119      });
 6120    }
 121
 10122    if (task.CurrentProgressId == (int)ProgressState.Completed || task.CurrentProgressId == (int)ProgressState.Aborted)
 2123    {
 124      // Return immediately if the task is completed / aborted
 2125      return new RobotClientsAutoTask {
 2126        TaskId = task.Id,
 2127        TaskName = task.Name ?? string.Empty,
 2128        TaskProgressId = task.CurrentProgressId,
 2129        TaskProgressName = progress!.Name ?? string.Empty,
 2130        Waypoints = {},
 2131        NextToken = string.Empty,
 2132      };
 133    }
 134
 8135    var flowDetail = await _context.FlowDetails.AsNoTracking()
 8136      .Where(fd => fd.FlowId == task.FlowId && fd.Order == (int)task.CurrentProgressOrder!)
 8137      .FirstOrDefaultAsync();
 8138    if (!continueAutoTask && flowDetail!.TriggerId != null)
 1139    {
 140      // Fire the trigger
 1141      await _triggerService.InitialiseTriggerAsync(task, flowDetail);
 1142    }
 143
 8144    List<RobotClientsDof> waypoints = [];
 8145    if (task.CurrentProgressId == (int)ProgressState.PreMoving)
 1146    {
 1147      var firstTaskDetail = await _context.AutoTasksDetail.AsNoTracking()
 1148        .Where(t => t.AutoTaskId == task.Id)
 1149        .Include(t => t.Waypoint)
 1150        .OrderBy(t => t.Order)
 1151        .FirstOrDefaultAsync();
 1152      if (firstTaskDetail != null)
 1153        waypoints.Add(GenerateWaypoint(firstTaskDetail));
 1154    }
 7155    else if (task.CurrentProgressId == (int)ProgressState.Moving)
 6156    {
 6157      var taskDetails = await _context.AutoTasksDetail.AsNoTracking()
 6158        .Where(t => t.AutoTaskId == task.Id)
 6159        .Include(t => t.Waypoint)
 6160        .OrderBy(t => t.Order)
 6161        .ToListAsync();
 32162      foreach (var t in taskDetails)
 7163      {
 7164        waypoints.Add(GenerateWaypoint(t));
 7165      }
 6166    }
 167
 8168    string nextToken = task.NextToken ?? string.Empty;
 8169    if (flowDetail?.AutoTaskNextControllerId != (int) AutoTaskNextController.Robot)
 1170    {
 171      // API has the control
 1172      nextToken = string.Empty;
 1173    }
 174
 8175    return new RobotClientsAutoTask {
 8176      TaskId = task.Id,
 8177      TaskName = task.Name ?? string.Empty,
 8178      TaskProgressId = task.CurrentProgressId,
 8179      TaskProgressName = progress!.Name ?? string.Empty,
 8180      Waypoints = { waypoints },
 8181      NextToken = nextToken,
 8182    };
 13183  }
 184
 185  public void ResetIgnoreRobot(int realmId)
 1186  {
 1187    _memoryCache.Remove(GetIgnoreRobotsKey(realmId));
 1188  }
 189
 190  private async Task<AutoTask?> GetRunningAutoTaskSqlAsync(Guid robotId)
 8191  {
 8192    return await _context.AutoTasks.AsNoTracking()
 8193      .Include(t => t.AutoTaskDetails)
 8194      .Where(t => t.AssignedRobotId == robotId)
 8195      .Where(t => !LgdxHelper.AutoTaskStaticStates.Contains(t.CurrentProgressId))
 8196      .OrderByDescending(t => t.Priority) // In case the robot has multiple running task by mistake
 8197      .ThenByDescending(t => t.AssignedRobotId)
 8198      .ThenBy(t => t.Id)
 8199      .FirstOrDefaultAsync();
 8200  }
 201
 202  private async Task<AutoTask?> AssignAutoTaskSqlAsync(Guid robotId)
 3203  {
 3204    AutoTask? task = null;
 3205    using var transaction = await _context.Database.BeginTransactionAsync();
 206    try
 3207    {
 208      // Get waiting task
 3209      task = await _context.AutoTasks.FromSql(
 3210        $@"SELECT * FROM ""Automation.AutoTasks"" AS T
 3211            WHERE T.""CurrentProgressId"" = {(int)ProgressState.Waiting} AND (T.""AssignedRobotId"" = {robotId} OR T.""A
 3212            ORDER BY T.""Priority"" DESC, T.""AssignedRobotId"" DESC, T.""Id""
 3213            LIMIT 1 FOR UPDATE SKIP LOCKED"
 3214      ).FirstOrDefaultAsync();
 215
 216      // Get flow detail
 3217      var flowDetail = await _context.FlowDetails
 3218        .Where(f => f.FlowId == task!.FlowId)
 3219        .Select(f => new {
 3220          f.ProgressId,
 3221          f.Order
 3222        })
 3223        .OrderBy(f => f.Order)
 3224        .FirstOrDefaultAsync();
 225
 226      // Update task
 2227      task!.AssignedRobotId = robotId;
 2228      task.CurrentProgressId = flowDetail!.ProgressId;
 2229      task.CurrentProgressOrder = flowDetail.Order;
 2230      task.NextToken = LgdxHelper.GenerateMd5Hash($"{robotId} {task.Id} {task.CurrentProgressId} {DateTime.UtcNow}");
 2231      await _context.SaveChangesAsync();
 2232      await transaction.CommitAsync();
 2233      return task;
 234    }
 1235    catch (Exception)
 1236    {
 1237      await transaction.RollbackAsync();
 1238    }
 1239    return null;
 3240  }
 241
 242  public async Task<RobotClientsAutoTask?> GetAutoTaskAsync(Guid robotId)
 9243  {
 9244    var realmId = await _robotService.GetRobotRealmIdAsync(robotId) ?? 0;
 9245    _memoryCache.TryGetValue(GetIgnoreRobotsKey(realmId), out HashSet<Guid>? ignoreRobotIds);
 9246    if (ignoreRobotIds != null && (ignoreRobotIds?.Contains(robotId) ?? false))
 1247    {
 1248      return null;
 249    }
 250
 8251    var currentTask = await GetRunningAutoTaskSqlAsync(robotId);
 8252    bool continueAutoTask = currentTask != null;
 8253    if (currentTask == null)
 4254    {
 4255      if (!_onlineRobotsService.GetPauseAutoTaskAssignment(robotId))
 3256      {
 3257        currentTask = await AssignAutoTaskSqlAsync(robotId);
 3258        if (currentTask != null)
 2259        {
 2260          await AddAutoTaskJourney(currentTask);
 2261        }
 3262      }
 263      else
 1264      {
 265        // If pause auto task assignment is true, new task will not be assigned.
 1266        return null;
 267      }
 3268    }
 269
 7270    if (currentTask == null)
 1271    {
 272      // No task for this robot, pause database access.
 1273      ignoreRobotIds ??= [];
 1274      ignoreRobotIds.Add(robotId);
 1275      _memoryCache.Set(GetIgnoreRobotsKey(realmId), ignoreRobotIds, TimeSpan.FromMinutes(5));
 1276    }
 7277    return await GenerateTaskDetail(currentTask, continueAutoTask);
 9278  }
 279
 280  private async Task<AutoTask?> AutoTaskAbortSqlAsync(int taskId, Guid? robotId = null, string? token = null)
 4281  {
 4282    AutoTask? task = null;
 4283    using var transaction = await _context.Database.BeginTransactionAsync();
 284    try
 4285    {
 286      // Get task
 4287      if (robotId == null && string.IsNullOrWhiteSpace(token))
 2288      {
 289        // From API
 2290        task = await _context.AutoTasks.FromSql(
 2291          $@"SELECT * FROM ""Automation.AutoTasks"" AS T
 2292            WHERE T.""Id"" = {taskId}
 2293            LIMIT 1 FOR UPDATE NOWAIT"
 2294        ).FirstOrDefaultAsync();
 2295      }
 296      else
 2297      {
 2298        task = await _context.AutoTasks.FromSql(
 2299          $@"SELECT * FROM ""Automation.AutoTasks"" AS T
 2300              WHERE T.""Id"" = {taskId} AND T.""AssignedRobotId"" = {robotId} AND T.""NextToken"" = {token}
 2301              LIMIT 1 FOR UPDATE NOWAIT"
 2302        ).FirstOrDefaultAsync();
 2303      }
 304
 305      // Update task
 4306      task!.CurrentProgressId = (int)ProgressState.Aborted;
 2307      task.CurrentProgressOrder = null;
 2308      task.NextToken = null;
 2309      await _context.SaveChangesAsync();
 2310      await transaction.CommitAsync();
 2311    }
 2312    catch (Exception)
 2313    {
 2314      await transaction.RollbackAsync();
 2315    }
 4316    return task;
 4317  }
 318
 319  private async Task DeleteTriggerRetries(int taskId)
 2320  {
 2321    var count = await _context.TriggerRetries.Where(tr => tr.AutoTaskId == taskId).CountAsync();
 2322    if (count > 0)
 0323    {
 0324      await _context.TriggerRetries.Where(tr => tr.AutoTaskId == taskId).ExecuteDeleteAsync();
 0325    }
 2326  }
 327
 328  public async Task<RobotClientsAutoTask?> AutoTaskAbortAsync(Guid robotId, int taskId, string token, AutoTaskAbortReaso
 2329  {
 2330    var task = await AutoTaskAbortSqlAsync(taskId, robotId, token);
 2331    if (task != null)
 1332    {
 1333      await DeleteTriggerRetries(taskId);
 1334      await _emailService.SendAutoTaskAbortEmailAsync(robotId, taskId, autoTaskAbortReason);
 1335      await AddAutoTaskJourney(task);
 1336    }
 2337    return await GenerateTaskDetail(task);
 2338  }
 339
 340  public async Task<bool> AutoTaskAbortApiAsync(int taskId)
 2341  {
 2342    var task = await AutoTaskAbortSqlAsync(taskId);
 2343    if (task == null)
 1344      return false;
 345
 1346    await DeleteTriggerRetries(taskId);
 1347    await _emailService.SendAutoTaskAbortEmailAsync((Guid)task!.AssignedRobotId!, taskId, AutoTaskAbortReason.UserApi);
 1348    await AddAutoTaskJourney(task);
 1349    return true;
 2350  }
 351
 352  private async Task<AutoTask?> AutoTaskNextSqlAsync(Guid robotId, int taskId, string token)
 5353  {
 5354    AutoTask? task = null;
 5355    using var transaction = await _context.Database.BeginTransactionAsync();
 356    try
 5357    {
 358      // Get waiting task
 5359      task = await _context.AutoTasks.FromSql(
 5360        $@"SELECT * FROM ""Automation.AutoTasks"" AS T
 5361            WHERE T.""Id"" = {taskId} AND T.""AssignedRobotId"" = {robotId} AND T.""NextToken"" = {token}
 5362            LIMIT 1 FOR UPDATE NOWAIT"
 5363      ).FirstOrDefaultAsync();
 364
 365      // Get flow detail
 5366      var flowDetail = await _context.FlowDetails.AsNoTracking()
 5367        .Where(f => f.FlowId == task!.FlowId)
 5368        .Where(f => f.Order > task!.CurrentProgressOrder)
 5369        .Select(f => new {
 5370          f.ProgressId,
 5371          f.Order
 5372        })
 5373        .OrderBy(f => f.Order)
 5374        .FirstOrDefaultAsync();
 375
 376      // Update task
 3377      if (flowDetail != null)
 2378      {
 2379        task!.CurrentProgressId = flowDetail!.ProgressId;
 2380        task.CurrentProgressOrder = flowDetail.Order;
 2381        task.NextToken = LgdxHelper.GenerateMd5Hash($"{robotId} {task.Id} {task.CurrentProgressId} {DateTime.UtcNow}");
 2382      }
 383      else
 1384      {
 1385        task!.CurrentProgressId = (int)ProgressState.Completed;
 1386        task.CurrentProgressOrder = null;
 1387        task.NextToken = null;
 1388      }
 389
 3390      await _context.SaveChangesAsync();
 3391      await transaction.CommitAsync();
 3392    }
 2393    catch (Exception)
 2394    {
 2395      await transaction.RollbackAsync();
 2396    }
 5397    return task;
 5398  }
 399
 400  public async Task<RobotClientsAutoTask?> AutoTaskNextAsync(Guid robotId, int taskId, string token)
 3401  {
 3402    var task = await AutoTaskNextSqlAsync(robotId, taskId, token);
 3403    if (task != null)
 2404    {
 2405      await AddAutoTaskJourney(task);
 2406    }
 3407    return await GenerateTaskDetail(task);
 3408  }
 409
 410  public async Task<AutoTask?> AutoTaskNextApiAsync(Guid robotId, int taskId, string token)
 2411  {
 2412    var task = await AutoTaskNextSqlAsync(robotId, taskId, token);
 2413    if (task != null)
 1414    {
 1415      await AddAutoTaskJourney(task);
 1416    }
 2417    return task;
 2418  }
 419
 420  public async Task<RobotClientsAutoTask?> AutoTaskNextConstructAsync(AutoTask autoTask)
 1421  {
 1422    return await GenerateTaskDetail(autoTask);
 1423  }
 424}