diff --git a/backend/api.test/Database/DatabaseUtilities.cs b/backend/api.test/Database/DatabaseUtilities.cs index 22cd2a506..6e08aaaf2 100644 --- a/backend/api.test/Database/DatabaseUtilities.cs +++ b/backend/api.test/Database/DatabaseUtilities.cs @@ -13,6 +13,7 @@ namespace Api.Test.Database public class DatabaseUtilities { private readonly AccessRoleService _accessRoleService; + private readonly MissionTaskService _missionTaskService; private readonly AreaService _areaService; private readonly DeckService _deckService; private readonly InstallationService _installationService; @@ -28,13 +29,14 @@ public DatabaseUtilities(FlotillaDbContext context) _accessRoleService = new AccessRoleService(context, new HttpContextAccessor()); _installationService = new InstallationService(context, _accessRoleService); + _missionTaskService = new MissionTaskService(context, new Mock>().Object); _plantService = new PlantService(context, _installationService, _accessRoleService); _deckService = new DeckService(context, defaultLocalizationPoseService, _installationService, _plantService, _accessRoleService, new MockSignalRService()); _areaService = new AreaService(context, _installationService, _plantService, _deckService, defaultLocalizationPoseService, _accessRoleService); _userInfoService = new UserInfoService(context, new HttpContextAccessor(), new Mock>().Object); - _missionRunService = new MissionRunService(context, new MockSignalRService(), new Mock>().Object, _accessRoleService, _userInfoService); _robotModelService = new RobotModelService(context); - _robotService = new RobotService(context, new Mock>().Object, _robotModelService, new MockSignalRService(), _accessRoleService, _installationService, _areaService, _missionRunService); + _robotService = new RobotService(context, new Mock>().Object, _robotModelService, new MockSignalRService(), _accessRoleService, _installationService, _areaService); + _missionRunService = new MissionRunService(context, new MockSignalRService(), new Mock>().Object, _accessRoleService, _missionTaskService, _areaService, _robotService, _userInfoService); } public async Task NewMissionRun( @@ -161,7 +163,7 @@ public async Task NewRobot(RobotStatus status, Installation installation, RobotCapabilities = [RobotCapabilitiesEnum.drive_to_pose, RobotCapabilitiesEnum.take_image, RobotCapabilitiesEnum.return_to_home, RobotCapabilitiesEnum.localize] }; - var robotModel = await _robotModelService.ReadByRobotType(createRobotQuery.RobotType); + var robotModel = await _robotModelService.ReadByRobotType(createRobotQuery.RobotType, readOnly: true); var robot = new Robot(createRobotQuery, installation, area) { Model = robotModel! diff --git a/backend/api.test/EventHandlers/TestMissionEventHandler.cs b/backend/api.test/EventHandlers/TestMissionEventHandler.cs index ef7f97596..b5d74de1f 100644 --- a/backend/api.test/EventHandlers/TestMissionEventHandler.cs +++ b/backend/api.test/EventHandlers/TestMissionEventHandler.cs @@ -53,6 +53,8 @@ public TestMissionEventHandler(DatabaseFixture fixture) var missionDefinitionServiceLogger = new Mock>().Object; var lastMissionRunServiceLogger = new Mock>().Object; var sourceServiceLogger = new Mock>().Object; + var errorHandlingServiceLogger = new Mock>().Object; + var missionTaskServiceLogger = new Mock>().Object; var configuration = WebApplication.CreateBuilder().Configuration; @@ -64,12 +66,11 @@ public TestMissionEventHandler(DatabaseFixture fixture) _mqttService = new MqttService(mqttServiceLogger, configuration); - _missionRunService = new MissionRunService(context, signalRService, missionLogger, accessRoleService, userInfoService); - + var missionTaskService = new MissionTaskService(context, missionTaskServiceLogger); var missionLoader = new MockMissionLoader(); var stidServiceMock = new MockStidService(context); - var missionDefinitionService = new MissionDefinitionService(context, missionLoader, signalRService, accessRoleService, missionDefinitionServiceLogger, _missionRunService); + var sourceService = new SourceService(context, sourceServiceLogger); var robotModelService = new RobotModelService(context); var taskDurationServiceMock = new MockTaskDurationService(); var isarServiceMock = new MockIsarService(); @@ -79,12 +80,14 @@ public TestMissionEventHandler(DatabaseFixture fixture) var deckService = new DeckService(context, defaultLocalizationPoseService, installationService, plantService, accessRoleService, signalRService); var areaService = new AreaService(context, installationService, plantService, deckService, defaultLocalizationPoseService, accessRoleService); var mapServiceMock = new MockMapService(); - _robotService = new RobotService(context, robotServiceLogger, robotModelService, signalRService, accessRoleService, installationService, areaService, _missionRunService); + _robotService = new RobotService(context, robotServiceLogger, robotModelService, signalRService, accessRoleService, installationService, areaService); + _missionRunService = new MissionRunService(context, signalRService, missionLogger, accessRoleService, missionTaskService, areaService, _robotService, userInfoService); + var missionDefinitionService = new MissionDefinitionService(context, missionLoader, signalRService, accessRoleService, missionDefinitionServiceLogger, _missionRunService, sourceService); _localizationService = new LocalizationService(localizationServiceLogger, _robotService, installationService, areaService); - + var errorHandlingService = new ErrorHandlingService(errorHandlingServiceLogger, _robotService, _missionRunService); var returnToHomeService = new ReturnToHomeService(returnToHomeServiceLogger, _robotService, _missionRunService, mapServiceMock); _missionSchedulingService = new MissionSchedulingService(missionSchedulingServiceLogger, _missionRunService, _robotService, areaService, - isarServiceMock, _localizationService, returnToHomeService, signalRService); + isarServiceMock, _localizationService, returnToHomeService, signalRService, errorHandlingService); var lastMissionRunService = new LastMissionRunService(missionDefinitionService); _databaseUtilities = new DatabaseUtilities(context); @@ -164,7 +167,7 @@ public async Task ScheduledMissionStartedWhenSystemIsAvailable() Thread.Sleep(100); // Assert - var postTestMissionRun = await _missionRunService.ReadById(missionRun.Id); + var postTestMissionRun = await _missionRunService.ReadById(missionRun.Id, readOnly: true); Assert.Equal(MissionStatus.Ongoing, postTestMissionRun!.Status); } @@ -186,8 +189,8 @@ public async Task SecondScheduledMissionQueuedIfRobotIsBusy() await _missionRunService.Create(missionRunTwo); // Assert - var postTestMissionRunOne = await _missionRunService.ReadById(missionRunOne.Id); - var postTestMissionRunTwo = await _missionRunService.ReadById(missionRunTwo.Id); + var postTestMissionRunOne = await _missionRunService.ReadById(missionRunOne.Id, readOnly: true); + var postTestMissionRunTwo = await _missionRunService.ReadById(missionRunTwo.Id, readOnly: true); Assert.Equal(MissionStatus.Ongoing, postTestMissionRunOne!.Status); Assert.Equal(MissionStatus.Pending, postTestMissionRunTwo!.Status); } @@ -222,7 +225,7 @@ public async Task NewMissionIsStartedWhenRobotBecomesAvailable() Thread.Sleep(500); // Assert - var postTestMissionRun = await _missionRunService.ReadById(missionRun.Id); + var postTestMissionRun = await _missionRunService.ReadById(missionRun.Id, readOnly: true); Assert.Equal(MissionStatus.Ongoing, postTestMissionRun!.Status); } @@ -258,7 +261,7 @@ public async Task ReturnToHomeMissionIsStartedIfQueueIsEmptyWhenRobotBecomesAvai ], OrderBy = "DesiredStartTime", PageSize = 100 - }); + }, readOnly: true); Assert.True(ongoingMission.Any()); } @@ -272,6 +275,7 @@ public async Task ReturnToHomeMissionIsNotStartedIfReturnToHomeIsNotSupported() var area = await _databaseUtilities.NewArea(installation.InstallationCode, plant.PlantCode, deck.Name); var robot = await _databaseUtilities.NewRobot(RobotStatus.Busy, installation, area); robot.RobotCapabilities!.Remove(RobotCapabilitiesEnum.return_to_home); + await _robotService.Update(robot); var mqttEventArgs = new MqttReceivedArgs( new IsarStatusMessage @@ -295,7 +299,7 @@ public async Task ReturnToHomeMissionIsNotStartedIfReturnToHomeIsNotSupported() ], OrderBy = "DesiredStartTime", PageSize = 100 - }); + }, readOnly: true); Assert.False(ongoingMission.Any()); } @@ -317,7 +321,7 @@ public async Task MissionRunIsStartedForOtherAvailableRobotIfOneRobotHasAnOngoin Thread.Sleep(100); // Assert - var postStartMissionRunOne = await _missionRunService.ReadById(missionRunOne.Id); + var postStartMissionRunOne = await _missionRunService.ReadById(missionRunOne.Id, readOnly: true); Assert.NotNull(postStartMissionRunOne); Assert.Equal(MissionStatus.Ongoing, postStartMissionRunOne.Status); @@ -326,7 +330,7 @@ public async Task MissionRunIsStartedForOtherAvailableRobotIfOneRobotHasAnOngoin Thread.Sleep(100); // Assert - var postStartMissionRunTwo = await _missionRunService.ReadById(missionRunTwo.Id); + var postStartMissionRunTwo = await _missionRunService.ReadById(missionRunTwo.Id, readOnly: true); Assert.NotNull(postStartMissionRunTwo); Assert.Equal(MissionStatus.Ongoing, postStartMissionRunTwo.Status); } @@ -360,7 +364,7 @@ public async Task QueuedMissionsAreAbortedWhenLocalizationFails() Thread.Sleep(500); // Assert - var postTestMissionRun = await _missionRunService.ReadById(missionRun1.Id); + var postTestMissionRun = await _missionRunService.ReadById(missionRun1.Id, readOnly: true); Assert.Equal(MissionStatus.Aborted, postTestMissionRun!.Status); } @@ -373,7 +377,7 @@ public async Task QueuedMissionsAreNotAbortedWhenRobotAvailableHappensAtTheSameT var deck = await _databaseUtilities.NewDeck(installation.InstallationCode, plant.PlantCode); var area = await _databaseUtilities.NewArea(installation.InstallationCode, plant.PlantCode, deck.Name); var robot = await _databaseUtilities.NewRobot(RobotStatus.Available, installation, null); - var missionRun1 = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, area, true); + var missionRun1 = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, area, true, isarMissionId: Guid.NewGuid().ToString()); Thread.Sleep(100); var missionRun2 = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, area, true); Thread.Sleep(100); @@ -400,10 +404,10 @@ public async Task QueuedMissionsAreNotAbortedWhenRobotAvailableHappensAtTheSameT Thread.Sleep(500); // Assert - var postTestMissionRun1 = await _missionRunService.ReadById(missionRun1.Id); + var postTestMissionRun1 = await _missionRunService.ReadById(missionRun1.Id, readOnly: true); Assert.Equal(MissionRunType.Localization, postTestMissionRun1!.MissionRunType); Assert.Equal(MissionStatus.Successful, postTestMissionRun1!.Status); - var postTestMissionRun2 = await _missionRunService.ReadById(missionRun2.Id); + var postTestMissionRun2 = await _missionRunService.ReadById(missionRun2.Id, readOnly: true); Assert.Equal(MissionStatus.Pending, postTestMissionRun2!.Status); } @@ -449,10 +453,10 @@ public async Task QueuedContinuesWhenOnIsarStatusHappensAtTheSameTimeAsOnIsarMis Thread.Sleep(2500); // Accommodate for sleep in OnIsarStatus // Assert - var postTestMissionRun1 = await _missionRunService.ReadById(missionRun1.Id); + var postTestMissionRun1 = await _missionRunService.ReadById(missionRun1.Id, readOnly: true); Assert.Equal(MissionRunType.Localization, postTestMissionRun1!.MissionRunType); Assert.Equal(Api.Database.Models.TaskStatus.Successful, postTestMissionRun1!.Tasks[0].Status); - var postTestMissionRun2 = await _missionRunService.ReadById(missionRun2.Id); + var postTestMissionRun2 = await _missionRunService.ReadById(missionRun2.Id, readOnly: true); Assert.Equal(MissionStatus.Ongoing, postTestMissionRun2!.Status); } @@ -476,7 +480,7 @@ public async Task LocalizationMissionCompletesAfterPressingSendToSafeZoneButton( Thread.Sleep(1000); // Assert - var updatedRobot = await _robotService.ReadById(robot.Id); + var updatedRobot = await _robotService.ReadById(robot.Id, readOnly: true); Assert.True(updatedRobot?.MissionQueueFrozen); bool isRobotLocalized = await _localizationService.RobotIsLocalized(robot.Id); @@ -525,7 +529,7 @@ public async Task ReturnHomeMissionAbortedIfNewMissionScheduled() Thread.Sleep(500); // Assert - var updatedReturnHomeMission = await _missionRunService.ReadById(returnToHomeMission.Id); + var updatedReturnHomeMission = await _missionRunService.ReadById(returnToHomeMission.Id, readOnly: true); Assert.True(updatedReturnHomeMission?.Status.Equals(MissionStatus.Aborted)); // Act @@ -552,7 +556,7 @@ public async Task ReturnHomeMissionAbortedIfNewMissionScheduled() _mqttService.RaiseEvent(nameof(MqttService.MqttIsarStatusReceived), mqttIsarStatusEventArgs); Thread.Sleep(500); - var updatedMissionRun = await _missionRunService.ReadById(missionRun.Id); + var updatedMissionRun = await _missionRunService.ReadById(missionRun.Id, readOnly: true); Assert.True(updatedMissionRun?.Status.Equals(MissionStatus.Ongoing)); } } diff --git a/backend/api.test/Mocks/RobotControllerMock.cs b/backend/api.test/Mocks/RobotControllerMock.cs index b70cc1cdf..513cc4439 100644 --- a/backend/api.test/Mocks/RobotControllerMock.cs +++ b/backend/api.test/Mocks/RobotControllerMock.cs @@ -13,6 +13,7 @@ internal class RobotControllerMock public readonly Mock RobotModelServiceMock; public readonly Mock RobotServiceMock; public readonly Mock InstallationServiceMock; + public readonly Mock ErrorHandlingServiceMock; public RobotControllerMock() { @@ -22,6 +23,7 @@ public RobotControllerMock() RobotModelServiceMock = new Mock(); AreaServiceMock = new Mock(); InstallationServiceMock = new Mock(); + ErrorHandlingServiceMock = new Mock(); var mockLoggerController = new Mock>(); @@ -32,7 +34,8 @@ public RobotControllerMock() MissionServiceMock.Object, RobotModelServiceMock.Object, AreaServiceMock.Object, - InstallationServiceMock.Object + InstallationServiceMock.Object, + ErrorHandlingServiceMock.Object ) { CallBase = true diff --git a/backend/api.test/Services/MissionService.cs b/backend/api.test/Services/MissionService.cs index ae1ba2e56..b4a4ab720 100644 --- a/backend/api.test/Services/MissionService.cs +++ b/backend/api.test/Services/MissionService.cs @@ -21,15 +21,30 @@ public class MissionServiceTest : IDisposable private readonly ISignalRService _signalRService; private readonly IAccessRoleService _accessRoleService; private readonly UserInfoService _userInfoService; + private readonly IMissionTaskService _missionTaskService; + private readonly IAreaService _areaService; + private readonly IDeckService _deckService; + private readonly IInstallationService _installationService; + private readonly IPlantService _plantService; + private readonly IRobotModelService _robotModelService; + private readonly IRobotService _robotService; public MissionServiceTest(DatabaseFixture fixture) { _context = fixture.NewContext; + var defaultLocalizationPoseService = new DefaultLocalizationPoseService(_context); _logger = new Mock>().Object; _signalRService = new MockSignalRService(); _accessRoleService = new AccessRoleService(_context, new HttpContextAccessor()); _userInfoService = new UserInfoService(_context, new HttpContextAccessor(), new Mock>().Object); - _missionRunService = new MissionRunService(_context, _signalRService, _logger, _accessRoleService, _userInfoService); + _missionTaskService = new MissionTaskService(_context, new Mock>().Object); + _installationService = new InstallationService(_context, _accessRoleService); + _plantService = new PlantService(_context, _installationService, _accessRoleService); + _deckService = new DeckService(_context, defaultLocalizationPoseService, _installationService, _plantService, _accessRoleService, new MockSignalRService()); + _areaService = new AreaService(_context, _installationService, _plantService, _deckService, defaultLocalizationPoseService, _accessRoleService); + _robotModelService = new RobotModelService(_context); + _robotService = new RobotService(_context, new Mock>().Object, _robotModelService, new MockSignalRService(), _accessRoleService, _installationService, _areaService); + _missionRunService = new MissionRunService(_context, _signalRService, _logger, _accessRoleService, _missionTaskService, _areaService, _robotService, _userInfoService); _databaseUtilities = new DatabaseUtilities(_context); } @@ -42,7 +57,7 @@ public void Dispose() [Fact] public async Task ReadIdDoesNotExist() { - var missionRun = await _missionRunService.ReadById("some_id_that_does_not_exist"); + var missionRun = await _missionRunService.ReadById("some_id_that_does_not_exist", readOnly: true); Assert.Null(missionRun); } @@ -50,7 +65,8 @@ public async Task ReadIdDoesNotExist() public async Task Create() { var reportsBefore = await _missionRunService.ReadAll( - new MissionRunQueryStringParameters() + new MissionRunQueryStringParameters(), + readOnly: true ); int nReportsBefore = reportsBefore.Count; diff --git a/backend/api.test/Services/RobotService.cs b/backend/api.test/Services/RobotService.cs index 70d176ac4..ba0c0962d 100644 --- a/backend/api.test/Services/RobotService.cs +++ b/backend/api.test/Services/RobotService.cs @@ -25,8 +25,6 @@ public class RobotServiceTest : IDisposable private readonly IDefaultLocalizationPoseService _defaultLocalizationPoseService; private readonly IDeckService _deckService; private readonly IAreaService _areaService; - private readonly IMissionRunService _missionRunService; - private readonly IUserInfoService _userInfoService; public RobotServiceTest(DatabaseFixture fixture) { @@ -40,8 +38,6 @@ public RobotServiceTest(DatabaseFixture fixture) _defaultLocalizationPoseService = new DefaultLocalizationPoseService(_context); _deckService = new DeckService(_context, _defaultLocalizationPoseService, _installationService, _plantService, _accessRoleService, _signalRService); _areaService = new AreaService(_context, _installationService, _plantService, _deckService, _defaultLocalizationPoseService, _accessRoleService); - _userInfoService = new UserInfoService(_context, new HttpContextAccessor(), new Mock>().Object); - _missionRunService = new MissionRunService(_context, _signalRService, new Mock>().Object, _accessRoleService, _userInfoService); } public void Dispose() @@ -53,7 +49,7 @@ public void Dispose() [Fact] public async Task ReadAll() { - var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService, _areaService, _missionRunService); + var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService, _areaService); var robots = await robotService.ReadAll(); Assert.True(robots.Any()); @@ -62,10 +58,10 @@ public async Task ReadAll() [Fact] public async Task Read() { - var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService, _areaService, _missionRunService); + var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService, _areaService); var robots = await robotService.ReadAll(); var firstRobot = robots.First(); - var robotById = await robotService.ReadById(firstRobot.Id); + var robotById = await robotService.ReadById(firstRobot.Id, readOnly: false); Assert.Equal(firstRobot, robotById); } @@ -73,7 +69,7 @@ public async Task Read() [Fact] public async Task ReadIdDoesNotExist() { - var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService, _areaService, _missionRunService); + var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService, _areaService); var robot = await robotService.ReadById("some_id_that_does_not_exist"); Assert.Null(robot); } @@ -81,7 +77,7 @@ public async Task ReadIdDoesNotExist() [Fact] public async Task Create() { - var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService, _areaService, _missionRunService); + var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService, _areaService); var installationService = new InstallationService(_context, _accessRoleService); var installation = await installationService.Create(new CreateInstallationQuery @@ -90,7 +86,7 @@ public async Task Create() InstallationCode = "JSV" }); - var robotsBefore = await robotService.ReadAll(); + var robotsBefore = await robotService.ReadAll(readOnly: true); int nRobotsBefore = robotsBefore.Count(); var videoStreamQuery = new CreateVideoStreamQuery { @@ -119,7 +115,7 @@ public async Task Create() robot.Model = robotModel; await robotService.Create(robot); - var robotsAfter = await robotService.ReadAll(); + var robotsAfter = await robotService.ReadAll(readOnly: true); int nRobotsAfter = robotsAfter.Count(); Assert.Equal(nRobotsBefore + 1, nRobotsAfter); diff --git a/backend/api/Controllers/AreaController.cs b/backend/api/Controllers/AreaController.cs index 4372f068b..5ba4f74f6 100644 --- a/backend/api/Controllers/AreaController.cs +++ b/backend/api/Controllers/AreaController.cs @@ -131,7 +131,7 @@ public async Task> UpdateDefaultLocalizationPose([Fro logger.LogInformation("Updating default localization pose on area '{areaId}'", areaId); try { - var area = await areaService.ReadById(areaId); + var area = await areaService.ReadById(areaId, readOnly: true); if (area is null) { logger.LogInformation("A area with id '{areaId}' does not exist", areaId); @@ -301,7 +301,7 @@ public async Task>> GetMissionDefi if (area == null) return NotFound($"Could not find area with id {id}"); - var missionDefinitions = await missionDefinitionService.ReadByAreaId(area.Id); + var missionDefinitions = await missionDefinitionService.ReadByAreaId(area.Id, readOnly: true); var missionDefinitionResponses = missionDefinitions.FindAll(m => !m.IsDeprecated).Select(m => new MissionDefinitionResponse(m)); return Ok(missionDefinitionResponses); } diff --git a/backend/api/Controllers/DeckController.cs b/backend/api/Controllers/DeckController.cs index a6e35f9ad..039887fb6 100644 --- a/backend/api/Controllers/DeckController.cs +++ b/backend/api/Controllers/DeckController.cs @@ -121,7 +121,7 @@ public async Task>> GetMissionDefi if (deck == null) return NotFound($"Could not find deck with id {deckId}"); - var missionDefinitions = await missionDefinitionService.ReadByDeckId(deck.Id); + var missionDefinitions = await missionDefinitionService.ReadByDeckId(deck.Id, readOnly: true); var missionDefinitionResponses = missionDefinitions.FindAll(m => !m.IsDeprecated).Select(m => new MissionDefinitionResponse(m)); return Ok(missionDefinitionResponses); } @@ -204,7 +204,7 @@ public async Task> UpdateDefaultLocalizationPose([Fro logger.LogInformation("Updating default localization pose on deck '{deckId}'", deckId); try { - var deck = await deckService.ReadById(deckId); + var deck = await deckService.ReadById(deckId, readOnly: false); if (deck is null) { logger.LogInformation("A deck with id '{deckId}' does not exist", deckId); diff --git a/backend/api/Controllers/EmergencyActionController.cs b/backend/api/Controllers/EmergencyActionController.cs index 72ae5a679..934ee81aa 100644 --- a/backend/api/Controllers/EmergencyActionController.cs +++ b/backend/api/Controllers/EmergencyActionController.cs @@ -30,7 +30,7 @@ public async Task> AbortCurrentMissionAndSendAllRobotsToSaf [FromRoute] string installationCode) { - var robots = await robotService.ReadRobotsForInstallation(installationCode); + var robots = await robotService.ReadRobotsForInstallation(installationCode, readOnly: true); foreach (var robot in robots) { @@ -58,7 +58,7 @@ public async Task> AbortCurrentMissionAndSendAllRobotsToSaf public async Task> ClearEmergencyStateForAllRobots( [FromRoute] string installationCode) { - var robots = await robotService.ReadRobotsForInstallation(installationCode); + var robots = await robotService.ReadRobotsForInstallation(installationCode, readOnly: true); foreach (var robot in robots) { diff --git a/backend/api/Controllers/InspectionFindingController.cs b/backend/api/Controllers/InspectionFindingController.cs index fb211f707..ce4512b9c 100644 --- a/backend/api/Controllers/InspectionFindingController.cs +++ b/backend/api/Controllers/InspectionFindingController.cs @@ -67,7 +67,7 @@ public async Task> GetInspections([FromRoute] string id logger.LogInformation("Get inspection by ID '{id}'", id); try { - var inspection = await inspectionService.ReadByIsarStepId(id); + var inspection = await inspectionService.ReadByIsarStepId(id, readOnly: true); if (inspection != null) { return Ok(inspection); diff --git a/backend/api/Controllers/MissionDefinitionController.cs b/backend/api/Controllers/MissionDefinitionController.cs index 5577800d9..e553e4cb5 100644 --- a/backend/api/Controllers/MissionDefinitionController.cs +++ b/backend/api/Controllers/MissionDefinitionController.cs @@ -31,7 +31,7 @@ [FromQuery] MissionDefinitionQueryStringParameters parameters PagedList missionDefinitions; try { - missionDefinitions = await missionDefinitionService.ReadAll(parameters); + missionDefinitions = await missionDefinitionService.ReadAll(parameters, readOnly: true); } catch (InvalidDataException e) { @@ -71,7 +71,7 @@ [FromQuery] MissionDefinitionQueryStringParameters parameters [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> GetMissionDefinitionWithTasksById([FromRoute] string id) { - var missionDefinition = await missionDefinitionService.ReadById(id); + var missionDefinition = await missionDefinitionService.ReadById(id, readOnly: true); if (missionDefinition == null) { return NotFound($"Could not find mission definition with id {id}"); @@ -93,7 +93,7 @@ public async Task> GetMissionDefinitionW [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> GetNextMissionRun([FromRoute] string id) { - var missionDefinition = await missionDefinitionService.ReadById(id); + var missionDefinition = await missionDefinitionService.ReadById(id, readOnly: true); if (missionDefinition == null) { return NotFound($"Could not find mission definition with id {id}"); @@ -129,7 +129,7 @@ [FromBody] UpdateMissionDefinitionQuery missionDefinitionQuery return BadRequest("Invalid data."); } - var missionDefinition = await missionDefinitionService.ReadById(id); + var missionDefinition = await missionDefinitionService.ReadById(id, readOnly: false); if (missionDefinition == null) { return NotFound($"Could not find mission definition with id '{id}'"); @@ -175,7 +175,7 @@ [FromBody] UpdateMissionDefinitionIsDeprecatedQuery missionDefinitionIsDeprecate return BadRequest("Invalid data."); } - var missionDefinition = await missionDefinitionService.ReadById(id); + var missionDefinition = await missionDefinitionService.ReadById(id, readOnly: false); if (missionDefinition == null) { return NotFound($"Could not find mission definition with id '{id}'"); diff --git a/backend/api/Controllers/MissionLoaderController.cs b/backend/api/Controllers/MissionLoaderController.cs index 88cc709ea..8cbc900e2 100644 --- a/backend/api/Controllers/MissionLoaderController.cs +++ b/backend/api/Controllers/MissionLoaderController.cs @@ -149,7 +149,7 @@ public async Task> GetPlantInfos() [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task>> GetActivePlants() { - var plants = await robotService.ReadAllActivePlants(); + var plants = await robotService.ReadAllActivePlants(readOnly: true); if (plants == null) { diff --git a/backend/api/Controllers/MissionSchedulingController.cs b/backend/api/Controllers/MissionSchedulingController.cs index d000626f6..58da590c1 100644 --- a/backend/api/Controllers/MissionSchedulingController.cs +++ b/backend/api/Controllers/MissionSchedulingController.cs @@ -124,11 +124,11 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery ) { Robot robot; - try { robot = await robotService.GetRobotWithPreCheck(scheduledMissionQuery.RobotId); } + try { robot = await robotService.GetRobotWithPreCheck(scheduledMissionQuery.RobotId, readOnly: true); } catch (Exception e) when (e is RobotNotFoundException) { return NotFound(e.Message); } catch (Exception e) when (e is RobotPreCheckFailedException) { return BadRequest(e.Message); } - var missionDefinition = await missionDefinitionService.ReadById(missionDefinitionId); + var missionDefinition = await missionDefinitionService.ReadById(missionDefinitionId, readOnly: true); if (missionDefinition == null) { return NotFound("Mission definition not found"); @@ -198,7 +198,7 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery ) { Robot robot; - try { robot = await robotService.GetRobotWithPreCheck(scheduledMissionQuery.RobotId); } + try { robot = await robotService.GetRobotWithPreCheck(scheduledMissionQuery.RobotId, readOnly: true); } catch (Exception e) when (e is RobotNotFoundException) { return NotFound(e.Message); } catch (Exception e) when (e is RobotPreCheckFailedException) { return BadRequest(e.Message); } string missionId = scheduledMissionQuery.MissionId.ToString(CultureInfo.CurrentCulture); @@ -276,7 +276,7 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery } else { - var missionDefinitions = await missionDefinitionService.ReadBySourceId(source.SourceId); + var missionDefinitions = await missionDefinitionService.ReadBySourceId(source.SourceId, readOnly: true); if (missionDefinitions.Count > 0) { existingMissionDefinition = missionDefinitions.First(); @@ -361,7 +361,7 @@ [FromBody] CustomMissionQuery customMissionQuery ) { Robot robot; - try { robot = await robotService.GetRobotWithPreCheck(customMissionQuery.RobotId); } + try { robot = await robotService.GetRobotWithPreCheck(customMissionQuery.RobotId, readOnly: true); } catch (Exception e) when (e is RobotNotFoundException) { return NotFound(e.Message); } catch (Exception e) when (e is RobotPreCheckFailedException) { return BadRequest(e.Message); } @@ -374,8 +374,7 @@ [FromBody] CustomMissionQuery customMissionQuery try { Area? area = null; - if (customMissionQuery.AreaName != null) { area = await areaService.ReadByInstallationAndName(customMissionQuery.InstallationCode, customMissionQuery.AreaName, readOnly: false); } - + if (customMissionQuery.AreaName != null) { area = await areaService.ReadByInstallationAndName(customMissionQuery.InstallationCode, customMissionQuery.AreaName, readOnly: true); } if (area == null) { throw new AreaNotFoundException($"No area with name {customMissionQuery.AreaName} in installation {customMissionQuery.InstallationCode} was found"); @@ -390,7 +389,7 @@ [FromBody] CustomMissionQuery customMissionQuery } else { - var missionDefinitions = await missionDefinitionService.ReadBySourceId(source.SourceId); + var missionDefinitions = await missionDefinitionService.ReadBySourceId(source.SourceId, readOnly: true); if (missionDefinitions.Count > 0) { existingMissionDefinition = missionDefinitions.First(); } } diff --git a/backend/api/Controllers/ReturnToHomeController.cs b/backend/api/Controllers/ReturnToHomeController.cs index 4abc78242..e26b07500 100644 --- a/backend/api/Controllers/ReturnToHomeController.cs +++ b/backend/api/Controllers/ReturnToHomeController.cs @@ -27,7 +27,7 @@ public async Task>> ScheduleReturnToHomeMission( [FromRoute] string robotId ) { - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot is null) { logger.LogWarning("Could not find robot with id={Id}", robotId); @@ -43,8 +43,6 @@ [FromRoute] string robotId } return Ok(returnToHomeMission); - } - } } diff --git a/backend/api/Controllers/RobotController.cs b/backend/api/Controllers/RobotController.cs index bc91f7140..c2a773a7a 100644 --- a/backend/api/Controllers/RobotController.cs +++ b/backend/api/Controllers/RobotController.cs @@ -16,7 +16,8 @@ public class RobotController( IIsarService isarService, IMissionSchedulingService missionSchedulingService, IRobotModelService robotModelService, - IAreaService areaService + IAreaService areaService, + IErrorHandlingService errorHandlingService ) : ControllerBase { /// @@ -35,7 +36,7 @@ public async Task>> GetRobots() { try { - var robots = await robotService.ReadAll(); + var robots = await robotService.ReadAll(readOnly: true); var robotResponses = robots.Select(robot => new RobotResponse(robot)); return Ok(robotResponses); } @@ -65,7 +66,7 @@ public async Task> GetRobotById([FromRoute] string i logger.LogInformation("Getting robot with id={Id}", id); try { - var robot = await robotService.ReadById(id); + var robot = await robotService.ReadById(id, readOnly: true); if (robot == null) { logger.LogWarning("Could not find robot with id={Id}", id); @@ -206,7 +207,7 @@ [FromBody] UpdateRobotQuery query try { - var robot = await robotService.ReadById(id); + var robot = await robotService.ReadById(id, readOnly: true); if (robot == null) { string errorMessage = $"No robot with id: {id} could be found"; @@ -275,7 +276,7 @@ [FromRoute] bool deprecated try { - var robot = await robotService.ReadById(id); + var robot = await robotService.ReadById(id, readOnly: true); if (robot == null) { string errorMessage = $"No robot with id: {id} could be found"; @@ -346,7 +347,7 @@ [FromBody] RobotStatus robotStatus if (!ModelState.IsValid) return BadRequest("Invalid data"); - var robot = await robotService.ReadById(id); + var robot = await robotService.ReadById(id, readOnly: true); if (robot == null) { string errorMessage = $"No robot with id: {id} could be found"; @@ -389,7 +390,7 @@ [FromBody] RobotStatus robotStatus [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task>> GetVideoStreams([FromRoute] string robotId) { - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot == null) { logger.LogWarning("Could not find robot with id={Id}", robotId); @@ -419,7 +420,7 @@ public async Task> CreateVideoStream( [FromBody] VideoStream videoStream ) { - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot == null) { logger.LogWarning("Could not find robot with id={Id}", robotId); @@ -467,7 +468,7 @@ [FromBody] VideoStream videoStream [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task StopMission([FromRoute] string robotId) { - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot == null) { logger.LogWarning("Could not find robot with id={Id}", robotId); @@ -479,7 +480,7 @@ public async Task StopMission([FromRoute] string robotId) { const string Message = "Error connecting to ISAR while stopping mission"; logger.LogError(e, "{Message}", Message); - await robotService.HandleLosingConnectionToIsar(robot.Id); + await errorHandlingService.HandleLosingConnectionToIsar(robot.Id); return StatusCode(StatusCodes.Status502BadGateway, Message); } catch (MissionException e) @@ -523,7 +524,7 @@ public async Task StopMission([FromRoute] string robotId) [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task PauseMission([FromRoute] string robotId) { - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot == null) { logger.LogWarning("Could not find robot with id={Id}", robotId); @@ -538,7 +539,7 @@ public async Task PauseMission([FromRoute] string robotId) { const string Message = "Error connecting to ISAR while pausing mission"; logger.LogError(e, "{Message}", Message); - await robotService.HandleLosingConnectionToIsar(robot.Id); + await errorHandlingService.HandleLosingConnectionToIsar(robot.Id); return StatusCode(StatusCodes.Status502BadGateway, Message); } catch (MissionException e) @@ -573,7 +574,7 @@ public async Task PauseMission([FromRoute] string robotId) [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task ResumeMission([FromRoute] string robotId) { - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot == null) { logger.LogWarning("Could not find robot with id={Id}", robotId); @@ -588,7 +589,7 @@ public async Task ResumeMission([FromRoute] string robotId) { const string Message = "Error connecting to ISAR while resuming mission"; logger.LogError(e, "{Message}", Message); - await robotService.HandleLosingConnectionToIsar(robot.Id); + await errorHandlingService.HandleLosingConnectionToIsar(robot.Id); return StatusCode(StatusCodes.Status502BadGateway, Message); } catch (MissionException e) @@ -627,7 +628,7 @@ public async Task SetArmPosition( [FromRoute] string armPosition ) { - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot == null) { string errorMessage = $"Could not find robot with id {robotId}"; @@ -654,7 +655,7 @@ [FromRoute] string armPosition { string errorMessage = $"Error connecting to ISAR at {robot.IsarUri}"; logger.LogError(e, "{Message}", errorMessage); - await robotService.HandleLosingConnectionToIsar(robot.Id); + await errorHandlingService.HandleLosingConnectionToIsar(robot.Id); return StatusCode(StatusCodes.Status502BadGateway, errorMessage); } catch (MissionException e) @@ -692,7 +693,7 @@ public async Task ResetRobot( [FromRoute] string robotId ) { - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot == null) { string errorMessage = $"Could not find robot with id {robotId}"; diff --git a/backend/api/Controllers/RobotModelController.cs b/backend/api/Controllers/RobotModelController.cs index 53e24ea16..c55f4872f 100644 --- a/backend/api/Controllers/RobotModelController.cs +++ b/backend/api/Controllers/RobotModelController.cs @@ -149,7 +149,7 @@ [FromBody] UpdateRobotModelQuery robotModelQuery if (!ModelState.IsValid) return BadRequest("Invalid data."); - var robotModel = await robotModelService.ReadById(id); + var robotModel = await robotModelService.ReadById(id, readOnly: true); if (robotModel == null) return NotFound($"Could not find robot model with id '{id}'"); @@ -181,7 +181,7 @@ [FromBody] UpdateRobotModelQuery robotModelQuery if (!ModelState.IsValid) return BadRequest("Invalid data."); - var robotModel = await robotModelService.ReadByRobotType(robotType); + var robotModel = await robotModelService.ReadByRobotType(robotType, readOnly: true); if (robotModel == null) return NotFound($"Could not find robot model with robot type '{robotType}'"); diff --git a/backend/api/Controllers/SourceController.cs b/backend/api/Controllers/SourceController.cs index 605c0e2be..323da4a86 100644 --- a/backend/api/Controllers/SourceController.cs +++ b/backend/api/Controllers/SourceController.cs @@ -31,7 +31,7 @@ public async Task>> GetAllSources() List sources; try { - sources = await sourceService.ReadAll(); + sources = await sourceService.ReadAll(readOnly: true); } catch (InvalidDataException e) { diff --git a/backend/api/Database/Context/InitDb.cs b/backend/api/Database/Context/InitDb.cs index 6c837dc85..5be55e5e4 100644 --- a/backend/api/Database/Context/InitDb.cs +++ b/backend/api/Database/Context/InitDb.cs @@ -673,6 +673,7 @@ public static void PopulateDb(FlotillaDbContext context) context.AddRange(accessRoles); context.SaveChanges(); + context.ChangeTracker.Clear(); } } } diff --git a/backend/api/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index 44d533bfc..b8715a3d3 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -176,22 +176,6 @@ public void CalculateEstimatedDuration() } } - public void SetToFailed(string? failureReason = "") - { - Status = MissionStatus.Failed; - StatusReason = failureReason; - foreach (var task in Tasks.Where(task => !task.IsCompleted)) - { - task.Status = TaskStatus.Failed; - foreach ( - var inspection in task.Inspections.Where(inspection => !inspection.IsCompleted) - ) - { - inspection.Status = InspectionStatus.Failed; - } - } - } - public bool IsLocalizationMission() { return MissionRunType == MissionRunType.Localization; } public bool IsReturnHomeMission() { return MissionRunType == MissionRunType.ReturnHome; } diff --git a/backend/api/EventHandlers/InspectionFindingEventHandler.cs b/backend/api/EventHandlers/InspectionFindingEventHandler.cs index 1c0be4b60..6491941c6 100644 --- a/backend/api/EventHandlers/InspectionFindingEventHandler.cs +++ b/backend/api/EventHandlers/InspectionFindingEventHandler.cs @@ -39,7 +39,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) var lastReportingTime = DateTime.UtcNow - _timeSpan; - var inspectionFindings = await InspectionFindingService.RetrieveInspectionFindings(lastReportingTime); + var inspectionFindings = await InspectionFindingService.RetrieveInspectionFindings(lastReportingTime, readOnly: true); logger.LogInformation("Found {count} inspection findings in last {interval}", inspectionFindings.Count, _timeSpan); @@ -78,8 +78,8 @@ private async Task> GenerateFindingsList(List i foreach (var inspectionFinding in inspectionFindings) { - var missionRun = await InspectionFindingService.GetMissionRunByIsarStepId(inspectionFinding.IsarStepId); - var task = await InspectionFindingService.GetMissionTaskByIsarStepId(inspectionFinding.IsarStepId); + var missionRun = await InspectionFindingService.GetMissionRunByIsarStepId(inspectionFinding.IsarStepId, readOnly: true); + var task = await InspectionFindingService.GetMissionTaskByIsarStepId(inspectionFinding.IsarStepId, readOnly: true); if (task != null && missionRun != null) { diff --git a/backend/api/EventHandlers/IsarConnectionEventHandler.cs b/backend/api/EventHandlers/IsarConnectionEventHandler.cs index ac59cd4b0..c56562bcb 100644 --- a/backend/api/EventHandlers/IsarConnectionEventHandler.cs +++ b/backend/api/EventHandlers/IsarConnectionEventHandler.cs @@ -141,7 +141,7 @@ private async void OnTimeoutEvent(IsarRobotHeartbeatMessage robotHeartbeatMessag if (robot.CurrentMissionId != null) { - var missionRun = await MissionRunService.ReadById(robot.CurrentMissionId); + var missionRun = await MissionRunService.ReadById(robot.CurrentMissionId, readOnly: true); if (missionRun != null) { _logger.LogError( @@ -149,8 +149,7 @@ private async void OnTimeoutEvent(IsarRobotHeartbeatMessage robotHeartbeatMessag missionRun.Id, missionRun.Name ); - missionRun.SetToFailed("Lost connection to ISAR during mission"); - await MissionRunService.Update(missionRun); + await MissionRunService.SetMissionRunToFailed(missionRun.Id, "Lost connection to ISAR during mission"); } } diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index d87df1bfe..421688d8d 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -68,7 +68,7 @@ private async void OnMissionRunCreated(object? sender, MissionRunCreatedEventArg { _logger.LogInformation("Triggered MissionRunCreated event for mission run ID: {MissionRunId}", e.MissionRunId); - var missionRun = await MissionService.ReadById(e.MissionRunId); + var missionRun = await MissionService.ReadById(e.MissionRunId, readOnly: true); if (missionRun == null) { _logger.LogError("Mission run with ID: {MissionRunId} was not found in the database", e.MissionRunId); @@ -91,7 +91,7 @@ private async void OnMissionRunCreated(object? sender, MissionRunCreatedEventArg _startMissionSemaphore.WaitOne(); - if (missionRun.MissionRunType != MissionRunType.ReturnHome && await ReturnToHomeService.GetActiveReturnToHomeMissionRun(missionRun.Robot.Id) != null) + if (missionRun.MissionRunType != MissionRunType.ReturnHome && await ReturnToHomeService.GetActiveReturnToHomeMissionRun(missionRun.Robot.Id, readOnly: true) != null) { await MissionScheduling.AbortActiveReturnToHomeMission(missionRun.Robot.Id); } @@ -104,7 +104,7 @@ private async void OnMissionRunCreated(object? sender, MissionRunCreatedEventArg private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) { _logger.LogInformation("Triggered RobotAvailable event for robot ID: {RobotId}", e.RobotId); - var robot = await RobotService.ReadById(e.RobotId); + var robot = await RobotService.ReadById(e.RobotId, readOnly: true); if (robot == null) { _logger.LogError("Robot with ID: {RobotId} was not found in the database", e.RobotId); @@ -113,7 +113,7 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) if (robot.CurrentMissionId != null) { - var stuckMission = await MissionService.ReadById(robot.CurrentMissionId!); + var stuckMission = await MissionService.ReadById(robot.CurrentMissionId!, readOnly: true); if (stuckMission == null) { _logger.LogError("MissionRun with ID: {MissionId} was not found in the database", robot.CurrentMissionId); @@ -122,8 +122,7 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) if (stuckMission.Status == MissionStatus.Ongoing || stuckMission.Status == MissionStatus.Paused) { _logger.LogError("Ongoing/paused mission with ID: ${MissionId} is not being run in ISAR", robot.CurrentMissionId); - stuckMission.SetToFailed("Mission failed due to issue with ISAR"); - await MissionService.Update(stuckMission); + await MissionService.SetMissionRunToFailed(stuckMission.Id, "Mission failed due to issue with ISAR"); } } @@ -136,7 +135,7 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) private async void OnLocalizationMissionSuccessful(object? sender, LocalizationMissionSuccessfulEventArgs e) { _logger.LogInformation("Triggered LocalizationMissionSuccessful event for robot ID: {RobotId}", e.RobotId); - var robot = await RobotService.ReadById(e.RobotId); + var robot = await RobotService.ReadById(e.RobotId, readOnly: true); if (robot == null) { _logger.LogError("Robot with ID: {RobotId} was not found in the database", e.RobotId); @@ -163,7 +162,7 @@ private async void OnLocalizationMissionSuccessful(object? sender, LocalizationM private async void OnSendRobotToSafezoneTriggered(object? sender, RobotEmergencyEventArgs e) { _logger.LogInformation("Triggered EmergencyButtonPressed event for robot ID: {RobotId}", e.RobotId); - var robot = await RobotService.ReadById(e.RobotId); + var robot = await RobotService.ReadById(e.RobotId, readOnly: true); if (robot == null) { _logger.LogError("Robot with ID: {RobotId} was not found in the database", e.RobotId); @@ -244,7 +243,7 @@ private async void OnSendRobotToSafezoneTriggered(object? sender, RobotEmergency private async void OnReleaseRobotFromSafezoneTriggered(object? sender, RobotEmergencyEventArgs e) { _logger.LogInformation("Triggered EmergencyButtonPressed event for robot ID: {RobotId}", e.RobotId); - var robot = await RobotService.ReadById(e.RobotId); + var robot = await RobotService.ReadById(e.RobotId, readOnly: true); if (robot == null) { _logger.LogError("Robot with ID: {RobotId} was not found in the database", e.RobotId); @@ -275,7 +274,7 @@ private async void OnReleaseRobotFromSafezoneTriggered(object? sender, RobotEmer private async Task FindRelevantRobotAreaForSafePositionMission(string robotId) { - var robot = await RobotService.ReadById(robotId); + var robot = await RobotService.ReadById(robotId, readOnly: true); if (robot == null) { _logger.LogError("Robot with ID: {RobotId} was not found in the database", robotId); diff --git a/backend/api/EventHandlers/MqttEventHandler.cs b/backend/api/EventHandlers/MqttEventHandler.cs index 6c85a342e..d645e1081 100644 --- a/backend/api/EventHandlers/MqttEventHandler.cs +++ b/backend/api/EventHandlers/MqttEventHandler.cs @@ -80,7 +80,7 @@ private async void OnIsarStatus(object? sender, MqttReceivedArgs mqttArgs) { var isarStatus = (IsarStatusMessage)mqttArgs.Message; - var robot = await RobotService.ReadByIsarId(isarStatus.IsarId); + var robot = await RobotService.ReadByIsarId(isarStatus.IsarId, readOnly: true); if (robot == null) { @@ -92,7 +92,7 @@ private async void OnIsarStatus(object? sender, MqttReceivedArgs mqttArgs) if (await MissionRunService.OngoingOrPausedLocalizationMissionRunExists(robot.Id)) Thread.Sleep(5000); // Give localization mission update time to complete - var preUpdatedRobot = await RobotService.ReadByIsarId(isarStatus.IsarId); + var preUpdatedRobot = await RobotService.ReadByIsarId(isarStatus.IsarId, readOnly: true); if (preUpdatedRobot == null) { _logger.LogInformation("Received message from unknown ISAR instance {Id} with robot name {Name}", isarStatus.IsarId, isarStatus.RobotName); @@ -255,7 +255,7 @@ private async void OnIsarMissionUpdate(object? sender, MqttReceivedArgs mqttArgs return; } - var flotillaMissionRun = await MissionRunService.ReadByIsarMissionId(isarMission.MissionId); + var flotillaMissionRun = await MissionRunService.ReadByIsarMissionId(isarMission.MissionId, readOnly: true); if (flotillaMissionRun is null) { string errorMessage = $"Mission with isar mission Id {isarMission.IsarId} was not found"; @@ -312,7 +312,7 @@ private async void OnIsarMissionUpdate(object? sender, MqttReceivedArgs mqttArgs if (!updatedFlotillaMissionRun.IsCompleted) return; - var robot = await RobotService.ReadByIsarId(isarMission.IsarId); + var robot = await RobotService.ReadByIsarId(isarMission.IsarId, readOnly: true); if (robot is null) { _logger.LogError("Could not find robot '{RobotName}' with ISAR id '{IsarId}'", isarMission.RobotName, isarMission.IsarId); diff --git a/backend/api/Program.cs b/backend/api/Program.cs index 21d73d416..cfb5db510 100644 --- a/backend/api/Program.cs +++ b/backend/api/Program.cs @@ -84,6 +84,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/backend/api/Services/AccessRoleService.cs b/backend/api/Services/AccessRoleService.cs index 18f225056..ecc8ea487 100644 --- a/backend/api/Services/AccessRoleService.cs +++ b/backend/api/Services/AccessRoleService.cs @@ -14,16 +14,22 @@ public interface IAccessRoleService public Task Create(Installation installation, string roleName, RoleAccessLevel accessLevel); public Task ReadByInstallation(Installation installation); public Task> ReadAll(); + public void DetachTracking(AccessRole accessRole); } public class AccessRoleService(FlotillaDbContext context, IHttpContextAccessor httpContextAccessor) : IAccessRoleService { private const string SUPER_ADMIN_ROLE_NAME = "Role.Admin"; + private IQueryable GetAccessRoles(bool readOnly = false) + { + return readOnly ? context.AccessRoles.AsNoTracking() : context.AccessRoles.AsTracking(); + } + public async Task> GetAllowedInstallationCodes() { if (httpContextAccessor.HttpContext == null) - return await context.Installations.Select((i) => i.InstallationCode.ToUpperInvariant()).ToListAsync(); + return await context.Installations.AsNoTracking().Select((i) => i.InstallationCode.ToUpperInvariant()).ToListAsync(); var roles = httpContextAccessor.HttpContext.GetRequestedRoleNames(); @@ -33,9 +39,9 @@ public async Task> GetAllowedInstallationCodes() public async Task> GetAllowedInstallationCodes(List roles) { if (roles.Contains(SUPER_ADMIN_ROLE_NAME)) - return await context.Installations.Select((i) => i.InstallationCode.ToUpperInvariant()).ToListAsync(); + return await context.Installations.AsNoTracking().Select((i) => i.InstallationCode.ToUpperInvariant()).ToListAsync(); else - return await context.AccessRoles.Include((r) => r.Installation) + return await GetAccessRoles(readOnly: true).Include((r) => r.Installation) .Where((r) => roles.Contains(r.RoleName)).Select((r) => r.Installation != null ? r.Installation.InstallationCode.ToUpperInvariant() : "").ToListAsync(); } @@ -66,19 +72,20 @@ public async Task Create(Installation installation, string roleName, await context.AccessRoles.AddAsync(newAccessRole); await context.SaveChangesAsync(); + DetachTracking(newAccessRole); return newAccessRole!; } public async Task ReadByInstallation(Installation installation) { ThrowExceptionIfNotAdmin(); - return await context.AccessRoles.Include((r) => r.Installation).Where((r) => r.Installation.Id == installation.Id).FirstOrDefaultAsync(); + return await GetAccessRoles(readOnly: true).Include((r) => r.Installation).Where((r) => r.Installation.Id == installation.Id).FirstOrDefaultAsync(); } public async Task> ReadAll() { ThrowExceptionIfNotAdmin(); - return await context.AccessRoles.Include((r) => r.Installation).ToListAsync(); + return await GetAccessRoles(readOnly: true).Include((r) => r.Installation).ToListAsync(); } public bool IsUserAdmin() @@ -93,5 +100,10 @@ public bool IsAuthenticationAvailable() { return httpContextAccessor.HttpContext != null; } + + public void DetachTracking(AccessRole accessRole) + { + context.Entry(accessRole).State = EntityState.Detached; + } } } diff --git a/backend/api/Services/ActionServices/BatteryTimeseriesService.cs b/backend/api/Services/ActionServices/BatteryTimeseriesService.cs index cc7cf7c2b..a18b551cc 100644 --- a/backend/api/Services/ActionServices/BatteryTimeseriesService.cs +++ b/backend/api/Services/ActionServices/BatteryTimeseriesService.cs @@ -13,7 +13,7 @@ public class BatteryTimeseriesService(ILogger logger, public async Task AddBatteryEntry(float batteryLevel, string isarId) { - var robot = await robotService.ReadByIsarId(isarId); + var robot = await robotService.ReadByIsarId(isarId, readOnly: true); if (robot == null) { logger.LogWarning("Could not find corresponding robot for battery update on robot with ISAR id'{IsarId}'", isarId); diff --git a/backend/api/Services/ActionServices/PoseTimeseriesService.cs b/backend/api/Services/ActionServices/PoseTimeseriesService.cs index 2458d5ee0..06ba64fac 100644 --- a/backend/api/Services/ActionServices/PoseTimeseriesService.cs +++ b/backend/api/Services/ActionServices/PoseTimeseriesService.cs @@ -10,7 +10,7 @@ public class PoseTimeseriesService(ILogger logger, IRobot { public async Task AddPoseEntry(Pose pose, string isarId) { - var robot = await robotService.ReadByIsarId(isarId); + var robot = await robotService.ReadByIsarId(isarId, readOnly: true); if (robot == null) { logger.LogWarning("Could not find corresponding robot for pose update on robot with ISAR id '{IsarId}'", isarId); diff --git a/backend/api/Services/ActionServices/PressureTimeseriesService.cs b/backend/api/Services/ActionServices/PressureTimeseriesService.cs index a914b7054..91d0aff46 100644 --- a/backend/api/Services/ActionServices/PressureTimeseriesService.cs +++ b/backend/api/Services/ActionServices/PressureTimeseriesService.cs @@ -13,7 +13,7 @@ public class PressureTimeseriesService(ILogger logger public async Task AddPressureEntry(float pressureLevel, string isarId) { - var robot = await robotService.ReadByIsarId(isarId); + var robot = await robotService.ReadByIsarId(isarId, readOnly: true); if (robot == null) { logger.LogWarning("Could not find corresponding robot for pressure update on robot with ISAR id'{IsarId}'", isarId); diff --git a/backend/api/Services/ActionServices/TaskDurationService.cs b/backend/api/Services/ActionServices/TaskDurationService.cs index 25f9a2f63..718773b53 100644 --- a/backend/api/Services/ActionServices/TaskDurationService.cs +++ b/backend/api/Services/ActionServices/TaskDurationService.cs @@ -26,7 +26,7 @@ public async Task UpdateAverageDurationPerTask(RobotType robotType) }, readOnly: true); - var model = await robotModelService.ReadByRobotType(robotType); + var model = await robotModelService.ReadByRobotType(robotType, readOnly: true); if (model is null) { logger.LogWarning("Could not update average duration for robot model {RobotType} as the model was not found", robotType); diff --git a/backend/api/Services/AreaService.cs b/backend/api/Services/AreaService.cs index cf0f11acf..6b75025a6 100644 --- a/backend/api/Services/AreaService.cs +++ b/backend/api/Services/AreaService.cs @@ -25,6 +25,8 @@ public interface IAreaService public Task AddSafePosition(string installationCode, string areaName, SafePosition safePosition); public Task Delete(string id); + + public void DetachTracking(Area area); } [SuppressMessage( @@ -90,17 +92,17 @@ public async Task Create(CreateAreaQuery newAreaQuery, List position safePositions.Add(new SafePosition(pose)); } - var installation = await installationService.ReadByName(newAreaQuery.InstallationCode) ?? + var installation = await installationService.ReadByName(newAreaQuery.InstallationCode, readOnly: true) ?? throw new InstallationNotFoundException($"No installation with name {newAreaQuery.InstallationCode} could be found"); - var plant = await plantService.ReadByInstallationAndName(installation, newAreaQuery.PlantCode) ?? + var plant = await plantService.ReadByInstallationAndName(installation, newAreaQuery.PlantCode, readOnly: true) ?? throw new PlantNotFoundException($"No plant with name {newAreaQuery.PlantCode} could be found"); - var deck = await deckService.ReadByInstallationAndPlantAndName(installation, plant, newAreaQuery.DeckName) ?? + var deck = await deckService.ReadByInstallationAndPlantAndName(installation, plant, newAreaQuery.DeckName, readOnly: true) ?? throw new DeckNotFoundException($"No deck with name {newAreaQuery.DeckName} could be found"); var existingArea = await ReadByInstallationAndPlantAndDeckAndName( - installation, plant, deck, newAreaQuery.AreaName); + installation, plant, deck, newAreaQuery.AreaName, readOnly: true); if (existingArea != null) { throw new AreaExistsException($"Area with name {newAreaQuery.AreaName} already exists"); @@ -131,6 +133,8 @@ public async Task Create(CreateAreaQuery newAreaQuery, List position await context.Areas.AddAsync(newArea); await ApplyDatabaseUpdate(installation); + + DetachTracking(newArea); return newArea; } @@ -142,7 +146,7 @@ public async Task Create(CreateAreaQuery newArea) public async Task AddSafePosition(string installationCode, string areaName, SafePosition safePosition) { - var area = await ReadByInstallationAndName(installationCode, areaName); + var area = await ReadByInstallationAndName(installationCode, areaName, readOnly: false); if (area is null) { return null; } area.SafePositions.Add(safePosition); @@ -194,7 +198,7 @@ private IQueryable GetAreas(bool readOnly = false) .Include(area => area.Plant) .Include(area => area.Installation) .Where((area) => accessibleInstallationCodes.Result.Contains(area.Installation.InstallationCode.ToUpper())); - return readOnly ? query.AsNoTracking() : query; + return readOnly ? query.AsNoTracking() : query.AsTracking(); } private IQueryable GetAreasWithSubModels(bool readOnly = false) @@ -212,12 +216,12 @@ private IQueryable GetAreasWithSubModels(bool readOnly = false) .Include(a => a.Installation) .Include(a => a.DefaultLocalizationPose) .Where(a => a.Installation != null && accessibleInstallationCodes.Result.Contains(a.Installation.InstallationCode.ToUpper())); - return readOnly ? query.AsNoTracking() : query; + return readOnly ? query.AsNoTracking() : query.AsTracking(); } - public async Task ReadByInstallationAndPlantAndDeckAndName(Installation installation, Plant plant, Deck deck, string areaName) + public async Task ReadByInstallationAndPlantAndDeckAndName(Installation installation, Plant plant, Deck deck, string areaName, bool readOnly = false) { - return await GetAreas().Where(a => + return await GetAreas(readOnly: readOnly).Where(a => a.Deck != null && a.Deck.Id.Equals(deck.Id) && a.Plant.Id.Equals(plant.Id) && a.Installation.Id.Equals(installation.Id) && @@ -251,5 +255,13 @@ AreaQueryStringParameters parameters return Expression.Lambda>(body, area); } + public void DetachTracking(Area area) + { + if (area.Installation != null) installationService.DetachTracking(area.Installation); + if (area.Plant != null) plantService.DetachTracking(area.Plant); + if (area.Deck != null) deckService.DetachTracking(area.Deck); + if (area.DefaultLocalizationPose != null) defaultLocalizationPoseService.DetachTracking(area.DefaultLocalizationPose); + context.Entry(area).State = EntityState.Detached; + } } } diff --git a/backend/api/Services/DeckService.cs b/backend/api/Services/DeckService.cs index 7da93d954..3bfb86074 100644 --- a/backend/api/Services/DeckService.cs +++ b/backend/api/Services/DeckService.cs @@ -24,6 +24,8 @@ public interface IDeckService public Task Update(Deck deck); public Task Delete(string id); + + public void DetachTracking(Deck deck); } [SuppressMessage( @@ -82,11 +84,11 @@ public async Task> ReadByInstallation(string installationCode, public async Task Create(CreateDeckQuery newDeckQuery) { - var installation = await installationService.ReadByName(newDeckQuery.InstallationCode) ?? + var installation = await installationService.ReadByName(newDeckQuery.InstallationCode, readOnly: true) ?? throw new InstallationNotFoundException($"No installation with name {newDeckQuery.InstallationCode} could be found"); - var plant = await plantService.ReadByInstallationAndName(installation, newDeckQuery.PlantCode) ?? + var plant = await plantService.ReadByInstallationAndName(installation, newDeckQuery.PlantCode, readOnly: true) ?? throw new PlantNotFoundException($"No plant with name {newDeckQuery.PlantCode} could be found"); - var existingDeck = await ReadByInstallationAndPlantAndName(installation, plant, newDeckQuery.Name); + var existingDeck = await ReadByInstallationAndPlantAndName(installation, plant, newDeckQuery.Name, readOnly: true); if (existingDeck != null) { @@ -114,6 +116,7 @@ public async Task Create(CreateDeckQuery newDeckQuery) await context.Decks.AddAsync(deck); await ApplyDatabaseUpdate(deck.Installation); _ = signalRService.SendMessageAsync("Deck created", deck.Installation, new DeckResponse(deck)); + DetachTracking(deck); return deck!; } @@ -146,7 +149,7 @@ private IQueryable GetDecks(bool readOnly = false) var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); var query = context.Decks.Include(p => p.Plant).Include(i => i.Installation).Include(d => d.DefaultLocalizationPose) .Where((d) => accessibleInstallationCodes.Result.Contains(d.Installation.InstallationCode.ToUpper())); - return readOnly ? query.AsNoTracking() : query; + return readOnly ? query.AsNoTracking() : query.AsTracking(); } private async Task ApplyDatabaseUpdate(Installation? installation) @@ -157,5 +160,13 @@ private async Task ApplyDatabaseUpdate(Installation? installation) else throw new UnauthorizedAccessException($"User does not have permission to update deck in installation {installation.Name}"); } + + public void DetachTracking(Deck deck) + { + if (deck.Installation != null) installationService.DetachTracking(deck.Installation); + if (deck.Plant != null) plantService.DetachTracking(deck.Plant); + if (deck.DefaultLocalizationPose != null) defaultLocalizationPoseService.DetachTracking(deck.DefaultLocalizationPose); + context.Entry(deck).State = EntityState.Detached; + } } } diff --git a/backend/api/Services/DefaultLocalizationPoseService.cs b/backend/api/Services/DefaultLocalizationPoseService.cs index b48e05e73..3dd565cfd 100644 --- a/backend/api/Services/DefaultLocalizationPoseService.cs +++ b/backend/api/Services/DefaultLocalizationPoseService.cs @@ -7,9 +7,9 @@ namespace Api.Services { public interface IDefaultLocalizationPoseService { - public abstract Task> ReadAll(); + public abstract Task> ReadAll(bool readOnly = false); - public abstract Task ReadById(string id); + public abstract Task ReadById(string id, bool readOnly = false); public abstract Task Create(DefaultLocalizationPose defaultLocalizationPose); @@ -17,6 +17,7 @@ public interface IDefaultLocalizationPoseService public abstract Task Delete(string id); + public void DetachTracking(DefaultLocalizationPose defaultLocalizationPose); } [System.Diagnostics.CodeAnalysis.SuppressMessage( @@ -26,19 +27,19 @@ public interface IDefaultLocalizationPoseService )] public class DefaultLocalizationPoseService(FlotillaDbContext context) : IDefaultLocalizationPoseService { - public async Task> ReadAll() + public async Task> ReadAll(bool readOnly = false) { - return await GetDefaultLocalizationPoses().ToListAsync(); + return await GetDefaultLocalizationPoses(readOnly: readOnly).ToListAsync(); } - private DbSet GetDefaultLocalizationPoses() + private IQueryable GetDefaultLocalizationPoses(bool readOnly = false) { - return context.DefaultLocalizationPoses; + return readOnly ? context.DefaultLocalizationPoses.AsNoTracking() : context.DefaultLocalizationPoses.AsTracking(); } - public async Task ReadById(string id) + public async Task ReadById(string id, bool readOnly = false) { - return await GetDefaultLocalizationPoses() + return await GetDefaultLocalizationPoses(readOnly: readOnly) .FirstOrDefaultAsync(a => a.Id.Equals(id)); } @@ -48,6 +49,7 @@ public async Task Create(DefaultLocalizationPose defaul await context.DefaultLocalizationPoses.AddAsync(defaultLocalizationPose); await context.SaveChangesAsync(); + DetachTracking(defaultLocalizationPose); return defaultLocalizationPose; } @@ -72,5 +74,10 @@ public async Task Update(DefaultLocalizationPose defaul return defaultLocalizationPose; } + + public void DetachTracking(DefaultLocalizationPose defaultLocalizationPose) + { + context.Entry(defaultLocalizationPose).State = EntityState.Detached; + } } } diff --git a/backend/api/Services/ErrorHandlingService.cs b/backend/api/Services/ErrorHandlingService.cs new file mode 100644 index 000000000..570075c81 --- /dev/null +++ b/backend/api/Services/ErrorHandlingService.cs @@ -0,0 +1,29 @@ +using Api.Database.Models; +using Api.Utilities; +namespace Api.Services +{ + public interface IErrorHandlingService + { + public Task HandleLosingConnectionToIsar(string robotId); + } + + public class ErrorHandlingService(ILogger logger, IRobotService robotService, IMissionRunService missionRunService) : IErrorHandlingService + { + + public async Task HandleLosingConnectionToIsar(string robotId) + { + try + { + await missionRunService.UpdateCurrentRobotMissionToFailed(robotId); + await robotService.UpdateRobotStatus(robotId, RobotStatus.Offline); + await robotService.UpdateCurrentMissionId(robotId, null); + await robotService.UpdateRobotIsarConnected(robotId, false); + await robotService.UpdateCurrentArea(robotId, null); + } + catch (RobotNotFoundException) + { + logger.LogError("Robot with ID: {RobotId} was not found in the database", robotId); + } + } + } +} diff --git a/backend/api/Services/InspectionFindingService.cs b/backend/api/Services/InspectionFindingService.cs index c66241bc8..6a1543e87 100644 --- a/backend/api/Services/InspectionFindingService.cs +++ b/backend/api/Services/InspectionFindingService.cs @@ -6,20 +6,19 @@ namespace Api.Services { public class InspectionFindingService(FlotillaDbContext context, IAccessRoleService accessRoleService) { - public async Task> RetrieveInspectionFindings(DateTime lastReportingTime) + public async Task> RetrieveInspectionFindings(DateTime lastReportingTime, bool readOnly = false) { - var inspectionFindings = await context.InspectionFindings - .Where(f => f.InspectionDate > lastReportingTime) - .ToListAsync(); - return inspectionFindings; + var inspectionFindingsQuery = readOnly ? context.InspectionFindings.AsNoTracking() : context.InspectionFindings.AsTracking(); + return await inspectionFindingsQuery.Where(f => f.InspectionDate > lastReportingTime).ToListAsync(); } - public async Task GetMissionRunByIsarStepId(string isarStepId) + public async Task GetMissionRunByIsarStepId(string isarStepId, bool readOnly = false) { var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); + var query = readOnly ? context.MissionRuns.AsNoTracking() : context.MissionRuns.AsTracking(); + #pragma warning disable CA1304 - return await context.MissionRuns - .Include(missionRun => missionRun.Area).ThenInclude(area => area != null ? area.Plant : null) + return await query.Include(missionRun => missionRun.Area).ThenInclude(area => area != null ? area.Plant : null) .Include(missionRun => missionRun.Robot) .Include(missionRun => missionRun.Tasks).ThenInclude(task => task.Inspections) .Where(missionRun => missionRun.Tasks.Any(missionTask => missionTask.Inspections.Any(inspection => inspection.IsarStepId == isarStepId))) @@ -28,9 +27,9 @@ public async Task> RetrieveInspectionFindings(DateTime l #pragma warning restore CA1304 } - public async Task GetMissionTaskByIsarStepId(string isarStepId) + public async Task GetMissionTaskByIsarStepId(string isarStepId, bool readOnly = false) { - var missionRun = await GetMissionRunByIsarStepId(isarStepId); + var missionRun = await GetMissionRunByIsarStepId(isarStepId, readOnly: readOnly); return missionRun?.Tasks.Where(missionTask => missionTask.Inspections.Any(inspection => inspection.IsarStepId == isarStepId)).FirstOrDefault(); } } diff --git a/backend/api/Services/InspectionService.cs b/backend/api/Services/InspectionService.cs index d2b4cab03..2da77c5b6 100644 --- a/backend/api/Services/InspectionService.cs +++ b/backend/api/Services/InspectionService.cs @@ -11,7 +11,7 @@ namespace Api.Services public interface IInspectionService { public Task UpdateInspectionStatus(string isarStepId, IsarStepStatus isarStepStatus); - public Task ReadByIsarStepId(string id); + public Task ReadByIsarStepId(string id, bool readOnly = false); public Task AddFinding(InspectionFindingQuery inspectionFindingsQuery, string isarStepId); } @@ -25,7 +25,7 @@ public class InspectionService(FlotillaDbContext context, ILogger UpdateInspectionStatus(string isarStepId, IsarStepStatus isarStepStatus) { - var inspection = await ReadByIsarStepId(isarStepId); + var inspection = await ReadByIsarStepId(isarStepId, readOnly: false); if (inspection is null) { string errorMessage = $"Inspection with ID {isarStepId} could not be found"; @@ -54,7 +54,7 @@ private async Task Update(Inspection inspection) var missionRun = await context.MissionRuns .Include(missionRun => missionRun.Area).ThenInclude(area => area != null ? area.Installation : null) .Include(missionRun => missionRun.Robot) - .Where(missionRun => missionRun.Tasks.Any(missionTask => missionTask.Inspections.Any(i => i.Id == inspection.Id))) + .Where(missionRun => missionRun.Tasks.Any(missionTask => missionTask.Inspections.Any(i => i.Id == inspection.Id))).AsNoTracking() .FirstOrDefaultAsync(); var installation = missionRun?.Area?.Installation; @@ -63,22 +63,22 @@ private async Task Update(Inspection inspection) return entry.Entity; } - public async Task ReadByIsarStepId(string id) + public async Task ReadByIsarStepId(string id, bool readOnly = false) { - return await GetInspections().FirstOrDefaultAsync(inspection => inspection.IsarStepId != null && inspection.IsarStepId.Equals(id)); + return await GetInspections(readOnly: readOnly).FirstOrDefaultAsync(inspection => inspection.IsarStepId != null && inspection.IsarStepId.Equals(id)); } - private IQueryable GetInspections() + private IQueryable GetInspections(bool readOnly = false) { if (accessRoleService.IsUserAdmin() || !accessRoleService.IsAuthenticationAvailable()) - return context.Inspections.Include(inspection => inspection.InspectionFindings); + return (readOnly ? context.Inspections.AsNoTracking() : context.Inspections.AsTracking()).Include(inspection => inspection.InspectionFindings); else throw new UnauthorizedAccessException($"User does not have permission to view inspections"); } public async Task AddFinding(InspectionFindingQuery inspectionFindingQuery, string isarStepId) { - var inspection = await ReadByIsarStepId(isarStepId); + var inspection = await ReadByIsarStepId(isarStepId, readOnly: false); if (inspection is null) { diff --git a/backend/api/Services/InstallationService.cs b/backend/api/Services/InstallationService.cs index 9c63158c8..f3d9a4e3d 100644 --- a/backend/api/Services/InstallationService.cs +++ b/backend/api/Services/InstallationService.cs @@ -20,6 +20,7 @@ public interface IInstallationService public abstract Task Delete(string id); + public void DetachTracking(Installation installation); } [System.Diagnostics.CodeAnalysis.SuppressMessage( @@ -44,7 +45,7 @@ private IQueryable GetInstallations(bool readOnly = false) var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); var query = context.Installations .Where((i) => accessibleInstallationCodes.Result.Contains(i.InstallationCode.ToUpper())); - return readOnly ? query.AsNoTracking() : query; + return readOnly ? query.AsNoTracking() : query.AsTracking(); } private async Task ApplyUnprotectedDatabaseUpdate() @@ -88,6 +89,7 @@ public async Task Create(CreateInstallationQuery newInstallationQu }; await context.Installations.AddAsync(installation); await ApplyUnprotectedDatabaseUpdate(); + DetachTracking(installation); } return installation; @@ -114,5 +116,10 @@ public async Task Update(Installation installation) return installation; } + + public void DetachTracking(Installation installation) + { + context.Entry(installation).State = EntityState.Detached; + } } } diff --git a/backend/api/Services/LocalizationService.cs b/backend/api/Services/LocalizationService.cs index 935acadff..834dde290 100644 --- a/backend/api/Services/LocalizationService.cs +++ b/backend/api/Services/LocalizationService.cs @@ -33,7 +33,7 @@ public async Task EnsureRobotIsOnSameInstallationAsMission(Robot robot, MissionD public async Task RobotIsLocalized(string robotId) { - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot is null) { string errorMessage = $"Robot with ID: {robotId} was not found in the database"; @@ -46,7 +46,7 @@ public async Task RobotIsLocalized(string robotId) public async Task RobotIsOnSameDeckAsMission(string robotId, string areaId) { - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot is null) { string errorMessage = $"The robot with ID {robotId} was not found"; diff --git a/backend/api/Services/MissionDefinitionService.cs b/backend/api/Services/MissionDefinitionService.cs index 0a2765375..553094327 100644 --- a/backend/api/Services/MissionDefinitionService.cs +++ b/backend/api/Services/MissionDefinitionService.cs @@ -13,23 +13,25 @@ public interface IMissionDefinitionService { public Task Create(MissionDefinition missionDefinition); - public Task ReadById(string id); + public Task ReadById(string id, bool readOnly = false); - public Task> ReadAll(MissionDefinitionQueryStringParameters parameters); + public Task> ReadAll(MissionDefinitionQueryStringParameters parameters, bool readOnly = false); - public Task> ReadByAreaId(string areaId); + public Task> ReadByAreaId(string areaId, bool readOnly = false); - public Task> ReadByDeckId(string deckId); + public Task> ReadByDeckId(string deckId, bool readOnly = false); public Task?> GetTasksFromSource(Source source); - public Task> ReadBySourceId(string sourceId); + public Task> ReadBySourceId(string sourceId, bool readOnly = false); public Task UpdateLastSuccessfulMissionRun(string missionRunId, string missionDefinitionId); public Task Update(MissionDefinition missionDefinition); public Task Delete(string id); + + public void DetachTracking(MissionDefinition missionDefinition); } [SuppressMessage( @@ -47,7 +49,8 @@ public class MissionDefinitionService(FlotillaDbContext context, ISignalRService signalRService, IAccessRoleService accessRoleService, ILogger logger, - IMissionRunService missionRunService) : IMissionDefinitionService + IMissionRunService missionRunService, + ISourceService sourceService) : IMissionDefinitionService { public async Task Create(MissionDefinition missionDefinition) { @@ -58,18 +61,19 @@ public async Task Create(MissionDefinition missionDefinition) await context.MissionDefinitions.AddAsync(missionDefinition); await ApplyDatabaseUpdate(missionDefinition.Area?.Installation); _ = signalRService.SendMessageAsync("Mission definition created", missionDefinition.Area?.Installation, new MissionDefinitionResponse(missionDefinition)); + DetachTracking(missionDefinition); return missionDefinition; } - public async Task ReadById(string id) + public async Task ReadById(string id, bool readOnly = false) { - return await GetMissionDefinitionsWithSubModels().Where(m => m.IsDeprecated == false) + return await GetMissionDefinitionsWithSubModels(readOnly: readOnly).Where(m => m.IsDeprecated == false) .FirstOrDefaultAsync(missionDefinition => missionDefinition.Id.Equals(id)); } - public async Task> ReadAll(MissionDefinitionQueryStringParameters parameters) + public async Task> ReadAll(MissionDefinitionQueryStringParameters parameters, bool readOnly = false) { - var query = GetMissionDefinitionsWithSubModels().Where(m => m.IsDeprecated == false); + var query = GetMissionDefinitionsWithSubModels(readOnly: readOnly).Where(m => m.IsDeprecated == false); var filter = ConstructFilter(parameters); query = query.Where(filter); @@ -85,21 +89,21 @@ public async Task> ReadAll(MissionDefinitionQuerySt ); } - public async Task> ReadByAreaId(string areaId) + public async Task> ReadByAreaId(string areaId, bool readOnly = false) { - return await GetMissionDefinitionsWithSubModels().Where( + return await GetMissionDefinitionsWithSubModels(readOnly: readOnly).Where( m => m.IsDeprecated == false && m.Area != null && m.Area.Id == areaId).ToListAsync(); } - public async Task> ReadBySourceId(string sourceId) + public async Task> ReadBySourceId(string sourceId, bool readOnly = false) { - return await GetMissionDefinitionsWithSubModels().Where( + return await GetMissionDefinitionsWithSubModels(readOnly: readOnly).Where( m => m.IsDeprecated == false && m.Source.SourceId != null && m.Source.SourceId == sourceId).ToListAsync(); } - public async Task> ReadByDeckId(string deckId) + public async Task> ReadByDeckId(string deckId, bool readOnly = false) { - return await GetMissionDefinitionsWithSubModels().Where( + return await GetMissionDefinitionsWithSubModels(readOnly: readOnly).Where( m => m.IsDeprecated == false && m.Area != null && m.Area.Deck != null && m.Area.Deck.Id == deckId).ToListAsync(); } @@ -112,7 +116,7 @@ public async Task UpdateLastSuccessfulMissionRun(string missi logger.LogWarning("{Message}", errorMessage); throw new MissionNotFoundException(errorMessage); } - var missionDefinition = await ReadById(missionDefinitionId); + var missionDefinition = await ReadById(missionDefinitionId, readOnly: true); if (missionDefinition == null) { string errorMessage = $"Mission definition {missionDefinitionId} was not found"; @@ -165,14 +169,19 @@ private async Task ApplyDatabaseUpdate(Installation? installation) } - private IQueryable GetMissionDefinitionsWithSubModels() + private IQueryable GetMissionDefinitionsWithSubModels(bool readOnly = false) { var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); - return context.MissionDefinitions + var query = context.MissionDefinitions .Include(missionDefinition => missionDefinition.Area != null ? missionDefinition.Area.Deck : null) .ThenInclude(deck => deck != null ? deck.Plant : null) .ThenInclude(plant => plant != null ? plant.Installation : null) .Include(missionDefinition => missionDefinition.Area) + .ThenInclude(area => area != null ? area.Deck : null) + .Include(missionDefinition => missionDefinition.Area) + .ThenInclude(area => area != null ? area.Plant : null) + .Include(missionDefinition => missionDefinition.Area) + .ThenInclude(area => area != null ? area.Installation : null) .Include(missionDefinition => missionDefinition.Source) .Include(missionDefinition => missionDefinition.LastSuccessfulRun) .ThenInclude(missionRun => missionRun != null ? missionRun.Tasks : null)! @@ -182,6 +191,7 @@ private IQueryable GetMissionDefinitionsWithSubModels() .ThenInclude(deck => deck != null ? deck.DefaultLocalizationPose : null) .ThenInclude(defaultLocalizationPose => defaultLocalizationPose != null ? defaultLocalizationPose.Pose : null) .Where((m) => m.Area == null || accessibleInstallationCodes.Result.Contains(m.Area.Installation.InstallationCode.ToUpper())); + return readOnly ? query.AsNoTracking() : query.AsTracking(); } private static void SearchByName(ref IQueryable missionDefinitions, string? name) @@ -233,5 +243,12 @@ MissionDefinitionQueryStringParameters parameters // Constructing the resulting lambda expression by combining parameter and body return Expression.Lambda>(body, missionDefinitionExpression); } + + public void DetachTracking(MissionDefinition missionDefinition) + { + if (missionDefinition.LastSuccessfulRun != null) missionRunService.DetachTracking(missionDefinition.LastSuccessfulRun); + if (missionDefinition.Source != null) sourceService.DetachTracking(missionDefinition.Source); + context.Entry(missionDefinition).State = EntityState.Detached; + } } } diff --git a/backend/api/Services/MissionRunService.cs b/backend/api/Services/MissionRunService.cs index 0ab96623c..37cb878a5 100644 --- a/backend/api/Services/MissionRunService.cs +++ b/backend/api/Services/MissionRunService.cs @@ -6,6 +6,7 @@ using Api.Database.Context; using Api.Database.Models; using Api.Services.Events; +using Api.Services.Models; using Api.Utilities; using Microsoft.EntityFrameworkCore; namespace Api.Services @@ -44,18 +45,24 @@ public interface IMissionRunService public bool IncludesUnsupportedInspectionType(MissionRun missionRun); - public Task Update(MissionRun mission); - public Task UpdateMissionRunType(string missionRunId, MissionRunType missionRunType); public Task UpdateMissionRunStatusByIsarMissionId( string isarMissionId, MissionStatus missionStatus ); + public Task Delete(string id); - public Task OngoingMission(string robotId); + public Task UpdateMissionRunProperty(string missionRunId, string propertyName, object? value); + public Task UpdateWithIsarInfo(string missionRunId, IsarMission isarMission); + + public Task SetMissionRunToFailed(string missionRunId, string failureDescription); + + public Task UpdateCurrentRobotMissionToFailed(string robotId); + + public void DetachTracking(MissionRun missionRun); } [SuppressMessage( @@ -73,21 +80,14 @@ public class MissionRunService( ISignalRService signalRService, ILogger logger, IAccessRoleService accessRoleService, + IMissionTaskService missionTaskService, + IAreaService areaService, + IRobotService robotService, IUserInfoService userInfoService) : IMissionRunService { public async Task Create(MissionRun missionRun, bool triggerCreatedMissionRunEvent = true) { missionRun.Id ??= Guid.NewGuid().ToString(); // Useful for signalR messages - // Making sure database does not try to create new robot - try - { - context.Entry(missionRun.Robot).State = EntityState.Unchanged; - } - catch (InvalidOperationException e) - { - throw new DatabaseUpdateException($"Unable to create mission. {e}"); - } - if (IncludesUnsupportedInspectionType(missionRun)) { @@ -95,8 +95,10 @@ public async Task Create(MissionRun missionRun, bool triggerCreatedM } if (missionRun.Area is not null) { context.Entry(missionRun.Area).State = EntityState.Unchanged; } + if (missionRun.Robot is not null) { context.Entry(missionRun.Robot).State = EntityState.Unchanged; } await context.MissionRuns.AddAsync(missionRun); await ApplyDatabaseUpdate(missionRun.Area?.Installation); + _ = signalRService.SendMessageAsync("Mission run created", missionRun.Area?.Installation, new MissionRunResponse(missionRun)); if (triggerCreatedMissionRunEvent) @@ -117,6 +119,8 @@ public async Task Create(MissionRun missionRun, bool triggerCreatedM { logger.LogInformation(e, $"Failed to log user information because: {e.Message}"); } + DetachTracking(missionRun); + return missionRun; } @@ -206,7 +210,6 @@ public async Task> ReadMissionRuns(string robotId, MissionRunT .Where(m => m.Robot.Id == robotId) .Where(m => m.EndTime != null) .OrderByDescending(m => m.EndTime) - .AsNoTracking() .FirstOrDefaultAsync(); } @@ -285,6 +288,7 @@ public async Task Update(MissionRun missionRun) var entry = context.Update(missionRun); await ApplyDatabaseUpdate(missionRun.Area?.Installation); _ = signalRService.SendMessageAsync("Mission run updated", missionRun?.Area?.Installation, missionRun != null ? new MissionRunResponse(missionRun) : null); + DetachTracking(missionRun!); return entry.Entity; } @@ -303,20 +307,6 @@ public async Task Update(MissionRun missionRun) return missionRun; } - public async Task OngoingMission(string robotId) - { - var ongoingMissions = await ReadAll( - new MissionRunQueryStringParameters - { - Statuses = [MissionStatus.Ongoing], - RobotId = robotId, - OrderBy = "DesiredStartTime", - PageSize = 100 - }); - - return ongoingMissions.Any(); - } - private IQueryable GetMissionRunsWithSubModels(bool readOnly = false) { var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); @@ -343,7 +333,7 @@ private IQueryable GetMissionRunsWithSubModels(bool readOnly = false .ThenInclude(inspections => inspections.InspectionFindings) .Where((m) => m.Area == null || accessibleInstallationCodes.Result.Contains(m.Area.Installation.InstallationCode.ToUpper())) .Where((m) => m.IsDeprecated == false); - return readOnly ? query.AsNoTracking() : query; + return readOnly ? query.AsNoTracking() : query.AsTracking(); } protected virtual void OnMissionRunCreated(MissionRunCreatedEventArgs e) @@ -591,7 +581,7 @@ public async Task UpdateMissionRunStatusByIsarMissionId(string isarM public async Task UpdateMissionRunProperty(string missionRunId, string propertyName, object? value) { - var missionRun = await ReadById(missionRunId); + var missionRun = await ReadById(missionRunId, readOnly: false); if (missionRun is null) { string errorMessage = $"Mission with ID {missionRunId} was not found in the database"; @@ -612,5 +602,61 @@ public async Task UpdateMissionRunProperty(string missionRunId, stri catch (InvalidOperationException e) { logger.LogError(e, "Failed to update {missionRunName}", missionRun.Name); }; return missionRun; } + + public async Task UpdateCurrentRobotMissionToFailed(string robotId) + { + var robot = await robotService.ReadById(robotId, readOnly: true) ?? throw new RobotNotFoundException($"Robot with ID: {robotId} was not found in the database"); + if (robot.CurrentMissionId != null) + { + var missionRun = await SetMissionRunToFailed(robot.CurrentMissionId, "Lost connection to ISAR during mission"); + logger.LogWarning( + "Mission '{Id}' failed because ISAR could not be reached", + missionRun.Id + ); + } + } + + public async Task SetMissionRunToFailed(string missionRunId, string failureDescription) + { + var missionRun = await ReadById(missionRunId, readOnly: false) ?? throw new MissionRunNotFoundException($"Could not find mission run with ID {missionRunId}"); + + missionRun.Status = MissionStatus.Failed; + missionRun.StatusReason = failureDescription; + foreach (var task in missionRun.Tasks.Where(task => !task.IsCompleted)) + { + task.Status = Database.Models.TaskStatus.Failed; + foreach ( + var inspection in task.Inspections.Where(inspection => !inspection.IsCompleted) + ) + { + inspection.Status = InspectionStatus.Failed; + } + } + return await Update(missionRun); + } + + public void DetachTracking(MissionRun missionRun) + { + foreach (var task in missionRun.Tasks) + { + missionTaskService.DetachTracking(task); + } + if (missionRun.Area != null) areaService.DetachTracking(missionRun.Area); + if (missionRun.Robot != null) robotService.DetachTracking(missionRun.Robot); + context.Entry(missionRun).State = EntityState.Detached; + } + + public async Task UpdateWithIsarInfo(string missionRunId, IsarMission isarMission) + { + var missionRun = await ReadById(missionRunId, readOnly: false) ?? throw new MissionRunNotFoundException($"Could not find mission run with ID {missionRunId}"); + + missionRun.IsarMissionId = isarMission.IsarMissionId; + foreach (var isarTask in isarMission.Tasks) + { + var task = missionRun.GetTaskByIsarId(isarTask.IsarTaskId); + task?.UpdateWithIsarInfo(isarTask); + } + return await Update(missionRun); + } } } diff --git a/backend/api/Services/MissionSchedulingService.cs b/backend/api/Services/MissionSchedulingService.cs index 8cfdc6fc2..6299a4299 100644 --- a/backend/api/Services/MissionSchedulingService.cs +++ b/backend/api/Services/MissionSchedulingService.cs @@ -33,12 +33,12 @@ public interface IMissionSchedulingService } public class MissionSchedulingService(ILogger logger, IMissionRunService missionRunService, IRobotService robotService, - IAreaService areaService, IIsarService isarService, ILocalizationService localizationService, IReturnToHomeService returnToHomeService, ISignalRService signalRService) : IMissionSchedulingService + IAreaService areaService, IIsarService isarService, ILocalizationService localizationService, IReturnToHomeService returnToHomeService, ISignalRService signalRService, IErrorHandlingService errorHandlingService) : IMissionSchedulingService { public async Task StartNextMissionRunIfSystemIsAvailable(string robotId) { logger.LogInformation("Starting next mission run if system is available for robot ID: {RobotId}", robotId); - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot == null) { logger.LogError("Robot with ID: {RobotId} was not found in the database", robotId); @@ -89,7 +89,7 @@ public async Task StartNextMissionRunIfSystemIsAvailable(string robotId) if (missionRun == null) { return; } // The robot is already home - var postReturnToHomeMissionCreatedRobot = await robotService.ReadById(missionRun.Robot.Id); + var postReturnToHomeMissionCreatedRobot = await robotService.ReadById(missionRun.Robot.Id, readOnly: true); if (postReturnToHomeMissionCreatedRobot == null) { logger.LogInformation("Could not find robot {Name}", missionRun.Robot.Name); @@ -149,15 +149,13 @@ or RobotNotAvailableException or MissionRunNotFoundException or IsarCommunicationException) { - const MissionStatus NewStatus = MissionStatus.Failed; logger.LogError( ex, "Mission run {MissionRunId} was not started successfully due to {ErrorMessage}", missionRun.Id, ex.Message ); - await missionRunService.UpdateMissionRunProperty(missionRun.Id, "Status", NewStatus); - await missionRunService.UpdateMissionRunProperty(missionRun.Id, "StatusReason", ex.Message); + await missionRunService.SetMissionRunToFailed(missionRun.Id, "Mission run {MissionRunId} was not started successfully due to {ErrorMessage}"); } } @@ -190,7 +188,7 @@ or MissionRunNotFoundException public async Task OngoingMission(string robotId) { - var ongoingMissions = await GetOngoingMissions(robotId); + var ongoingMissions = await GetOngoingMissions(robotId, readOnly: true); return ongoingMissions is not null && ongoingMissions.Any(); } @@ -209,7 +207,7 @@ public async Task UnfreezeMissionRunQueueForRobot(string robotId) public async Task StopCurrentMissionRun(string robotId) { - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot == null) { string errorMessage = $"Robot with ID: {robotId} was not found in the database"; @@ -217,7 +215,7 @@ public async Task StopCurrentMissionRun(string robotId) throw new RobotNotFoundException(errorMessage); } - var ongoingMissionRuns = await GetOngoingMissions(robotId); + var ongoingMissionRuns = await GetOngoingMissions(robotId, readOnly: true); if (ongoingMissionRuns is null) { string errorMessage = $"There were no ongoing mission runs to stop for robot {robotId}"; @@ -232,7 +230,7 @@ public async Task StopCurrentMissionRun(string robotId) { const string Message = "Error connecting to ISAR while stopping mission"; logger.LogError(e, "{Message}", Message); - await robotService.HandleLosingConnectionToIsar(robot.Id); + await errorHandlingService.HandleLosingConnectionToIsar(robot.Id); throw new MissionException(Message, (int)e.StatusCode!); } catch (MissionException e) @@ -257,7 +255,7 @@ public async Task StopCurrentMissionRun(string robotId) public async Task AbortAllScheduledMissions(string robotId, string? abortReason) { - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot == null) { string errorMessage = $"Robot with ID: {robotId} was not found in the database"; @@ -265,7 +263,7 @@ public async Task AbortAllScheduledMissions(string robotId, string? abortReason) throw new RobotNotFoundException(errorMessage); } - var pendingMissionRuns = await missionRunService.ReadMissionRunQueue(robotId); + var pendingMissionRuns = await missionRunService.ReadMissionRunQueue(robotId, readOnly: true); if (pendingMissionRuns is null) { string infoMessage = $"There were no mission runs in the queue to abort for robot {robotId}"; @@ -290,7 +288,7 @@ public async Task ScheduleMissionToDriveToSafePosition(string robotId, string ar logger.LogError("Could not find area with ID {AreaId}", areaId); return; } - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot == null) { logger.LogError("Robot with ID: {RobotId} was not found in the database", robotId); @@ -349,7 +347,7 @@ public void TriggerLocalizationMissionSuccessful(LocalizationMissionSuccessfulEv private async Task SelectNextMissionRun(string robotId) { - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot == null) { string errorMessage = $"Could not find robot with id {robotId}"; @@ -412,7 +410,7 @@ private async Task StartMissionRun(MissionRun queuedMissionRun) string robotId = queuedMissionRun.Robot.Id; string missionRunId = queuedMissionRun.Id; - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot == null) { string errorMessage = $"Could not find robot with id {robotId}"; @@ -434,7 +432,7 @@ private async Task StartMissionRun(MissionRun queuedMissionRun) throw new RobotNotAvailableException(errorMessage); } - var missionRun = await missionRunService.ReadById(missionRunId); + var missionRun = await missionRunService.ReadById(missionRunId, readOnly: true); if (missionRun == null) { string errorMessage = $"Could not find mission run with id {missionRunId}"; @@ -448,7 +446,7 @@ private async Task StartMissionRun(MissionRun queuedMissionRun) { string errorMessage = $"Could not reach ISAR at {robot.IsarUri}"; logger.LogError(e, "{Message}", errorMessage); - await robotService.HandleLosingConnectionToIsar(robot.Id); + await errorHandlingService.HandleLosingConnectionToIsar(robot.Id); throw new IsarCommunicationException(errorMessage); } catch (MissionException e) @@ -464,7 +462,7 @@ private async Task StartMissionRun(MissionRun queuedMissionRun) throw new IsarCommunicationException(ErrorMessage); } - missionRun.UpdateWithIsarInfo(isarMission); + await missionRunService.UpdateWithIsarInfo(missionRun.Id, isarMission); await missionRunService.UpdateMissionRunProperty(missionRun.Id, "Status", MissionStatus.Ongoing); robot.Status = RobotStatus.Busy; @@ -497,7 +495,7 @@ private static Pose ClosestSafePosition(Pose robotPose, IList safe return closestPose; } - private async Task?> GetOngoingMissions(string robotId) + private async Task?> GetOngoingMissions(string robotId, bool readOnly = false) { var ongoingMissions = await missionRunService.ReadAll( new MissionRunQueryStringParameters @@ -506,7 +504,7 @@ private static Pose ClosestSafePosition(Pose robotPose, IList safe RobotId = robotId, OrderBy = "DesiredStartTime", PageSize = 100 - }, readOnly: true); + }, readOnly: readOnly); return ongoingMissions; } @@ -521,7 +519,7 @@ private async Task TheSystemIsAvailableToRunAMission(string robotId, strin return false; } - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot is null) { string errorMessage = $"Robot with ID: {robotId} was not found in the database"; @@ -568,7 +566,7 @@ private async Task TheSystemIsAvailableToRunAMission(string robotId, strin public async Task AbortActiveReturnToHomeMission(string robotId) { - var activeReturnToHomeMission = await returnToHomeService.GetActiveReturnToHomeMissionRun(robotId); + var activeReturnToHomeMission = await returnToHomeService.GetActiveReturnToHomeMissionRun(robotId, readOnly: true); if (activeReturnToHomeMission == null) { diff --git a/backend/api/Services/MissionTaskService.cs b/backend/api/Services/MissionTaskService.cs index b39118a15..7fae604dd 100644 --- a/backend/api/Services/MissionTaskService.cs +++ b/backend/api/Services/MissionTaskService.cs @@ -9,6 +9,8 @@ namespace Api.Services public interface IMissionTaskService { public Task UpdateMissionTaskStatus(string isarTaskId, IsarTaskStatus isarTaskStatus); + + public void DetachTracking(MissionTask missionTask); } [SuppressMessage( @@ -20,7 +22,7 @@ public class MissionTaskService(FlotillaDbContext context, ILogger UpdateMissionTaskStatus(string isarTaskId, IsarTaskStatus isarTaskStatus) { - var missionTask = await ReadByIsarTaskId(isarTaskId); + var missionTask = await ReadByIsarTaskId(isarTaskId, readOnly: false); if (missionTask is null) { string errorMessage = $"Inspection with ID {isarTaskId} could not be found"; @@ -41,14 +43,20 @@ private async Task Update(MissionTask missionTask) return entry.Entity; } - private async Task ReadByIsarTaskId(string id) + private async Task ReadByIsarTaskId(string id, bool readOnly = false) + { + return await GetMissionTasks(readOnly: readOnly).FirstOrDefaultAsync(missionTask => missionTask.IsarTaskId != null && missionTask.IsarTaskId.Equals(id)); + } + + private IQueryable GetMissionTasks(bool readOnly = false) { - return await GetMissionTasks().FirstOrDefaultAsync(missionTask => missionTask.IsarTaskId != null && missionTask.IsarTaskId.Equals(id)); + return (readOnly ? context.MissionTasks.AsNoTracking() : context.MissionTasks.AsTracking()) + .Include(missionTask => missionTask.Inspections).ThenInclude(inspection => inspection.InspectionFindings); } - private IQueryable GetMissionTasks() + public void DetachTracking(MissionTask missionTask) { - return context.MissionTasks.Include(missionTask => missionTask.Inspections).ThenInclude(inspection => inspection.InspectionFindings); + context.Entry(missionTask).State = EntityState.Detached; } } } diff --git a/backend/api/Services/PlantService.cs b/backend/api/Services/PlantService.cs index cd913732d..138c38b98 100644 --- a/backend/api/Services/PlantService.cs +++ b/backend/api/Services/PlantService.cs @@ -24,6 +24,8 @@ public interface IPlantService public Task Update(Plant plant); public Task Delete(string id); + + public void DetachTracking(Plant plant); } [SuppressMessage( @@ -76,10 +78,10 @@ public async Task> ReadByInstallation(string installationCode public async Task Create(CreatePlantQuery newPlantQuery) { - var installation = await installationService.ReadByName(newPlantQuery.InstallationCode) ?? + var installation = await installationService.ReadByName(newPlantQuery.InstallationCode, readOnly: true) ?? throw new InstallationNotFoundException($"No installation with name {newPlantQuery.InstallationCode} could be found"); - var plant = await ReadByInstallationAndName(installation, newPlantQuery.PlantCode); + var plant = await ReadByInstallationAndName(installation, newPlantQuery.PlantCode, readOnly: true); if (plant == null) { plant = new Plant @@ -91,6 +93,7 @@ public async Task Create(CreatePlantQuery newPlantQuery) context.Entry(plant.Installation).State = EntityState.Unchanged; await context.Plants.AddAsync(plant); await ApplyDatabaseUpdate(plant.Installation); + DetachTracking(plant); } return plant!; } @@ -122,7 +125,7 @@ private IQueryable GetPlants(bool readOnly = false) var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); var query = context.Plants.Include(i => i.Installation) .Where((p) => accessibleInstallationCodes.Result.Contains(p.Installation.InstallationCode.ToUpper())); - return readOnly ? query.AsNoTracking() : query; + return readOnly ? query.AsNoTracking() : query.AsTracking(); } private async Task ApplyDatabaseUpdate(Installation? installation) @@ -133,5 +136,11 @@ private async Task ApplyDatabaseUpdate(Installation? installation) else throw new UnauthorizedAccessException($"User does not have permission to update plant in installation {installation.Name}"); } + + public void DetachTracking(Plant plant) + { + if (plant.Installation != null) installationService.DetachTracking(plant.Installation); + context.Entry(plant).State = EntityState.Detached; + } } } diff --git a/backend/api/Services/ReturnToHomeService.cs b/backend/api/Services/ReturnToHomeService.cs index 9d9087d39..ff85c36c3 100644 --- a/backend/api/Services/ReturnToHomeService.cs +++ b/backend/api/Services/ReturnToHomeService.cs @@ -5,7 +5,7 @@ namespace Api.Services public interface IReturnToHomeService { public Task ScheduleReturnToHomeMissionRunIfNotAlreadyScheduledOrRobotIsHome(string robotId); - public Task GetActiveReturnToHomeMissionRun(string robotId); + public Task GetActiveReturnToHomeMissionRun(string robotId, bool readOnly = false); } public class ReturnToHomeService(ILogger logger, IRobotService robotService, IMissionRunService missionRunService, IMapService mapService) : IReturnToHomeService @@ -54,7 +54,7 @@ private async Task IsReturnToHomeMissionAlreadyScheduled(string robotId) } private async Task ScheduleReturnToHomeMissionRun(string robotId) { - var robot = await robotService.ReadById(robotId); + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot is null) { string errorMessage = $"Robot with ID {robotId} could not be retrieved from the database"; @@ -112,10 +112,10 @@ private async Task ScheduleReturnToHomeMissionRun(string robotId) return missionRun; } - public async Task GetActiveReturnToHomeMissionRun(string robotId) + public async Task GetActiveReturnToHomeMissionRun(string robotId, bool readOnly = false) { IList missionStatuses = [MissionStatus.Ongoing, MissionStatus.Pending, MissionStatus.Paused]; - var activeReturnToHomeMissions = await missionRunService.ReadMissionRuns(robotId, MissionRunType.ReturnHome, missionStatuses); + var activeReturnToHomeMissions = await missionRunService.ReadMissionRuns(robotId, MissionRunType.ReturnHome, missionStatuses, readOnly: readOnly); if (activeReturnToHomeMissions.Count == 0) { return null; } diff --git a/backend/api/Services/RobotModelService.cs b/backend/api/Services/RobotModelService.cs index 04d4be450..2b2a09088 100644 --- a/backend/api/Services/RobotModelService.cs +++ b/backend/api/Services/RobotModelService.cs @@ -17,6 +17,8 @@ public interface IRobotModelService public abstract Task Update(RobotModel robotModel); public abstract Task Delete(string id); + + public void DetachTracking(RobotModel robotModel); } [System.Diagnostics.CodeAnalysis.SuppressMessage( @@ -32,7 +34,7 @@ public RobotModelService(FlotillaDbContext context) { _context = context; - if (!ReadAll().Result.Any()) + if (!ReadAll(readOnly: true).Result.Any()) { // If no models in database, add default ones // Robot models are essentially database enums and should just be added to all databases @@ -48,7 +50,7 @@ public async Task> ReadAll(bool readOnly = false) private IQueryable GetRobotModels(bool readOnly = false) { - return readOnly ? _context.RobotModels.AsNoTracking() : _context.RobotModels; + return readOnly ? _context.RobotModels.AsNoTracking() : _context.RobotModels.AsTracking(); } public async Task ReadById(string id, bool readOnly = false) @@ -67,6 +69,7 @@ public async Task Create(RobotModel newRobotModel) { await _context.RobotModels.AddAsync(newRobotModel); await _context.SaveChangesAsync(); + DetachTracking(newRobotModel); return newRobotModel; } @@ -90,5 +93,10 @@ public async Task Update(RobotModel robotModel) return robotModel; } + + public void DetachTracking(RobotModel robotModel) + { + _context.Entry(robotModel).State = EntityState.Detached; + } } } diff --git a/backend/api/Services/RobotService.cs b/backend/api/Services/RobotService.cs index f4d27bd11..26623ecf2 100644 --- a/backend/api/Services/RobotService.cs +++ b/backend/api/Services/RobotService.cs @@ -11,10 +11,9 @@ public interface IRobotService { public Task Create(Robot newRobot); public Task CreateFromQuery(CreateRobotQuery robotQuery); - public Task GetRobotWithPreCheck(string robotId, bool readOnly = false); public Task> ReadAll(bool readOnly = false); - public Task> ReadAllActivePlants(); + public Task> ReadAllActivePlants(bool readOnly = false); public Task ReadById(string id, bool readOnly = false); public Task ReadByIsarId(string isarId, bool readOnly = false); public Task> ReadRobotsForInstallation(string installationCode, bool readOnly = false); @@ -30,7 +29,7 @@ public interface IRobotService public Task UpdateMissionQueueFrozen(string robotId, bool missionQueueFrozen); public Task UpdateFlotillaStatus(string robotId, RobotFlotillaStatus status); public Task Delete(string id); - public Task HandleLosingConnectionToIsar(string robotId); + public void DetachTracking(Robot robot); } [SuppressMessage( @@ -45,22 +44,24 @@ public class RobotService( ISignalRService signalRService, IAccessRoleService accessRoleService, IInstallationService installationService, - IAreaService areaService, - IMissionRunService missionRunService) : IRobotService + IAreaService areaService) : IRobotService { public async Task Create(Robot newRobot) { if (newRobot.CurrentArea is not null) context.Entry(newRobot.CurrentArea).State = EntityState.Unchanged; + if (newRobot.CurrentInstallation != null) context.Entry(newRobot.CurrentInstallation).State = EntityState.Unchanged; + if (newRobot.Model != null) context.Entry(newRobot.Model).State = EntityState.Unchanged; await context.Robots.AddAsync(newRobot); await ApplyDatabaseUpdate(newRobot.CurrentInstallation); + DetachTracking(newRobot); return newRobot; } public async Task CreateFromQuery(CreateRobotQuery robotQuery) { - var robotModel = await robotModelService.ReadByRobotType(robotQuery.RobotType); + var robotModel = await robotModelService.ReadByRobotType(robotQuery.RobotType, readOnly: true); if (robotModel != null) { var installation = await installationService.ReadByName(robotQuery.CurrentInstallationCode, readOnly: true); @@ -85,13 +86,15 @@ public async Task CreateFromQuery(CreateRobotQuery robotQuery) { Model = robotModel }; - context.Entry(robotModel).State = EntityState.Unchanged; + if (newRobot.CurrentArea is not null) context.Entry(newRobot.CurrentArea).State = EntityState.Unchanged; - if (newRobot.CurrentInstallation is not null) context.Entry(newRobot.CurrentInstallation).State = EntityState.Unchanged; + if (newRobot.CurrentInstallation != null) context.Entry(newRobot.CurrentInstallation).State = EntityState.Unchanged; + if (newRobot.Model != null) context.Entry(newRobot.Model).State = EntityState.Unchanged; await context.Robots.AddAsync(newRobot); await ApplyDatabaseUpdate(newRobot.CurrentInstallation); _ = signalRService.SendMessageAsync("Robot added", newRobot!.CurrentInstallation, new RobotResponse(newRobot!)); + DetachTracking(newRobot); return newRobot!; } throw new DbUpdateException("Could not create new robot in database as robot model does not exist"); @@ -155,7 +158,7 @@ public async Task UpdateRobotStatus(string robotId, RobotStatus status) public async Task UpdateRobotBatteryLevel(string robotId, float batteryLevel) { - var robotQuery = context.Robots.Where(robot => robot.Id == robotId).Include(robot => robot.CurrentInstallation); + var robotQuery = context.Robots.Where(robot => robot.Id == robotId).Include(robot => robot.CurrentInstallation).AsTracking(); var robot = await robotQuery.FirstOrDefaultAsync(); ThrowIfRobotIsNull(robot, robotId); @@ -166,6 +169,7 @@ public async Task UpdateRobotBatteryLevel(string robotId, float batteryLe robot = await robotQuery.FirstOrDefaultAsync(); ThrowIfRobotIsNull(robot, robotId); NotifySignalROfUpdatedRobot(robot!, robot!.CurrentInstallation!); + DetachTracking(robot); return robot; } @@ -183,6 +187,7 @@ public async Task UpdateRobotPressureLevel(string robotId, float? pressur robot = await robotQuery.FirstOrDefaultAsync(); ThrowIfRobotIsNull(robot, robotId); NotifySignalROfUpdatedRobot(robot!, robot!.CurrentInstallation!); + DetachTracking(robot); return robot; } @@ -221,6 +226,7 @@ await robotQuery robot = await robotQuery.FirstOrDefaultAsync(); ThrowIfRobotIsNull(robot, robotId); NotifySignalROfUpdatedRobot(robot!, robot!.CurrentInstallation!); + DetachTracking(robot); return robot; } @@ -238,6 +244,7 @@ public async Task UpdateRobotIsarConnected(string robotId, bool isarConne robot = await robotQuery.FirstOrDefaultAsync(); ThrowIfRobotIsNull(robot, robotId); NotifySignalROfUpdatedRobot(robot!, robot!.CurrentInstallation!); + DetachTracking(robot); return robot; } @@ -255,6 +262,7 @@ public async Task UpdateCurrentMissionId(string robotId, string? currentM robot = await robotQuery.FirstOrDefaultAsync(); ThrowIfRobotIsNull(robot, robotId); NotifySignalROfUpdatedRobot(robot!, robot!.CurrentInstallation!); + DetachTracking(robot); return robot; } @@ -263,7 +271,7 @@ public async Task UpdateCurrentArea(string robotId, string? areaId) { logger.LogInformation("Updating current area for robot with Id {robotId} to area with Id {areaId}", robotId, areaId); if (areaId is null) { return await UpdateRobotProperty(robotId, "CurrentArea", null); } - var area = await areaService.ReadById(areaId); + var area = await areaService.ReadById(areaId, readOnly: true); if (area is null) { logger.LogError("Could not find area '{AreaId}' setting robot '{IsarId}' area to null", areaId, robotId); @@ -289,24 +297,24 @@ public async Task UpdateFlotillaStatus(string robotId, RobotFlotillaStatu public async Task ReadByIsarId(string isarId, bool readOnly = false) { - return await GetRobotsWithSubModels() + return await GetRobotsWithSubModels(readOnly: readOnly) .FirstOrDefaultAsync(robot => robot.IsarId.Equals(isarId)); } - public async Task> ReadAllActivePlants() + public async Task> ReadAllActivePlants(bool readOnly = false) { - return await GetRobotsWithSubModels().Where(r => r.IsarConnected && r.CurrentInstallation != null).Select(r => r.CurrentInstallation!.InstallationCode).ToListAsync(); + return await GetRobotsWithSubModels(readOnly: readOnly).Where(r => r.IsarConnected && r.CurrentInstallation != null).Select(r => r.CurrentInstallation!.InstallationCode).ToListAsync(); } public async Task Update(Robot robot) { if (robot.CurrentArea is not null) context.Entry(robot.CurrentArea).State = EntityState.Unchanged; - context.Entry(robot.Model).State = EntityState.Unchanged; var entry = context.Update(robot); await ApplyDatabaseUpdate(robot.CurrentInstallation); _ = signalRService.SendMessageAsync("Robot updated", robot?.CurrentInstallation, robot != null ? new RobotResponse(robot) : null); + DetachTracking(robot!); return entry.Entity; } @@ -333,40 +341,6 @@ public async Task> ReadRobotsForInstallation(string installationCod .ToListAsync(); } - public async Task UpdateCurrentRobotMissionToFailed(string robotId) - { - var robot = await ReadById(robotId) ?? throw new RobotNotFoundException($"Robot with ID: {robotId} was not found in the database"); - if (robot.CurrentMissionId != null) - { - var missionRun = await missionRunService.ReadById(robot.CurrentMissionId); - if (missionRun != null) - { - missionRun.SetToFailed("Lost connection to ISAR during mission"); - await missionRunService.Update(missionRun); - logger.LogWarning( - "Mission '{Id}' failed because ISAR could not be reached", - missionRun.Id - ); - } - } - } - - public async Task HandleLosingConnectionToIsar(string robotId) - { - try - { - await UpdateCurrentRobotMissionToFailed(robotId); - await UpdateRobotStatus(robotId, RobotStatus.Offline); - await UpdateCurrentMissionId(robotId, null); - await UpdateRobotIsarConnected(robotId, false); - await UpdateCurrentArea(robotId, null); - } - catch (RobotNotFoundException) - { - logger.LogError("Robot with ID: {RobotId} was not found in the database", robotId); - } - } - private IQueryable GetRobotsWithSubModels(bool readOnly = false) { var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); @@ -389,12 +363,12 @@ private IQueryable GetRobotsWithSubModels(bool readOnly = false) #pragma warning disable CA1304 .Where((r) => r.CurrentInstallation == null || r.CurrentInstallation.InstallationCode == null || accessibleInstallationCodes.Result.Contains(r.CurrentInstallation.InstallationCode.ToUpper())); #pragma warning restore CA1304 - return readOnly ? query.AsNoTracking() : query; + return readOnly ? query.AsNoTracking() : query.AsTracking(); } private async Task UpdateRobotProperty(string robotId, string propertyName, object? value) { - var robot = await ReadById(robotId); + var robot = await ReadById(robotId, readOnly: false); if (robot is null) { string errorMessage = $"Robot with ID {robotId} was not found in the database"; @@ -413,6 +387,7 @@ private async Task UpdateRobotProperty(string robotId, string propertyNam try { robot = await Update(robot); } catch (InvalidOperationException e) { logger.LogError(e, "Failed to update {robotName}", robot.Name); }; + DetachTracking(robot); return robot; } @@ -438,5 +413,12 @@ private void NotifySignalROfUpdatedRobot(Robot robot, Installation installation) _ = signalRService.SendMessageAsync("Robot updated", installation, robot != null ? new RobotResponse(robot) : null); } + public void DetachTracking(Robot robot) + { + if (robot.CurrentInstallation != null) installationService.DetachTracking(robot.CurrentInstallation); + if (robot.CurrentArea != null) areaService.DetachTracking(robot.CurrentArea); + if (robot.Model != null) robotModelService.DetachTracking(robot.Model); + context.Entry(robot).State = EntityState.Detached; + } } } diff --git a/backend/api/Services/SourceService.cs b/backend/api/Services/SourceService.cs index 57fe4f514..1527c66e4 100644 --- a/backend/api/Services/SourceService.cs +++ b/backend/api/Services/SourceService.cs @@ -10,20 +10,21 @@ public interface ISourceService { public abstract Task Create(Source source); - public abstract Task> ReadAll(); + public abstract Task> ReadAll(bool readOnly = false); - public abstract Task ReadById(string id); + public abstract Task ReadById(string id, bool readOnly = false); public abstract Task CheckForExistingSource(string sourceId); public abstract Task CheckForExistingSourceFromTasks(IList tasks); - public abstract Task CreateSourceIfDoesNotExist(List tasks); + public abstract Task CreateSourceIfDoesNotExist(List tasks, bool readOnly = false); public abstract Task Update(Source source); public abstract Task Delete(string id); + public void DetachTracking(Source source); } [System.Diagnostics.CodeAnalysis.SuppressMessage( @@ -39,30 +40,31 @@ public async Task Create(Source source) { context.Sources.Add(source); await context.SaveChangesAsync(); + DetachTracking(source); return source; } - public async Task> ReadAll() + public async Task> ReadAll(bool readOnly = false) { - var query = GetSources(); + var query = GetSources(readOnly: readOnly); return await query.ToListAsync(); } - private DbSet GetSources() + private IQueryable GetSources(bool readOnly = false) { - return context.Sources; + return readOnly ? context.Sources.AsNoTracking() : context.Sources.AsTracking(); } - public async Task ReadById(string id) + public async Task ReadById(string id, bool readOnly = false) { - return await GetSources() + return await GetSources(readOnly: readOnly) .FirstOrDefaultAsync(s => s.Id.Equals(id)); } - public async Task ReadBySourceId(string sourceId) + public async Task ReadBySourceId(string sourceId, bool readOnly = false) { - return await GetSources() + return await GetSources(readOnly: readOnly) .FirstOrDefaultAsync(s => s.SourceId.Equals(sourceId)); } @@ -79,7 +81,7 @@ private DbSet GetSources() public async Task?> GetMissionTasksFromSourceId(string id) { - var existingSource = await ReadBySourceId(id); + var existingSource = await ReadBySourceId(id, readOnly: true); if (existingSource == null || existingSource.CustomMissionTasks == null) return null; try @@ -102,12 +104,12 @@ private DbSet GetSources() } } - public async Task CreateSourceIfDoesNotExist(List tasks) + public async Task CreateSourceIfDoesNotExist(List tasks, bool readOnly = false) { string json = JsonSerializer.Serialize(tasks); string hash = MissionTask.CalculateHashFromTasks(tasks); - var existingSource = await ReadById(hash); + var existingSource = await ReadById(hash, readOnly: readOnly); if (existingSource != null) return existingSource; @@ -119,6 +121,7 @@ public async Task CreateSourceIfDoesNotExist(List tasks) } ); + DetachTracking(newSource); return newSource; } @@ -143,5 +146,10 @@ public async Task Update(Source source) return source; } + + public void DetachTracking(Source source) + { + context.Entry(source).State = EntityState.Detached; + } } }