Files
MoShiro/backend/shared/MoShiro.Measurement/Core/Contracts.cs
T
justme 83e5120377 refactor!: rename the project from Monitor to MoShiro
Namespaces, assemblies, projects and directories, the solution, binaries,
images, systemd units, config paths, environment variables, protocol method
names, the media type and the raw chunk magic all move to MoShiro/moshiro.
Existing data and deployments are not compatible and must be recreated.
Also fix the architecture tests after the solution layout change and make
the update tests independent of their order.
2026-10-07 01:49:04 +08:00

249 lines
9.4 KiB
C#

using System.Net;
using System.Text.Json;
using Fleet.Core;
using MoShiro.Protocol;
namespace MoShiro.Measurement;
/// <summary>
/// 数据面会话:<c>session_id</c> 和密钥(由点对点握手里的 ECDH 派生)。两端的数据面包都用它认证。
/// Agent 的会话还带重放窗口等状态,所以是可以继承的。
/// </summary>
public class DataSession
{
public DataSession(long sessionId, byte[] key)
{
SessionId = sessionId;
Key = key;
}
public long SessionId { get; }
public byte[] Key { get; }
/// <summary>对端最近一次通过认证的 UDP 源地址(监听方把单向数据发往这里;拨号方不需要)。</summary>
public virtual IPEndPoint? UdpEndpoint => null;
/// <summary>TCP 首帧的随机数只能用一次。拨号方不接受连接,所以默认放行。</summary>
public virtual bool UseNonce(ReadOnlySpan<byte> nonce) => true;
}
public interface IDataSessions
{
DataSession? Find(long sessionId);
}
/// <summary>只有一个会话(拨号方:Probe 与某个 Agent 的会话)。</summary>
public sealed class SingleSession : IDataSessions
{
private readonly DataSession _session;
public SingleSession(DataSession session)
{
_session = session;
}
public DataSession? Find(long sessionId) => sessionId == _session.SessionId ? _session : null;
}
public enum Admission
{
Accepted,
Invalid,
RateLimited,
}
/// <summary>
/// 监听方的准入策略(安全边界):重放窗口、令牌桶限速、源地址学习。积木本身不含任何策略;
/// 拨号方不需要,只做 MAC 校验。任何一步失败都是静默丢弃并计数,不回复。
/// </summary>
public interface IAdmission
{
/// <summary>UDP 包:<paramref name="source"/> 是认证通过的源地址(实现可以据此学习 NAT 重绑定)。</summary>
Admission AdmitPacket(DataSession session, in UdpHeader header, SocketAddress source, long nowUs);
/// <summary>TCP 回显的一条消息(只限速)。</summary>
bool AdmitMessage(DataSession session, uint streamId, long nowUs);
}
/// <summary>数据面计数(只有计数,没有主机信息):用于心跳和面板上的节点状态。</summary>
public sealed class DataPlaneCounters
{
private long _packetsIn;
private long _packetsOut;
private long _invalid;
private long _rateLimited;
public long PacketsIn => Interlocked.Read(ref _packetsIn);
public long PacketsOut => Interlocked.Read(ref _packetsOut);
public long DroppedInvalid => Interlocked.Read(ref _invalid);
public long DroppedRateLimited => Interlocked.Read(ref _rateLimited);
public void PacketIn() => Interlocked.Increment(ref _packetsIn);
public void PacketOut() => Interlocked.Increment(ref _packetsOut);
public void DropInvalid() => Interlocked.Increment(ref _invalid);
public void DropRateLimited() => Interlocked.Increment(ref _rateLimited);
}
/// <summary>一个列块属于线路任务还是按需测试的某个阶段。</summary>
public sealed record FragmentScope(TaskId? Task, TestId? Test, int? Phase);
/// <summary>
/// 积木交出一侧的行的地方。Probe:放进发给 Agent 的发件箱(列块);Agent 自己运行的积木(目标任务):直接盖章上传。
/// </summary>
public interface IFragmentSink
{
void Ship(string kind, FragmentScope scope, IReadOnlyList<RawStream> streams, IReadOnlyDictionary<string, JsonElement>? parameters, long startUtcMs, int durationMs, int port);
}
public enum FlushMode
{
/// <summary>只交出已经稳定的行(回显类等过宽限期的)。</summary>
Settled,
/// <summary>交出全部(阶段结束或角色停止)。</summary>
All,
}
/// <summary>积木无法继续(例如监听方等不到对端的地址):由编排者决定整个测试怎么结束。</summary>
public sealed class BlockFaultException : Exception
{
public BlockFaultException(string message)
: base(message)
{
}
}
/// <summary>
/// 一个运行中的积木角色。同一种采集方法的两端是一对角色(拨号方 = Probe,监听方 = Agent),写在同一个文件里,
/// 共用包的构造、校验、行的记录;它们只产生<b>原始列</b>,不做任何统计,也不知道上层的方案或协议。
/// </summary>
public interface IBlock
{
/// <summary>运行到取消(回显、保持)或到自己结束(单向数据流的发送端)。</summary>
Task RunAsync(CancellationToken ct);
/// <summary>
/// 拨号方把已完成的行切成列块交给 <see cref="IFragmentSink"/>,返回块数;监听方返回 0(它的行留在 <see cref="Records"/> 里等合并)。
/// <paramref name="peerProduced"/>:对端是行的主导方时,对端产生的行数(单向数据流的发送包数)。
/// </summary>
int Flush(FlushMode mode, long peerProduced = 0);
/// <summary>这一侧作为主导方产生的行数(发出的包数)。</summary>
long Produced { get; }
/// <summary>这一侧为合并保留的按键记录;没有则为 null。</summary>
RowStore? Records(string streamName);
}
/// <summary>运行一个积木所需的全部参数:Kind、已经校验并补全默认值的参数、流编号、端口、列块归属。</summary>
public sealed class BlockSpec
{
public BlockSpec(KindSpec kind, ParamValues parameters, uint streamId, int port, FragmentScope scope)
{
Kind = kind;
Params = parameters;
StreamId = streamId;
Port = port;
Scope = scope;
}
public KindSpec Kind { get; }
public ParamValues Params { get; }
public uint StreamId { get; }
public int Port { get; }
public FragmentScope Scope { get; }
public string KindId => Kind.Id;
public long Int(string name) => Params.GetInt(name);
public bool Bool(string name) => Params.GetBool(name);
/// <summary>参数或端口变化时调和器重启这个积木(不含流编号:Agent 的流编号每次重建都会重新分配)。</summary>
public string ConfigSignature => KindId + "|" + Port + "|" +
string.Join(',', Params.Values.OrderBy(p => p.Key, StringComparer.Ordinal).Select(p => p.Key + "=" + p.Value.GetRawText()));
/// <summary>参数、端口或流编号变化时重启:Probe 据此判断 Agent 下发的任务有没有变(Agent 重启后流编号会变)。</summary>
public string Signature => StreamId + "|" + ConfigSignature;
/// <summary>校验参数(补全默认值)后构造;Kind 不存在或参数非法返回 null。</summary>
public static BlockSpec? Create(string kindId, IReadOnlyDictionary<string, JsonElement> parameters, uint streamId, int port, FragmentScope scope)
{
KindSpec? kind = Kinds.Find(kindId);
if (kind is null)
{
return null;
}
ParamResolution resolved = ParamResolver.Resolve(kind.Params, parameters);
return resolved.IsValid ? new BlockSpec(kind, resolved.Values!, streamId, port, scope) : null;
}
}
/// <summary>积木运行的环境:时钟、套接字、对端地址、列块出口。拨号方和监听方各自组装自己用得到的部分。</summary>
public sealed class BlockEnv
{
public required MonoClock Clock { get; init; }
public required INodeLog Log { get; init; }
/// <summary>当前的数据面会话(拨号方固定;监听方是 Agent 当前的 Probe 会话,可能没有)。</summary>
public required Func<DataSession?> Session { get; init; }
public UdpMux? Udp { get; init; }
/// <summary>监听方的准入策略(限速等);拨号方为 null。</summary>
public IAdmission? Admission { get; init; }
public DataPlaneCounters Counters { get; init; } = new();
/// <summary>监听方:接受 TCP 数据连接。</summary>
public TcpAcceptor? TcpListen { get; init; }
/// <summary>拨号方:对端(Agent,或目标任务的目标地址)。</summary>
public IPAddress? PeerAddress { get; init; }
/// <summary>拨号方:对端 TLS 证书的 SPKI 指纹;null 表示不校验证书(目标任务,目标是任意地址)。</summary>
public byte[]? PeerSpkiSha256 { get; init; }
/// <summary>拨号方:列块出口。</summary>
public IFragmentSink? Fragments { get; init; }
/// <summary>本地硬上限:批量传输(tcp_bulk)发送端的码率(Mbit/s)。批量传输会尽可能占满链路,这是防止把小机器打满的保险。</summary>
public double MaxBulkMbps { get; init; } = 100;
/// <summary>本地硬上限:单向数据流接收缓冲的包速率上界。</summary>
public long MaxPacketsPerSecond { get; init; } = 20_000;
public TimeProvider Time => Clock.Time;
/// <summary>同一套环境,但绑定到一个固定的数据面会话(Agent 为每个 Probe 会话各起一组监听方积木)。</summary>
public BlockEnv WithSession(DataSession session) =>
new()
{
Clock = Clock,
Log = Log,
Session = () => session,
Udp = Udp,
Admission = Admission,
Counters = Counters,
TcpListen = TcpListen,
PeerAddress = PeerAddress,
PeerSpkiSha256 = PeerSpkiSha256,
Fragments = Fragments,
MaxBulkMbps = MaxBulkMbps,
MaxPacketsPerSecond = MaxPacketsPerSecond,
};
}