Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix concurrency issues for updating robot object #1092

Merged
merged 9 commits into from
Nov 2, 2023
13 changes: 6 additions & 7 deletions backend/api.test/Mocks/RobotControllerMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@
using Api.Services;
using Microsoft.Extensions.Logging;
using Moq;

namespace Api.Test.Mocks
{
internal class RobotControllerMock
{
public Mock<IIsarService> IsarServiceMock;
public Mock<IRobotService> RobotServiceMock;
public Mock<IRobotModelService> RobotModelServiceMock;
public Mock<IMissionRunService> MissionServiceMock;
public Mock<RobotController> Mock;
public Mock<IAreaService> AreaServiceMock;
public readonly Mock<IAreaService> AreaServiceMock;
public readonly Mock<IIsarService> IsarServiceMock;
public readonly Mock<IMissionRunService> MissionServiceMock;
public readonly Mock<RobotController> Mock;
public readonly Mock<IRobotModelService> RobotModelServiceMock;
public readonly Mock<IRobotService> RobotServiceMock;

public RobotControllerMock()
{
Expand Down
10 changes: 0 additions & 10 deletions backend/api/Controllers/RobotController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -422,16 +422,6 @@ [FromRoute] string missionRunId

await _missionRunService.Update(missionRun);

if (robot.CurrentMissionId != null)
{
var orphanedMissionRun = await _missionRunService.ReadById(robot.CurrentMissionId);
if (orphanedMissionRun != null)
{
orphanedMissionRun.SetToFailed();
await _missionRunService.Update(orphanedMissionRun);
}
}

robot.Status = RobotStatus.Busy;
robot.CurrentMissionId = missionRun.Id;
await _robotService.Update(robot);
Expand Down
87 changes: 46 additions & 41 deletions backend/api/Database/Models/Pose.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
using System.ComponentModel.DataAnnotations;
#nullable disable
using System.ComponentModel.DataAnnotations;
using Api.Mqtt.MessageModels;
using Api.Services.Models;
using Microsoft.EntityFrameworkCore;

#nullable disable
namespace Api.Database.Models
{
[Owned]
public class Orientation
{
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
public float W { get; set; }

public Orientation()
{
Expand All @@ -28,11 +24,14 @@ public Orientation(float x = 0, float y = 0, float z = 0, float w = 1)
Z = z;
W = w;
}
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
public float W { get; set; }

public override bool Equals(object obj)
{
if (obj is not Orientation)
return false;
if (obj is not Orientation) { return false; }
const float Tolerance = 1e-6F;
var orientation = (Orientation)obj;
if (MathF.Abs(orientation.X - X) > Tolerance)
Expand Down Expand Up @@ -63,9 +62,6 @@ public override int GetHashCode()
[Owned]
public class Position
{
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }

public Position()
{
Expand All @@ -80,47 +76,27 @@ public Position(float x = 0, float y = 0, float z = 0)
Y = y;
Z = z;
}
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
}

[Owned]
public class Pose
{
[Required]
public Position Position { get; set; }

[Required]
public Orientation Orientation { get; set; }

// Since this is a ground robot the only quaternion vector
// that makes sense is up (0, 0, 1)
// Echo representes North at 0deg and increases this value clockwise
// Our representation has East at 0deg with rotations anti-clockwise
public Orientation AxisAngleToQuaternion(float echoAngle)
{
float angle;
echoAngle %= 2F * MathF.PI;

if (echoAngle < 0) echoAngle += 2F * MathF.PI;

angle = (450 * MathF.PI / 180) - echoAngle;

var quaternion = new Orientation()
{
X = 0,
Y = 0,
Z = MathF.Sin(angle / 2),
W = MathF.Cos(angle / 2)
};

return quaternion;
}

public Pose()
{
Position = new Position();
Orientation = new Orientation();
}

public Pose(IsarPoseMqtt isarPose)
{
Position = new Position(isarPose.Position.X, isarPose.Position.Y, isarPose.Position.Z);
Orientation = new Orientation(isarPose.Orientation.X, isarPose.Orientation.Y, isarPose.Orientation.Z, isarPose.Orientation.W);
}

public Pose(
float x_pos,
float y_pos,
Expand All @@ -146,5 +122,34 @@ public Pose(Position position, Orientation orientation)
Position = position;
Orientation = orientation;
}
[Required]
public Position Position { get; set; }

[Required]
public Orientation Orientation { get; set; }

// Since this is a ground robot the only quaternion vector
// that makes sense is up (0, 0, 1)
// Echo representes North at 0deg and increases this value clockwise
// Our representation has East at 0deg with rotations anti-clockwise
public Orientation AxisAngleToQuaternion(float echoAngle)
{
float angle;
echoAngle %= 2F * MathF.PI;

if (echoAngle < 0) { echoAngle += 2F * MathF.PI; }

angle = (450 * MathF.PI / 180) - echoAngle;

var quaternion = new Orientation
{
X = 0,
Y = 0,
Z = MathF.Sin(angle / 2),
W = MathF.Cos(angle / 2)
};

return quaternion;
}
}
}
6 changes: 5 additions & 1 deletion backend/api/EventHandlers/IsarConnectionEventHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,11 @@ private void AddTimerForRobot(IsarRobotHeartbeatMessage isarRobotHeartbeat, Robo
timer.Start();

if (_isarConnectionTimers.TryAdd(robot.IsarId, timer)) { _logger.LogInformation("Added new timer for ISAR '{IsarId}' ('{RobotName}')", robot.IsarId, robot.Name); }
else { _logger.LogWarning("Failed to add new timer for ISAR '{IsarId}' ('{RobotName})'", robot.IsarId, robot.Name); }
else
{
_logger.LogWarning("Failed to add new timer for ISAR '{IsarId}' ('{RobotName})'", robot.IsarId, robot.Name);
timer.Close();
aeshub marked this conversation as resolved.
Show resolved Hide resolved
}
}

private async void OnTimeoutEvent(IsarRobotHeartbeatMessage robotHeartbeatMessage)
Expand Down
57 changes: 16 additions & 41 deletions backend/api/EventHandlers/MqttEventHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ namespace Api.EventHandlers
/// <summary>
/// A background service which listens to events and performs callback functions.
/// </summary>
///
public interface IMqttEventHandler
{
public void TriggerRobotAvailable(RobotAvailableEventArgs e);
Expand All @@ -36,6 +35,11 @@ public MqttEventHandler(ILogger<MqttEventHandler> logger, IServiceScopeFactory s
Subscribe();
}

public void TriggerRobotAvailable(RobotAvailableEventArgs e)
{
OnRobotAvailable(e);
}

private IServiceProvider GetServiceProvider() { return _scopeFactory.CreateScope().ServiceProvider; }

public override void Subscribe()
Expand Down Expand Up @@ -64,11 +68,6 @@ public override void Unsubscribe()

protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await stoppingToken; }

public void TriggerRobotAvailable(RobotAvailableEventArgs e)
{
OnRobotAvailable(e);
}

protected virtual void OnRobotAvailable(RobotAvailableEventArgs e)
{
RobotAvailable?.Invoke(this, e);
Expand Down Expand Up @@ -238,21 +237,22 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs)
return;
}

if (flotillaMissionRun.IsCompleted) { robot.CurrentMissionId = null; }
if (!flotillaMissionRun.IsCompleted) { return; }

await robotService.Update(robot);
_logger.LogInformation("Robot '{Id}' ('{Name}') - completed mission {MissionId}", robot.IsarId, robot.Name, flotillaMissionRun.MissionId);
robot.CurrentMissionId = null;

if (!flotillaMissionRun.IsCompleted) return;
await taskDurationService.UpdateAverageDurationPerTask(robot.Model.Type);
await robotService.Update(robot);
_logger.LogInformation("Robot '{Id}' ('{Name}') - completed mission run {MissionRunId}", robot.IsarId, robot.Name, flotillaMissionRun.Id);

if (flotillaMissionRun.MissionId == null) return;
if (flotillaMissionRun.MissionId == null) { return; }

var missionDefinition = await missionDefinitionService.ReadById(flotillaMissionRun.MissionId);
if (missionDefinition == null) return;
if (missionDefinition == null) { return; }

missionDefinition.LastRun = flotillaMissionRun;
await missionDefinitionService.Update(missionDefinition);

await taskDurationService.UpdateAverageDurationPerTask(robot.Model.Type);
}

private async void OnTaskUpdate(object? sender, MqttReceivedArgs mqttArgs)
Expand Down Expand Up @@ -369,37 +369,12 @@ await timeseriesService.Create(
private async void OnPoseUpdate(object? sender, MqttReceivedArgs mqttArgs)
{
var provider = GetServiceProvider();
var robotService = provider.GetRequiredService<IRobotService>();
var timeseriesService = provider.GetRequiredService<ITimeseriesService>();
var poseTimeseriesService = provider.GetRequiredService<IPoseTimeseriesService>();

var poseStatus = (IsarPoseMessage)mqttArgs.Message;
var pose = new Pose(poseStatus.Pose);

var robot = await robotService.ReadByIsarId(poseStatus.IsarId);
if (robot == null)
{
_logger.LogWarning(
"Could not find corresponding robot for pose update on robot '{RobotName}' with ISAR id '{IsarId}'", poseStatus.RobotName, poseStatus.IsarId);
}
else
{
try { poseStatus.Pose.CopyIsarPoseToRobotPose(robot.Pose); }
catch (NullReferenceException e)
{
_logger.LogWarning(
"NullReferenceException while updating pose on robot '{RobotName}' with ISAR id '{IsarId}': {Message}", robot.Name, robot.IsarId, e.Message);
}

await robotService.Update(robot);
await timeseriesService.Create(
new RobotPoseTimeseries(robot.Pose)
{
MissionId = robot.CurrentMissionId,
RobotId = robot.Id,
Time = DateTimeOffset.UtcNow
}
);
_logger.LogDebug("Updated pose on robot '{RobotName}' with ISAR id '{IsarId}'", robot.Name, robot.IsarId);
}
await poseTimeseriesService.AddPoseEntry(pose, poseStatus.IsarId);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Api.Mqtt.MessageModels
public class IsarPoseMessage : MqttMessage
{
[JsonPropertyName("pose")]
public IsarPose Pose { get; set; }
public IsarPoseMqtt Pose { get; set; }

[JsonPropertyName("robot_name")]
public string RobotName { get; set; }
Expand All @@ -20,7 +20,7 @@ public class IsarPoseMessage : MqttMessage
public DateTime Timestamp { get; set; }
}

public class IsarPose
public class IsarPoseMqtt
{
[JsonPropertyName("position")]
public IsarPosition Position { get; set; }
Expand Down
1 change: 1 addition & 0 deletions backend/api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
builder.Services.AddScoped<IMissionSchedulingService, MissionSchedulingService>();
builder.Services.AddScoped<ICustomMissionSchedulingService, CustomMissionSchedulingService>();
builder.Services.AddScoped<ITaskDurationService, TaskDurationService>();
builder.Services.AddScoped<IPoseTimeseriesService, PoseTimeseriesService>();


bool useInMemoryDatabase = builder.Configuration
Expand Down
47 changes: 47 additions & 0 deletions backend/api/Services/ActionServices/PoseTimeseriesService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Api.Database.Models;
namespace Api.Services.ActionServices
{
public interface IPoseTimeseriesService
{
public Task AddPoseEntry(Pose pose, string isarId);
}

public class PoseTimeseriesService : IPoseTimeseriesService
{
private readonly ILogger<PoseTimeseriesService> _logger;
private readonly IRobotService _robotService;
private readonly ITimeseriesService _timeseriesService;

public PoseTimeseriesService(ILogger<PoseTimeseriesService> logger, IRobotService robotService, ITimeseriesService timeseriesService)
{
_logger = logger;
_robotService = robotService;
_timeseriesService = timeseriesService;
}

public async Task AddPoseEntry(Pose pose, string isarId)
{
var robot = await _robotService.ReadByIsarId(isarId);
if (robot == null)
{
_logger.LogWarning(
"Could not find corresponding robot for pose update on robot with ISAR id '{IsarId}'", isarId);
return;
}

robot.Pose = pose;

await _robotService.Update(robot);
await _timeseriesService.Create(
new RobotPoseTimeseries(robot.Pose)
{
MissionId = robot.CurrentMissionId,
RobotId = robot.Id,
Time = DateTimeOffset.UtcNow
}
);
_logger.LogDebug("Updated pose on robot '{RobotName}' with ISAR id '{IsarId}'", robot.Name, robot.IsarId);

}
}
}
2 changes: 2 additions & 0 deletions backend/api/Services/MissionDefinitionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ public async Task<List<MissionDefinition>> ReadByDeckId(string deckId)

public async Task<MissionDefinition> Update(MissionDefinition missionDefinition)
{
if (missionDefinition.LastRun is not null) { _context.Entry(missionDefinition.LastRun.Robot).State = EntityState.Unchanged; }

var entry = _context.Update(missionDefinition);
await _context.SaveChangesAsync();
return entry.Entity;
Expand Down
5 changes: 1 addition & 4 deletions backend/api/Services/RobotService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,7 @@ public async Task<Robot> Update(Robot robot)
public async Task<Robot?> Delete(string id)
{
var robot = await GetRobotsWithSubModels().FirstOrDefaultAsync(ev => ev.Id.Equals(id));
if (robot is null)
{
return null;
}
if (robot is null) { return null; }

_context.Robots.Remove(robot);
await _context.SaveChangesAsync();
Expand Down
Loading