using System.Security.Cryptography;
namespace Fleet.Core;
public enum NodeLogLevel
{
Info,
Warn,
Error,
}
/// 节点日志:字段名统一为 logfmt(ts level msg agent dialer session task result err)。逐包事件不记录,只累加计数。
public interface INodeLog
{
void Write(NodeLogLevel level, string message, params (string Key, object? Value)[] fields);
}
/// One structured node log line queued for delivery to the control plane.
public sealed record NodeLogRecord(long AtUtcMs, string Level, string Message, Dictionary Fields);
public sealed record NodeLogBatch(IReadOnlyList Records, long Dropped);
/// Optional bounded log source used by node heartbeats to upload recent local logs.
public interface INodeLogBatchSource
{
NodeLogBatch TakeBatch(int maxRecords);
}
public sealed class NullNodeLog : INodeLog
{
public static NullNodeLog Instance { get; } = new();
public void Write(NodeLogLevel level, string message, params (string Key, object? Value)[] fields)
{
}
}
public static class NodeLogExtensions
{
public static void Info(this INodeLog log, string message, params (string Key, object? Value)[] fields) => log.Write(NodeLogLevel.Info, message, fields);
public static void Warn(this INodeLog log, string message, params (string Key, object? Value)[] fields) => log.Write(NodeLogLevel.Warn, message, fields);
public static void Error(this INodeLog log, string message, params (string Key, object? Value)[] fields) => log.Write(NodeLogLevel.Error, message, fields);
}
/// 指数退避(带随机抖动):1 s → 20 s(对端恢复之后最多等 20 秒;节点数量不大,重连风暴不是问题)。连接成功之后调用 。
public sealed class Backoff
{
private readonly TimeSpan _initial;
private readonly TimeSpan _max;
private int _attempt;
public Backoff(TimeSpan? initial = null, TimeSpan? max = null)
{
_initial = initial ?? TimeSpan.FromSeconds(1);
_max = max ?? TimeSpan.FromSeconds(20);
}
public TimeSpan Next()
{
double baseMs = Math.Min(_max.TotalMilliseconds, _initial.TotalMilliseconds * Math.Pow(2, Math.Min(_attempt, 20)));
_attempt++;
// 抖动:0.5 ~ 1.0 倍,避免所有节点同时重连
double jitter = 0.5 + (RandomNumberGenerator.GetInt32(0, 1000) / 2000.0);
return TimeSpan.FromMilliseconds(baseMs * jitter);
}
public void Reset() => _attempt = 0;
}
///
/// 序号重放窗口(滑动 1024 位):同一个序号只接受一次;比窗口还旧的序号视为重放。
/// 用于数据面的 UDP 包,每个 (session, stream) 一个。
///
public sealed class ReplayWindow
{
private const int Size = 1024;
private readonly ulong[] _bits = new ulong[Size / 64];
private long _highest = -1;
public bool TryAccept(uint seq)
{
if (_highest < 0)
{
_highest = seq;
Set(seq);
return true;
}
if (seq > _highest)
{
long shift = seq - _highest;
if (shift >= Size)
{
Array.Clear(_bits);
}
else
{
for (long s = _highest + 1; s <= seq; s++)
{
Clear((uint)s);
}
}
_highest = seq;
Set(seq);
return true;
}
if (_highest - seq >= Size)
{
return false; // 太旧
}
if (IsSet(seq))
{
return false; // 重复
}
Set(seq);
return true;
}
private void Set(uint seq) => _bits[(int)(seq % Size) / 64] |= 1UL << (int)(seq % 64);
private void Clear(uint seq) => _bits[(int)(seq % Size) / 64] &= ~(1UL << (int)(seq % 64));
private bool IsSet(uint seq) => (_bits[(int)(seq % Size) / 64] & (1UL << (int)(seq % 64))) != 0;
}