< 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: 301
Coverable lines: 301
Total lines: 403
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 106
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.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
 027public class AutoTaskSchedulerService(
 028    IBus bus,
 029    IEmailService emailService,
 030    IMemoryCache memoryCache,
 031    IOnlineRobotsService onlineRobotsService,
 032    IRobotService robotService,
 033    ITriggerService triggerService,
 034    LgdxContext context
 035  ) : IAutoTaskSchedulerService
 36{
 037  private readonly IBus _bus = bus ?? throw new ArgumentNullException(nameof(bus));
 038  private readonly IEmailService _emailService = emailService ?? throw new ArgumentNullException(nameof(emailService));
 039  private readonly IMemoryCache _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
 040  private readonly IOnlineRobotsService _onlineRobotsService = onlineRobotsService ?? throw new ArgumentNullException(na
 041  private readonly IRobotService _robotService = robotService ?? throw new ArgumentNullException(nameof(robotService));
 042  private readonly ITriggerService _triggerService = triggerService ?? throw new ArgumentNullException(nameof(triggerSer
 043  private readonly LgdxContext _context = context ?? throw new ArgumentNullException(nameof(context));
 044  private static string GetIgnoreRobotsKey(int realmId) => $"AutoTaskSchedulerService_IgnoreRobot_{realmId}";
 45
 46  private static RobotClientsDof GenerateWaypoint(AutoTaskDetail taskDetail)
 047  {
 048    if (taskDetail.Waypoint != null)
 049    {
 050      var waypoint = new RobotClientsDof
 051        { X = taskDetail.Waypoint.X, Y = taskDetail.Waypoint.Y, Rotation = taskDetail.Waypoint.Rotation };
 052      if (taskDetail.CustomX != null)
 053        waypoint.X = (double)taskDetail.CustomX;
 054      if (taskDetail.CustomY != null)
 055        waypoint.X = (double)taskDetail.CustomY;
 056      if (taskDetail.CustomRotation != null)
 057        waypoint.X = (double)taskDetail.CustomRotation;
 058      return waypoint;
 59    }
 60    else
 061    {
 062      return new RobotClientsDof {
 063        X = taskDetail.CustomX != null ? (double)taskDetail.CustomX : 0,
 064        Y = taskDetail.CustomY != null ? (double)taskDetail.CustomY : 0,
 065        Rotation = taskDetail.CustomRotation != null ? (double)taskDetail.CustomRotation : 0 };
 66    }
 067  }
 68
 69  private async Task<RobotClientsAutoTask?> GenerateTaskDetail(AutoTask? task, bool continueAutoTask = false)
 070  {
 071    if (task == null)
 072    {
 073      return null;
 74    }
 75
 076    var progress = await _context.Progresses.AsNoTracking()
 077      .Where(p => p.Id == task.CurrentProgressId)
 078      .Select(p => new { p.Name })
 079      .FirstOrDefaultAsync();
 080    if (!continueAutoTask)
 081    {
 82      // Notify the updated task
 083      var flowName = await _context.Flows.AsNoTracking()
 084        .Where(f => f.Id == task.FlowId)
 085        .Select(f => f.Name)
 086        .FirstOrDefaultAsync();
 087      string? robotName = null;
 088      if (task.AssignedRobotId != null)
 089      {
 090        robotName = await _context.Robots.AsNoTracking()
 091          .Where(r => r.Id == task.AssignedRobotId)
 092          .Select(r => r.Name)
 093          .FirstOrDefaultAsync();
 094      }
 095      await _bus.Publish(new AutoTaskUpdateContract{
 096        Id = task.Id,
 097        Name = task.Name,
 098        Priority = task.Priority,
 099        FlowId = task.FlowId ?? 0,
 0100        FlowName = flowName ?? "Deleted Flow",
 0101        RealmId = task.RealmId,
 0102        AssignedRobotId = task.AssignedRobotId,
 0103        AssignedRobotName = robotName,
 0104        CurrentProgressId = task.CurrentProgressId,
 0105        CurrentProgressName = progress!.Name,
 0106      });
 0107    }
 108
 0109    if (task.CurrentProgressId == (int)ProgressState.Completed || task.CurrentProgressId == (int)ProgressState.Aborted)
 0110    {
 111      // Return immediately if the task is completed / aborted
 0112      return new RobotClientsAutoTask {
 0113        TaskId = task.Id,
 0114        TaskName = task.Name ?? string.Empty,
 0115        TaskProgressId = task.CurrentProgressId,
 0116        TaskProgressName = progress!.Name ?? string.Empty,
 0117        Waypoints = {},
 0118        NextToken = string.Empty,
 0119      };
 120    }
 121
 0122    var flowDetail = await _context.FlowDetails.AsNoTracking()
 0123      .Where(fd => fd.FlowId == task.FlowId && fd.Order == (int)task.CurrentProgressOrder!)
 0124      .FirstOrDefaultAsync();
 0125    if (!continueAutoTask && flowDetail!.TriggerId != null)
 0126    {
 127      // Fire the trigger
 0128      await _triggerService.InitialiseTriggerAsync(task, flowDetail);
 0129    }
 130
 0131    List<RobotClientsDof> waypoints = [];
 0132    if (task.CurrentProgressId == (int)ProgressState.PreMoving)
 0133    {
 0134      var firstTaskDetail = await _context.AutoTasksDetail.AsNoTracking()
 0135        .Where(t => t.AutoTaskId == task.Id)
 0136        .Include(t => t.Waypoint)
 0137        .OrderBy(t => t.Order)
 0138        .FirstOrDefaultAsync();
 0139      if (firstTaskDetail != null)
 0140        waypoints.Add(GenerateWaypoint(firstTaskDetail));
 0141    }
 0142    else if (task.CurrentProgressId == (int)ProgressState.Moving)
 0143    {
 0144      var taskDetails = await _context.AutoTasksDetail.AsNoTracking()
 0145        .Where(t => t.AutoTaskId == task.Id)
 0146        .Include(t => t.Waypoint)
 0147        .OrderBy(t => t.Order)
 0148        .ToListAsync();
 0149      foreach (var t in taskDetails)
 0150      {
 0151        if (t.Waypoint != null)
 0152          waypoints.Add(GenerateWaypoint(t));
 0153      }
 0154    }
 155
 0156    string nextToken = task.NextToken ?? string.Empty;
 0157    if (flowDetail?.AutoTaskNextControllerId != (int) AutoTaskNextController.Robot)
 0158    {
 159      // API has the control
 0160      nextToken = string.Empty;
 0161    }
 162
 0163    return new RobotClientsAutoTask {
 0164      TaskId = task.Id,
 0165      TaskName = task.Name ?? string.Empty,
 0166      TaskProgressId = task.CurrentProgressId,
 0167      TaskProgressName = progress!.Name ?? string.Empty,
 0168      Waypoints = { waypoints },
 0169      NextToken = nextToken,
 0170    };
 0171  }
 172
 173  public void ResetIgnoreRobot(int realmId)
 0174  {
 0175    _memoryCache.Remove(GetIgnoreRobotsKey(realmId));
 0176  }
 177
 178  private async Task<AutoTask?> GetRunningAutoTaskSqlAsync(Guid robotId)
 0179  {
 0180    return await _context.AutoTasks.AsNoTracking()
 0181      .Include(t => t.AutoTaskDetails)
 0182      .Where(t => t.AssignedRobotId == robotId)
 0183      .Where(t => !LgdxHelper.AutoTaskStaticStates.Contains(t.CurrentProgressId))
 0184      .OrderByDescending(t => t.Priority) // In case the robot has multiple running task by mistake
 0185      .ThenByDescending(t => t.AssignedRobotId)
 0186      .ThenBy(t => t.Id)
 0187      .FirstOrDefaultAsync();
 0188  }
 189
 190  private async Task<AutoTask?> AssignAutoTaskSqlAsync(Guid robotId)
 0191  {
 0192    AutoTask? task = null;
 0193    using var transaction = await _context.Database.BeginTransactionAsync();
 194    try
 0195    {
 196      // Get waiting task
 0197      task = await _context.AutoTasks.FromSql(
 0198        $@"SELECT * FROM ""Automation.AutoTasks"" AS T
 0199            WHERE T.""CurrentProgressId"" = {(int)ProgressState.Waiting} AND (T.""AssignedRobotId"" = {robotId} OR T.""A
 0200            ORDER BY T.""Priority"" DESC, T.""AssignedRobotId"" DESC, T.""Id""
 0201            LIMIT 1 FOR UPDATE SKIP LOCKED"
 0202      ).FirstOrDefaultAsync();
 203
 204      // Get flow detail
 0205      var flowDetail = await _context.FlowDetails
 0206        .Where(f => f.FlowId == task!.FlowId)
 0207        .Select(f => new {
 0208          f.ProgressId,
 0209          f.Order
 0210        })
 0211        .OrderBy(f => f.Order)
 0212        .FirstOrDefaultAsync();
 213
 214      // Update task
 0215      task!.AssignedRobotId = robotId;
 0216      task.CurrentProgressId = flowDetail!.ProgressId;
 0217      task.CurrentProgressOrder = flowDetail.Order;
 0218      task.NextToken = LgdxHelper.GenerateMd5Hash($"{robotId} {task.Id} {task.CurrentProgressId} {DateTime.UtcNow}");
 0219      await _context.SaveChangesAsync();
 0220      await transaction.CommitAsync();
 0221      return task;
 222    }
 0223    catch (Exception)
 0224    {
 0225      await transaction.RollbackAsync();
 0226    }
 0227    return null;
 0228  }
 229
 230  public async Task<RobotClientsAutoTask?> GetAutoTaskAsync(Guid robotId)
 0231  {
 0232    var realmId = await _robotService.GetRobotRealmIdAsync(robotId) ?? 0;
 0233    var ignoreRobotIds = _memoryCache.Get<HashSet<Guid>>(GetIgnoreRobotsKey(realmId));
 0234    if (ignoreRobotIds != null && (ignoreRobotIds?.Contains(robotId) ?? false))
 0235    {
 0236      return null;
 237    }
 238
 0239    var currentTask = await GetRunningAutoTaskSqlAsync(robotId);
 0240    bool continueAutoTask = currentTask != null;
 0241    if (currentTask == null)
 0242    {
 0243      if (!_onlineRobotsService.GetPauseAutoTaskAssignment(robotId))
 0244      {
 0245        var count = await _context.AutoTasks.AsNoTracking()
 0246          .Where(t => t.AssignedRobotId == robotId)
 0247          .Where(t => !LgdxHelper.AutoTaskStaticStates.Contains(t.CurrentProgressId))
 0248          .CountAsync();
 0249        if (count > 0)
 0250        {
 251          // If there is already a task running, no task will be assigned.
 0252          return null;
 253        }
 0254        currentTask = await AssignAutoTaskSqlAsync(robotId);
 0255      }
 256      else
 0257      {
 258        // If pause auto task assignment is true, new task will not be assigned.
 0259        return null;
 260      }
 0261    }
 262
 0263    if (currentTask == null)
 0264    {
 265      // No task for this robot, pause database access.
 0266      ignoreRobotIds ??= [];
 0267      ignoreRobotIds.Add(robotId);
 0268      _memoryCache.Set(GetIgnoreRobotsKey(realmId), ignoreRobotIds, TimeSpan.FromMinutes(5));
 0269    }
 0270    return await GenerateTaskDetail(currentTask, continueAutoTask);
 0271  }
 272
 273  private async Task<AutoTask?> AutoTaskAbortSqlAsync(int taskId, Guid? robotId = null, string? token = null)
 0274  {
 0275    AutoTask? task = null;
 0276    using var transaction = await _context.Database.BeginTransactionAsync();
 277    try
 0278    {
 279      // Get task
 0280      if (robotId == null && string.IsNullOrWhiteSpace(token))
 0281      {
 282        // From API
 0283        task = await _context.AutoTasks.FromSql(
 0284          $@"SELECT * FROM ""Automation.AutoTasks"" AS T
 0285            WHERE T.""Id"" = {taskId}
 0286            LIMIT 1 FOR UPDATE NOWAIT"
 0287        ).FirstOrDefaultAsync();
 0288      }
 289      else
 0290      {
 0291        task = await _context.AutoTasks.FromSql(
 0292          $@"SELECT * FROM ""Automation.AutoTasks"" AS T
 0293              WHERE T.""Id"" = {taskId} AND T.""AssignedRobotId"" = {robotId} AND T.""NextToken"" = {token}
 0294              LIMIT 1 FOR UPDATE NOWAIT"
 0295        ).FirstOrDefaultAsync();
 0296      }
 297
 298      // Update task
 0299      task!.CurrentProgressId = (int)ProgressState.Aborted;
 0300      task.CurrentProgressOrder = null;
 0301      task.NextToken = null;
 0302      await _context.SaveChangesAsync();
 0303      await transaction.CommitAsync();
 0304    }
 0305    catch (Exception)
 0306    {
 0307      await transaction.RollbackAsync();
 0308    }
 0309    return task;
 0310  }
 311
 312  private async Task DeleteTriggerRetries(int taskId)
 0313  {
 0314    var count = await _context.TriggerRetries.Where(tr => tr.AutoTaskId == taskId).CountAsync();
 0315    if (count > 0)
 0316    {
 0317      await _context.TriggerRetries.Where(tr => tr.AutoTaskId == taskId).ExecuteDeleteAsync();
 0318    }
 0319  }
 320
 321  public async Task<RobotClientsAutoTask?> AutoTaskAbortAsync(Guid robotId, int taskId, string token, AutoTaskAbortReaso
 0322  {
 0323    var task = await AutoTaskAbortSqlAsync(taskId, robotId, token);
 0324    await DeleteTriggerRetries(taskId);
 0325    await _emailService.SendAutoTaskAbortEmailAsync(robotId, taskId, autoTaskAbortReason);
 0326    return await GenerateTaskDetail(task);
 0327  }
 328
 329  public async Task<bool> AutoTaskAbortApiAsync(int taskId)
 0330  {
 0331    var task = await AutoTaskAbortSqlAsync(taskId);
 0332    if (task == null)
 0333      return false;
 334
 0335    await DeleteTriggerRetries(taskId);
 0336    await _emailService.SendAutoTaskAbortEmailAsync((Guid)task!.AssignedRobotId!, taskId, AutoTaskAbortReason.UserApi);
 0337    return true;
 0338  }
 339
 340  private async Task<AutoTask?> AutoTaskNextSqlAsync(Guid robotId, int taskId, string token)
 0341  {
 0342    AutoTask? task = null;
 0343    using var transaction = await _context.Database.BeginTransactionAsync();
 344    try
 0345    {
 346      // Get waiting task
 0347      task = await _context.AutoTasks.FromSql(
 0348        $@"SELECT * FROM ""Automation.AutoTasks"" AS T
 0349            WHERE T.""Id"" = {taskId} AND T.""AssignedRobotId"" = {robotId} AND T.""NextToken"" = {token}
 0350            LIMIT 1 FOR UPDATE NOWAIT"
 0351      ).FirstOrDefaultAsync();
 352
 353      // Get flow detail
 0354      var flowDetail = await _context.FlowDetails.AsNoTracking()
 0355        .Where(f => f.FlowId == task!.FlowId)
 0356        .Where(f => f.Order > task!.CurrentProgressOrder)
 0357        .Select(f => new {
 0358          f.ProgressId,
 0359          f.Order
 0360        })
 0361        .OrderBy(f => f.Order)
 0362        .FirstOrDefaultAsync();
 363
 364      // Update task
 0365      if (flowDetail != null)
 0366      {
 0367        task!.CurrentProgressId = flowDetail!.ProgressId;
 0368        task.CurrentProgressOrder = flowDetail.Order;
 0369        task.NextToken = LgdxHelper.GenerateMd5Hash($"{robotId} {task.Id} {task.CurrentProgressId} {DateTime.UtcNow}");
 0370      }
 371      else
 0372      {
 0373        task!.CurrentProgressId = (int)ProgressState.Completed;
 0374        task.CurrentProgressOrder = null;
 0375        task.NextToken = null;
 0376      }
 377
 0378      await _context.SaveChangesAsync();
 0379      await transaction.CommitAsync();
 0380    }
 0381    catch (Exception)
 0382    {
 0383      await transaction.RollbackAsync();
 0384    }
 0385    return task;
 0386  }
 387
 388  public async Task<RobotClientsAutoTask?> AutoTaskNextAsync(Guid robotId, int taskId, string token)
 0389  {
 0390    var task = await AutoTaskNextSqlAsync(robotId, taskId, token);
 0391    return await GenerateTaskDetail(task);
 0392  }
 393
 394  public async Task<AutoTask?> AutoTaskNextApiAsync(Guid robotId, int taskId, string token)
 0395  {
 0396    return await AutoTaskNextSqlAsync(robotId, taskId, token);
 0397  }
 398
 399  public async Task<RobotClientsAutoTask?> AutoTaskNextConstructAsync(AutoTask autoTask)
 0400  {
 0401    return await GenerateTaskDetail(autoTask);
 0402  }
 403}