130 lines
3.7 KiB
C#
130 lines
3.7 KiB
C#
|
|
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");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|