using System.Collections.Concurrent;
using Fleet.Core;
using MoShiro.Backend.Analysis;
using MoShiro.Protocol;
namespace MoShiro.Backend.Application;
///
/// 1 秒精度的内存热层:最近 1 小时的累加器。桶以 编码后的字节保存,
/// 15 条线路约十几 MB;它同时是 SSE 实时推送的数据来源。重启后为空,由后台任务从原始块回填。
///
public sealed class HotLayer
{
public const long BucketMs = 1000;
public static readonly TimeSpan Retention = TimeSpan.FromHours(1);
private readonly ConcurrentDictionary<(NodeId Agent, string Series), Ring> _rings = new();
private readonly TimeProvider _time;
private volatile bool _ready;
public HotLayer(TimeProvider time)
{
_time = time;
}
/// 回填完成之前热层不完整,查询规划器不会选用它。
public bool Ready => _ready;
public void MarkReady() => _ready = true;
/// 评估一个块并合并进热层。同一个块重复合并(例如回填与实时竞争)会被忽略。
public void Merge(NodeId agent, string series, RawChunk chunk)
{
Ring ring = _rings.GetOrAdd((agent, series), _ => new Ring());
var buckets = new SortedDictionary();
WindowEvaluator.Default.Evaluate(chunk, BucketMs, buckets);
long cutoff = _time.GetUtcNow().ToUnixTimeMilliseconds() - (long)Retention.TotalMilliseconds;
lock (ring)
{
if (!ring.Chunks.Add(chunk.Header.ChunkId))
{
return;
}
foreach ((long start, MetricSet set) in buckets)
{
if (start < cutoff)
{
continue;
}
if (ring.Buckets.TryGetValue(start, out byte[]? existing) && AccumulatorCodec.Decode(existing).TryGet(out MetricSet? merged, out _))
{
merged.Merge(set);
ring.Buckets[start] = AccumulatorCodec.Encode(merged);
}
else
{
ring.Buckets[start] = AccumulatorCodec.Encode(set);
}
}
Prune(ring, cutoff);
}
}
/// [from, to) 内的 1 秒桶(解码后的副本,按时间升序)。
public IReadOnlyList> GetRange(NodeId agent, string series, long fromMs, long toMs)
{
if (!_rings.TryGetValue((agent, series), out Ring? ring))
{
return [];
}
var result = new List>();
lock (ring)
{
foreach ((long start, byte[] bytes) in ring.Buckets)
{
if (start >= toMs)
{
break;
}
if (start >= fromMs && AccumulatorCodec.Decode(bytes).TryGet(out MetricSet? set, out _))
{
result.Add(new KeyValuePair(start, set));
}
}
}
return result;
}
private static void Prune(Ring ring, long cutoff)
{
while (ring.Buckets.Count > 0)
{
long first = ring.Buckets.Keys.First();
if (first >= cutoff)
{
break;
}
ring.Buckets.Remove(first);
}
// 块 ID 集合只需要覆盖热层的时间窗,限制大小即可
if (ring.Chunks.Count > 2000)
{
ring.Chunks.Clear();
}
}
private sealed class Ring
{
public SortedDictionary Buckets { get; } = [];
public HashSet Chunks { get; } = [];
}
}