221 lines
8.7 KiB
C#
221 lines
8.7 KiB
C#
using RobotNet10.FleetManager.Events;
|
|
using RobotNet10.FleetManager.Events.Events;
|
|
using RobotNet10.FleetManager.Services.RobotManager;
|
|
using RobotNet10.FleetManager.Services.TrafficControl.Models;
|
|
|
|
namespace RobotNet10.FleetManager.Services.TrafficControl.Services;
|
|
|
|
/// <summary>
|
|
/// Service for managing robot information cache
|
|
/// Uses event-based cache invalidation for accuracy and performance
|
|
/// </summary>
|
|
public class RobotInfoService : IRobotInfoService
|
|
{
|
|
private readonly Logger<RobotInfoService> _logger;
|
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
|
private readonly IRobotEventBus? _eventBus;
|
|
|
|
// Robot static information cache (from RobotModel)
|
|
// Only cache static info: Length, Width, NavigationPoint
|
|
// Dynamic info (CurrentX, CurrentY, etc.) is always fetched fresh
|
|
// Cache is invalidated via events when RobotModel is updated
|
|
private readonly Dictionary<string, (double Length, double Width, double NavigationPointX, double NavigationPointY)> _staticInfoCache = [];
|
|
private readonly Lock _staticInfoCacheLock = new();
|
|
|
|
public RobotInfoService(
|
|
Logger<RobotInfoService> logger,
|
|
IServiceScopeFactory serviceScopeFactory,
|
|
IRobotEventBus? eventBus = null)
|
|
{
|
|
_logger = logger;
|
|
_serviceScopeFactory = serviceScopeFactory;
|
|
_eventBus = eventBus;
|
|
|
|
// Subscribe to events for automatic cache invalidation
|
|
if (_eventBus != null)
|
|
{
|
|
_eventBus.RobotModelUpdated += OnRobotModelUpdated;
|
|
_eventBus.RobotModelIdChanged += OnRobotModelIdChanged;
|
|
}
|
|
else
|
|
{
|
|
_logger.Warning("IRobotEventBus not available - cache invalidation via events disabled");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event handler: RobotModel updated - invalidate cache for all affected robots
|
|
/// </summary>
|
|
private void OnRobotModelUpdated(object? sender, RobotModelUpdatedEvent e)
|
|
{
|
|
try
|
|
{
|
|
if (e.AffectedRobotIds == null || e.AffectedRobotIds.Count == 0)
|
|
{
|
|
// If no specific robot IDs, clear all cache (safe but less efficient)
|
|
_logger.Warning($"RobotModelUpdated event for ModelId {e.ModelId} has no AffectedRobotIds - clearing all cache");
|
|
ClearAllRobotInfoCache();
|
|
}
|
|
else
|
|
{
|
|
// Clear cache only for affected robots
|
|
lock (_staticInfoCacheLock)
|
|
{
|
|
foreach (var robotId in e.AffectedRobotIds)
|
|
{
|
|
if (_staticInfoCache.Remove(robotId))
|
|
{
|
|
_logger.Debug($"Invalidated cache for robot {robotId} due to RobotModel {e.ModelId} update");
|
|
}
|
|
}
|
|
}
|
|
_logger.Info($"Invalidated cache for {e.AffectedRobotIds.Count} robot(s) due to RobotModel {e.ModelId} update");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error($"Error handling RobotModelUpdated event: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event handler: Robot's ModelId changed - invalidate cache for that robot
|
|
/// </summary>
|
|
private void OnRobotModelIdChanged(object? sender, RobotModelIdChangedEvent e)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrWhiteSpace(e.RobotId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (_staticInfoCacheLock)
|
|
{
|
|
if (_staticInfoCache.Remove(e.RobotId))
|
|
{
|
|
_logger.Info($"Invalidated cache for robot {e.RobotId} due to ModelId change (from {e.PreviousModelId} to {e.NewModelId})");
|
|
}
|
|
else
|
|
{
|
|
_logger.Debug($"Cache for robot {e.RobotId} was not found (may not have been cached yet)");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error($"Error handling RobotModelIdChanged event: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public async Task<RobotInfo?> GetRobotInfoAsync(string robotId, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
// Step 1: Get static info (from cache or database)
|
|
// Cache is invalidated via events, so if it exists, it's valid
|
|
double length = 0, width = 0, navPointX = 0, navPointY = 0;
|
|
bool needToFetch = true;
|
|
|
|
lock (_staticInfoCacheLock)
|
|
{
|
|
if (_staticInfoCache.TryGetValue(robotId, out var cachedStaticInfo))
|
|
{
|
|
// Cache exists and is valid (invalidated via events)
|
|
length = cachedStaticInfo.Length;
|
|
width = cachedStaticInfo.Width;
|
|
navPointX = cachedStaticInfo.NavigationPointX;
|
|
navPointY = cachedStaticInfo.NavigationPointY;
|
|
needToFetch = false;
|
|
_logger.Debug($"Using cached static info for robot {robotId}");
|
|
}
|
|
}
|
|
|
|
// If cache miss, fetch from database
|
|
if (needToFetch)
|
|
{
|
|
using var scope = _serviceScopeFactory.CreateScope();
|
|
var robotService = scope.ServiceProvider.GetRequiredService<IRobotService>();
|
|
var robotModelService = scope.ServiceProvider.GetRequiredService<IRobotModelService>();
|
|
|
|
// Get robot from database
|
|
var robot = await robotService.GetByRobotIdAsync(robotId);
|
|
if (robot == null)
|
|
{
|
|
_logger.Warning($"Robot {robotId} not found in database");
|
|
return null;
|
|
}
|
|
|
|
// Get robot model
|
|
var robotModel = await robotModelService.GetByIdAsync(robot.ModelId);
|
|
if (robotModel == null)
|
|
{
|
|
_logger.Warning($"RobotModel {robot.ModelId} not found for robot {robotId}");
|
|
return null;
|
|
}
|
|
|
|
// Extract static info
|
|
length = robotModel.Length;
|
|
width = robotModel.Width;
|
|
navPointX = robotModel.NavigationPointX;
|
|
navPointY = robotModel.NavigationPointY;
|
|
|
|
// Cache static info (no timestamp needed - invalidated via events)
|
|
lock (_staticInfoCacheLock)
|
|
{
|
|
_staticInfoCache[robotId] = (length, width, navPointX, navPointY);
|
|
}
|
|
|
|
_logger.Debug($"Fetched and cached fresh static info for robot {robotId}");
|
|
}
|
|
|
|
// Step 2: Get dynamic info (ALWAYS fresh from RobotManager - never cached)
|
|
// Resolve IRobotManagerService lazily to avoid circular dependency
|
|
using var scopeForRobotManager = _serviceScopeFactory.CreateScope();
|
|
var robotManager = scopeForRobotManager.ServiceProvider.GetRequiredService<IRobotManagerService>();
|
|
var robotController = robotManager.GetRobotController(robotId);
|
|
var currentState = robotController?.Data?.State;
|
|
|
|
// Create RobotInfo with static info (cached) + dynamic info (fresh)
|
|
var robotInfo = new RobotInfo
|
|
{
|
|
RobotId = robotId,
|
|
Length = length,
|
|
Width = width,
|
|
NavigationPointX = navPointX,
|
|
NavigationPointY = navPointY,
|
|
// Dynamic info - always fresh, never cached
|
|
CurrentX = currentState?.AgvPosition?.X ?? 0.0,
|
|
CurrentY = currentState?.AgvPosition?.Y ?? 0.0,
|
|
CurrentTheta = currentState?.AgvPosition?.Theta ?? 0.0,
|
|
LastNodeId = currentState?.LastNodeId ?? string.Empty
|
|
};
|
|
|
|
_logger.Debug($"Retrieved robot info for {robotId}: Length={robotInfo.Length}, Width={robotInfo.Width}, NavPoint=({robotInfo.NavigationPointX}, {robotInfo.NavigationPointY}), Position=({robotInfo.CurrentX}, {robotInfo.CurrentY})");
|
|
|
|
return robotInfo;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error($"Error getting robot info for robot {robotId}: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public void ClearRobotInfoCache(string robotId)
|
|
{
|
|
lock (_staticInfoCacheLock)
|
|
{
|
|
_staticInfoCache.Remove(robotId);
|
|
}
|
|
}
|
|
|
|
public void ClearAllRobotInfoCache()
|
|
{
|
|
lock (_staticInfoCacheLock)
|
|
{
|
|
_staticInfoCache.Clear();
|
|
}
|
|
}
|
|
}
|