Files
I150/srcs/RobotNet10/FleetManager/RobotNet10.FleetManager/Services/TrafficControl/Services/RobotInfoService.cs
2026-07-03 16:37:12 +07:00

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();
}
}
}