using Fleet.Core; using MoShiro.Protocol; namespace MoShiro.Backend.Application; /// /// moshiro.host:Agent 每隔几秒上报的主机资源占用快照。最新值放在内存里并实时推给订阅者;每个节点每分钟存一个样本做历史。 /// 快照里没有 CPU 型号、主机名、网卡名和 IP(节点根本不采集它们)。 /// public sealed class HostStatusService { private static readonly TimeSpan s_stale = TimeSpan.FromSeconds(15); private readonly AgentRuntime _runtime; private readonly IHostStore _store; private readonly LiveHub _live; private readonly TimeProvider _time; private readonly Dictionary _lastSavedMinute = []; private readonly object _gate = new(); public HostStatusService(AgentRuntime runtime, IHostStore store, LiveHub live, TimeProvider time) { _runtime = runtime; _store = store; _live = live; _time = time; } public async ValueTask OnHostAsync(NodeId agent, HostSnapshot snapshot, CancellationToken ct) { long now = _time.GetUtcNow().ToUnixTimeMilliseconds(); snapshot.AtUtcMs = now; // 以 Backend 收到的时间为准:节点的时钟不可信 AgentRuntimeState state = _runtime.Get(agent); state.Host = snapshot; state.HostReceivedAtMs = now; _live.Publish(new HostSignal(agent, snapshot)); long minute = now - (now % 60_000); bool save; lock (_gate) { save = !_lastSavedMinute.TryGetValue(agent, out long last) || last != minute; if (save) { _lastSavedMinute[agent] = minute; } } if (save) { await _store.SaveAsync(agent, minute, snapshot, ct); } } /// 最新快照;超过 15 秒没有更新就是过期(Agent 离线或者没有开启上报)。 public (HostSnapshot? Host, bool Stale) Latest(NodeId agent) { if (!_runtime.TryGet(agent, out AgentRuntimeState? state) || state.Host is null) { return (null, true); } return (state.Host, _time.GetUtcNow().ToUnixTimeMilliseconds() - state.HostReceivedAtMs > s_stale.TotalMilliseconds); } public ValueTask> HistoryAsync(NodeId agent, long fromMs, long toMs, CancellationToken ct) => _store.ReadAsync(agent, fromMs, toMs, ct); }