FatigueTestConfigService.cs 8.23 KB
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Rcs.Application.Services;
using StackExchange.Redis;

namespace Rcs.Infrastructure.Services;

/// <summary>
/// Fatigue test config service implementation based on Redis.
/// </summary>
public class FatigueTestConfigService : IFatigueTestConfigService
{
    private readonly IConnectionMultiplexer _redis;
    private readonly ILogger<FatigueTestConfigService> _logger;
    private readonly SemaphoreSlim _configLock = new(1, 1);

    private const string ConfigsKey = "hahrcs:fatigue_test:configs";

    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };

    public FatigueTestConfigService(
        IConnectionMultiplexer redis,
        ILogger<FatigueTestConfigService> logger)
    {
        _redis = redis;
        _logger = logger;
    }

    public async Task<List<FatigueTestConfig>> GetConfigsAsync(CancellationToken cancellationToken = default)
    {
        try
        {
            var db = _redis.GetDatabase();
            return await GetConfigsInternalAsync(db);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[FatigueTestConfig] Failed to get config list");
            return new List<FatigueTestConfig>();
        }
    }

    public async Task<FatigueTestConfig?> GetConfigByIdAsync(string configId, CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(configId))
        {
            return null;
        }

        var configs = await GetConfigsAsync(cancellationToken);
        return configs.FirstOrDefault(c => string.Equals(c.ConfigId, configId, StringComparison.OrdinalIgnoreCase));
    }

    public async Task<FatigueTestConfig?> UpsertConfigAsync(FatigueTestConfig config, CancellationToken cancellationToken = default)
    {
        var lockAcquired = false;
        try
        {
            await _configLock.WaitAsync(cancellationToken);
            lockAcquired = true;

            var db = _redis.GetDatabase();
            var configs = await GetConfigsInternalAsync(db);

            var now = DateTime.Now;
            var normalizedConfigId = NormalizeOrCreateConfigId(config.ConfigId);
            var existingIndex = configs.FindIndex(c => string.Equals(c.ConfigId, normalizedConfigId, StringComparison.OrdinalIgnoreCase));

            if (existingIndex >= 0)
            {
                var existing = configs[existingIndex];
                existing.MapId = config.MapId;
                existing.RobotIds = config.RobotIds;
                existing.LocationIds = config.LocationIds;
                existing.TaskIntervalMs = config.TaskIntervalMs;
                existing.UpdatedAt = now;
                configs[existingIndex] = existing;
            }
            else
            {
                config.ConfigId = normalizedConfigId;
                config.CreatedAt = now;
                config.UpdatedAt = now;
                config.IsRunning = false;
                configs.Add(config);
            }

            var saved = await SaveConfigsInternalAsync(db, configs);
            if (!saved)
            {
                return null;
            }

            return configs.FirstOrDefault(c => string.Equals(c.ConfigId, normalizedConfigId, StringComparison.OrdinalIgnoreCase));
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[FatigueTestConfig] Failed to upsert config");
            return null;
        }
        finally
        {
            if (lockAcquired)
            {
                _configLock.Release();
            }
        }
    }

    public async Task<bool> DeleteConfigAsync(string configId, CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(configId))
        {
            return false;
        }

        var lockAcquired = false;
        try
        {
            await _configLock.WaitAsync(cancellationToken);
            lockAcquired = true;

            var db = _redis.GetDatabase();
            var configs = await GetConfigsInternalAsync(db);
            var target = configs.FirstOrDefault(c => string.Equals(c.ConfigId, configId, StringComparison.OrdinalIgnoreCase));
            if (target == null)
            {
                return false;
            }

            target.IsRunning = false;
            target.UpdatedAt = DateTime.Now;

            configs = configs.Where(c => !string.Equals(c.ConfigId, configId, StringComparison.OrdinalIgnoreCase)).ToList();
            var result = await SaveConfigsInternalAsync(db, configs);

            if (result)
            {
                _logger.LogInformation("[FatigueTestConfig] Deleted config {ConfigId}", configId);
            }

            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[FatigueTestConfig] Failed to delete config {ConfigId}", configId);
            return false;
        }
        finally
        {
            if (lockAcquired)
            {
                _configLock.Release();
            }
        }
    }

    public async Task<bool> StartAsync(string configId, CancellationToken cancellationToken = default)
    {
        return await SetRunningAsync(configId, true, cancellationToken);
    }

    public async Task<bool> StopAsync(string configId, CancellationToken cancellationToken = default)
    {
        return await SetRunningAsync(configId, false, cancellationToken);
    }

    public async Task<List<FatigueTestStatus>> GetStatusesAsync(CancellationToken cancellationToken = default)
    {
        var configs = await GetConfigsAsync(cancellationToken);
        return configs.Select(c => new FatigueTestStatus
        {
            ConfigId = c.ConfigId,
            MapId = c.MapId,
            IsRunning = c.IsRunning,
            RobotCount = c.RobotIds.Count,
            LocationCount = c.LocationIds.Count,
            TaskIntervalMs = c.TaskIntervalMs
        }).ToList();
    }

    private async Task<bool> SetRunningAsync(string configId, bool isRunning, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(configId))
        {
            return false;
        }

        var lockAcquired = false;
        try
        {
            await _configLock.WaitAsync(cancellationToken);
            lockAcquired = true;

            var db = _redis.GetDatabase();
            var configs = await GetConfigsInternalAsync(db);
            var target = configs.FirstOrDefault(c => string.Equals(c.ConfigId, configId, StringComparison.OrdinalIgnoreCase));
            if (target == null)
            {
                _logger.LogWarning("[FatigueTestConfig] Config {ConfigId} not found", configId);
                return false;
            }

            target.IsRunning = isRunning;
            target.UpdatedAt = DateTime.Now;
            var result = await SaveConfigsInternalAsync(db, configs);

            if (result)
            {
                _logger.LogInformation("[FatigueTestConfig] Config {ConfigId} running status: {IsRunning}", configId, isRunning);
            }

            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[FatigueTestConfig] Failed to set running status for config {ConfigId}", configId);
            return false;
        }
        finally
        {
            if (lockAcquired)
            {
                _configLock.Release();
            }
        }
    }

    private async Task<List<FatigueTestConfig>> GetConfigsInternalAsync(IDatabase db)
    {
        var json = await db.StringGetAsync(ConfigsKey);
        if (json.IsNullOrEmpty)
        {
            return new List<FatigueTestConfig>();
        }

        var configs = JsonSerializer.Deserialize<List<FatigueTestConfig>>(json!, JsonOptions);
        return configs ?? new List<FatigueTestConfig>();
    }

    private async Task<bool> SaveConfigsInternalAsync(IDatabase db, List<FatigueTestConfig> configs)
    {
        var json = JsonSerializer.Serialize(configs, JsonOptions);
        return await db.StringSetAsync(ConfigsKey, json);
    }

    private static string NormalizeOrCreateConfigId(string? configId)
    {
        if (Guid.TryParse(configId, out var parsed))
        {
            return parsed.ToString("D");
        }

        return Guid.NewGuid().ToString("D");
    }
}