diff --git a/backend/api/Controllers/RobotController.cs b/backend/api/Controllers/RobotController.cs index 89063d6c1..71d81dd68 100644 --- a/backend/api/Controllers/RobotController.cs +++ b/backend/api/Controllers/RobotController.cs @@ -397,7 +397,7 @@ [FromRoute] string missionRunId { string errorMessage = $"Could not reach ISAR at {robot.IsarUri}"; _logger.LogError(e, "{Message}", errorMessage); - OnIsarUnavailable(robot); + await OnIsarUnavailable(robot); return StatusCode(StatusCodes.Status502BadGateway, errorMessage); } catch (MissionException e) @@ -464,7 +464,7 @@ public async Task StopMission([FromRoute] string robotId) { const string Message = "Error connecting to ISAR while stopping mission"; _logger.LogError(e, "{Message}", Message); - OnIsarUnavailable(robot); + await OnIsarUnavailable(robot); return StatusCode(StatusCodes.Status502BadGateway, Message); } catch (MissionException e) @@ -523,7 +523,7 @@ public async Task PauseMission([FromRoute] string robotId) { const string Message = "Error connecting to ISAR while pausing mission"; _logger.LogError(e, "{Message}", Message); - OnIsarUnavailable(robot); + await OnIsarUnavailable(robot); return StatusCode(StatusCodes.Status502BadGateway, Message); } catch (MissionException e) @@ -573,7 +573,7 @@ public async Task ResumeMission([FromRoute] string robotId) { const string Message = "Error connecting to ISAR while resuming mission"; _logger.LogError(e, "{Message}", Message); - OnIsarUnavailable(robot); + await OnIsarUnavailable(robot); return StatusCode(StatusCodes.Status502BadGateway, Message); } catch (MissionException e) @@ -631,7 +631,7 @@ [FromRoute] string armPosition { string errorMessage = $"Error connecting to ISAR at {robot.IsarUri}"; _logger.LogError(e, "{Message}", errorMessage); - OnIsarUnavailable(robot); + await OnIsarUnavailable(robot); return StatusCode(StatusCodes.Status502BadGateway, errorMessage); } catch (MissionException e) @@ -716,7 +716,7 @@ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery { string message = $"Could not reach ISAR at {robot.IsarUri}"; _logger.LogError(e, "{Message}", message); - OnIsarUnavailable(robot); + await OnIsarUnavailable(robot); return StatusCode(StatusCodes.Status502BadGateway, message); } catch (MissionException e) @@ -747,7 +747,7 @@ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery return Ok(missionRun); } - private async void OnIsarUnavailable(Robot robot) + private async Task OnIsarUnavailable(Robot robot) { robot.Enabled = false; robot.Status = RobotStatus.Offline; diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index 2d85286a9..7690f2629 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -1,5 +1,4 @@ -using Api.Controllers.Models; -using Api.Database.Models; +using Api.Database.Models; using Api.Services; using Api.Services.Events; using Api.Utilities; @@ -11,7 +10,7 @@ public class MissionEventHandler : EventHandlerBase private readonly ILogger _logger; // The mutex is used to ensure multiple missions aren't attempted scheduled simultaneously whenever multiple mission runs are created - private readonly Mutex _scheduleMissionMutex = new(); + private readonly Semaphore _scheduleMissionSemaphore = new(1, 1); private readonly IServiceScopeFactory _scopeFactory; public MissionEventHandler( @@ -34,24 +33,6 @@ IServiceScopeFactory scopeFactory private IMissionSchedulingService MissionScheduling => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IList MissionRunQueue(string robotId) - { - return MissionService - .ReadAll( - new MissionRunQueryStringParameters - { - Statuses = new List - { - MissionStatus.Pending - }, - RobotId = robotId, - OrderBy = "DesiredStartTime", - PageSize = 100 - } - ) - .Result; - } - public override void Subscribe() { MissionRunService.MissionRunCreated += OnMissionRunCreated; @@ -73,11 +54,11 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) await stoppingToken; } - private void OnMissionRunCreated(object? sender, MissionRunCreatedEventArgs e) + private async void OnMissionRunCreated(object? sender, MissionRunCreatedEventArgs e) { _logger.LogInformation("Triggered MissionRunCreated event for mission run ID: {MissionRunId}", e.MissionRunId); - var missionRun = MissionService.ReadById(e.MissionRunId).Result; + var missionRun = await MissionService.ReadById(e.MissionRunId); if (missionRun == null) { @@ -85,15 +66,15 @@ private void OnMissionRunCreated(object? sender, MissionRunCreatedEventArgs e) return; } - if (MissionScheduling.MissionRunQueueIsEmpty(MissionRunQueue(missionRun.Robot.Id))) + if (MissionScheduling.MissionRunQueueIsEmpty(await MissionService.ReadMissionRunQueue(missionRun.Robot.Id))) { _logger.LogInformation("Mission run {MissionRunId} was not started as there are no mission runs on the queue", e.MissionRunId); return; } - _scheduleMissionMutex.WaitOne(); - MissionScheduling.StartMissionRunIfSystemIsAvailable(missionRun); - _scheduleMissionMutex.ReleaseMutex(); + _scheduleMissionSemaphore.WaitOne(); + await MissionScheduling.StartMissionRunIfSystemIsAvailable(missionRun.Id); + _scheduleMissionSemaphore.Release(); } private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) @@ -106,20 +87,15 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) return; } - if (MissionScheduling.MissionRunQueueIsEmpty(MissionRunQueue(robot.Id))) + if (MissionScheduling.MissionRunQueueIsEmpty(await MissionService.ReadMissionRunQueue(robot.Id))) { _logger.LogInformation("The robot was changed to available but there are no mission runs in the queue to be scheduled"); return; } - var missionRun = (MissionRun?)null; - - if (robot.MissionQueueFrozen) - { - missionRun = MissionRunQueue(robot.Id).FirstOrDefault(missionRun => missionRun.Robot.Id == robot.Id && - missionRun.MissionRunPriority == MissionRunPriority.Emergency); - } - else { missionRun = MissionRunQueue(robot.Id).FirstOrDefault(missionRun => missionRun.Robot.Id == robot.Id); } + MissionRun? missionRun; + if (robot.MissionQueueFrozen) { missionRun = await MissionService.ReadNextScheduledEmergencyMissionRun(robot.Id); } + else { missionRun = await MissionService.ReadNextScheduledMissionRun(robot.Id); } if (missionRun == null) { @@ -127,9 +103,9 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) return; } - _scheduleMissionMutex.WaitOne(); - MissionScheduling.StartMissionRunIfSystemIsAvailable(missionRun); - _scheduleMissionMutex.ReleaseMutex(); + _scheduleMissionSemaphore.WaitOne(); + await MissionScheduling.StartMissionRunIfSystemIsAvailable(missionRun.Id); + _scheduleMissionSemaphore.Release(); } private async void OnEmergencyButtonPressedForRobot(object? sender, EmergencyButtonPressedForRobotEventArgs e) diff --git a/backend/api/Services/MissionRunService.cs b/backend/api/Services/MissionRunService.cs index c2e26a260..5b576aeff 100644 --- a/backend/api/Services/MissionRunService.cs +++ b/backend/api/Services/MissionRunService.cs @@ -18,8 +18,14 @@ public interface IMissionRunService public Task ReadByIsarMissionId(string isarMissionId); + public Task> ReadMissionRunQueue(string robotId); + public Task ReadNextScheduledRunByMissionId(string missionId); + public Task ReadNextScheduledMissionRun(string robotId); + + public Task ReadNextScheduledEmergencyMissionRun(string robotId); + public Task Update(MissionRun mission); public Task UpdateMissionRunStatusByIsarMissionId( @@ -101,6 +107,29 @@ public async Task> ReadAll(MissionRunQueryStringParameters .FirstOrDefaultAsync(missionRun => missionRun.Id.Equals(id)); } + public async Task> ReadMissionRunQueue(string robotId) + { + return await GetMissionRunsWithSubModels() + .Where(missionRun => missionRun.Robot.Id == robotId && missionRun.Status == MissionStatus.Pending) + .OrderBy(missionRun => missionRun.DesiredStartTime) + .ToListAsync(); + } + + public async Task ReadNextScheduledMissionRun(string robotId) + { + return await GetMissionRunsWithSubModels() + .OrderBy(missionRun => missionRun.DesiredStartTime) + .FirstOrDefaultAsync(missionRun => missionRun.Robot.Id == robotId && missionRun.Status == MissionStatus.Pending); + } + + public async Task ReadNextScheduledEmergencyMissionRun(string robotId) + { + return await GetMissionRunsWithSubModels() + .OrderBy(missionRun => missionRun.DesiredStartTime) + .FirstOrDefaultAsync(missionRun => + missionRun.Robot.Id == robotId && missionRun.MissionRunPriority == MissionRunPriority.Emergency && missionRun.Status == MissionStatus.Pending); + } + public async Task ReadNextScheduledRunByMissionId(string missionId) { return await GetMissionRunsWithSubModels() diff --git a/backend/api/Services/MissionSchedulingService.cs b/backend/api/Services/MissionSchedulingService.cs index 6dd13092e..096c0cdec 100644 --- a/backend/api/Services/MissionSchedulingService.cs +++ b/backend/api/Services/MissionSchedulingService.cs @@ -9,7 +9,7 @@ namespace Api.Services { public interface IMissionSchedulingService { - public void StartMissionRunIfSystemIsAvailable(MissionRun missionRun); + public Task StartMissionRunIfSystemIsAvailable(string missionRunId); public Task OngoingMission(string robotId); @@ -47,15 +47,23 @@ public MissionSchedulingService(ILogger logger, IMissi _isarService = isarService; } - public void StartMissionRunIfSystemIsAvailable(MissionRun missionRun) + public async Task StartMissionRunIfSystemIsAvailable(string missionRunId) { - if (!TheSystemIsAvailableToRunAMission(missionRun.Robot, missionRun).Result) + var missionRun = await _missionRunService.ReadById(missionRunId); + if (missionRun is null) + { + string errorMessage = $"Mission run with Id {missionRunId} was not found"; + _logger.LogError("{Message}", errorMessage); + throw new MissionRunNotFoundException(errorMessage); + } + + if (!await TheSystemIsAvailableToRunAMission(missionRun.Robot.Id, missionRun.Id)) { _logger.LogInformation("Mission {MissionRunId} was put on the queue as the system may not start a mission now", missionRun.Id); return; } - try { StartMissionRun(missionRun); } + try { await StartMissionRun(missionRun); } catch (MissionException ex) { const MissionStatus NewStatus = MissionStatus.Failed; @@ -67,7 +75,7 @@ public void StartMissionRunIfSystemIsAvailable(MissionRun missionRun) ); missionRun.Status = NewStatus; missionRun.StatusReason = $"Failed to start: '{ex.Message}'"; - _missionRunService.Update(missionRun); + await _missionRunService.Update(missionRun); } } @@ -115,7 +123,7 @@ public async Task StopCurrentMissionRun(string robotId) { const string Message = "Error connecting to ISAR while stopping mission"; _logger.LogError(e, "{Message}", Message); - OnIsarUnavailable(robot.Id); + await OnIsarUnavailable(robot.Id); throw new MissionException(Message, (int)e.StatusCode!); } catch (MissionException e) @@ -219,12 +227,12 @@ private async Task MoveInterruptedMissionsToQueue(IEnumerable interrupte } } - private void StartMissionRun(MissionRun queuedMissionRun) + private async Task StartMissionRun(MissionRun queuedMissionRun) { - var result = _robotController.StartMission( + var result = await _robotController.StartMission( queuedMissionRun.Robot.Id, queuedMissionRun.Id - ).Result; + ); if (result.Result is not OkObjectResult) { string errorMessage = "Unknown error from robot controller"; @@ -237,7 +245,7 @@ private void StartMissionRun(MissionRun queuedMissionRun) _logger.LogInformation("Started mission run '{Id}'", queuedMissionRun.Id); } - private async void OnIsarUnavailable(string robotId) + private async Task OnIsarUnavailable(string robotId) { var robot = await _robotService.ReadById(robotId); if (robot == null) @@ -292,15 +300,6 @@ private static Pose ClosestSafePosition(Pose robotPose, IList safe return closestPose; } - public async Task TheSystemIsAvailableToRunAMission(string robotId, MissionRun missionRun) - { - var robot = await _robotService.ReadById(robotId); - if (robot != null) { return await TheSystemIsAvailableToRunAMission(robot, missionRun); } - - _logger.LogError("Robot with ID: {RobotId} was not found in the database", robotId); - return false; - } - private async Task?> GetOngoingMissions(string robotId) { var ongoingMissions = await _missionRunService.ReadAll( @@ -318,9 +317,25 @@ public async Task TheSystemIsAvailableToRunAMission(string robotId, Missio return ongoingMissions; } - private async Task TheSystemIsAvailableToRunAMission(Robot robot, MissionRun missionRun) + private async Task TheSystemIsAvailableToRunAMission(string robotId, string missionRunId) { - bool ongoingMission = await OngoingMission(robot.Id); + bool ongoingMission = await OngoingMission(robotId); + + var robot = await _robotService.ReadById(robotId); + if (robot is null) + { + string errorMessage = $"Robot with ID: {robotId} was not found in the database"; + _logger.LogError("{Message}", errorMessage); + throw new RobotNotFoundException(errorMessage); + } + + var missionRun = await _missionRunService.ReadById(missionRunId); + if (missionRun is null) + { + string errorMessage = $"Mission run with Id {missionRunId} was not found in the database"; + _logger.LogError("{Message}", errorMessage); + throw new MissionRunNotFoundException(errorMessage); + } if (robot.MissionQueueFrozen && missionRun.MissionRunPriority != MissionRunPriority.Emergency) {