130 lines
3.7 KiB
C#
Raw Permalink Normal View History

2026-06-02 10:30:05 +08:00
using Microsoft.Extensions.Options;
using System.Collections.Concurrent;
using System.Text.Json;
using YD_TrainingCompetition_Api.Models;
using YD_TrainingCompetition_Api.Options;
namespace YD_TrainingCompetition_Api.Services;
public sealed class DeviceStateStore : IDeviceStateStore
{
private readonly ConcurrentDictionary<string, DeviceTelemetry> _states = new(StringComparer.OrdinalIgnoreCase);
private readonly ScoringOptions _options;
private readonly ILogger<DeviceStateStore> _logger;
public DeviceStateStore(IOptions<ScoringOptions> options, ILogger<DeviceStateStore> logger)
{
_options = options.Value;
_logger = logger;
LoadSnapshot();
}
public bool TryUpsert(DeviceTelemetry telemetry, out string? reason)
{
reason = null;
if (string.IsNullOrWhiteSpace(telemetry.DeviceId))
{
reason = "deviceId is required";
return false;
}
if (!_states.ContainsKey(telemetry.DeviceId) && _states.Count >= _options.MaxDevices)
{
reason = $"max device count({_options.MaxDevices}) reached";
return false;
}
telemetry.Timestamp = telemetry.Timestamp == default ? DateTimeOffset.UtcNow : telemetry.Timestamp;
_states[telemetry.DeviceId] = telemetry;
PersistSnapshot();
return true;
}
public IReadOnlyCollection<DeviceSnapshot> GetAll()
{
var now = DateTimeOffset.UtcNow;
return _states.Values
.OrderBy(x => x.DeviceId, StringComparer.OrdinalIgnoreCase)
.Select(x => new DeviceSnapshot
{
Telemetry = x,
LastSeen = x.Timestamp,
Online = now - x.Timestamp <= TimeSpan.FromSeconds(_options.OfflineThresholdSeconds)
})
.ToArray();
}
public DeviceSnapshot? GetByDeviceId(string deviceId)
{
if (!_states.TryGetValue(deviceId, out var telemetry))
{
return null;
}
var now = DateTimeOffset.UtcNow;
return new DeviceSnapshot
{
Telemetry = telemetry,
LastSeen = telemetry.Timestamp,
Online = now - telemetry.Timestamp <= TimeSpan.FromSeconds(_options.OfflineThresholdSeconds)
};
}
private void LoadSnapshot()
{
try
{
if (!File.Exists(_options.SnapshotPath))
{
return;
}
var json = File.ReadAllText(_options.SnapshotPath);
var data = JsonSerializer.Deserialize<List<DeviceTelemetry>>(json);
if (data is null)
{
return;
}
foreach (var telemetry in data)
{
if (!string.IsNullOrWhiteSpace(telemetry.DeviceId))
{
_states[telemetry.DeviceId] = telemetry;
}
}
_logger.LogInformation("Loaded {Count} device snapshots from disk", _states.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load device snapshot");
}
}
private void PersistSnapshot()
{
try
{
var directory = Path.GetDirectoryName(_options.SnapshotPath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
var json = JsonSerializer.Serialize(_states.Values, new JsonSerializerOptions
{
WriteIndented = false
});
File.WriteAllText(_options.SnapshotPath, json);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to persist device snapshot");
}
}
}