Files
MoShiro/backend/fleet/src/Fleet.Rpc/MethodTable.cs
T
justme ad5efc62ff agent: unify listening and dialing with directed mutual measurements
Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry.

Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
2026-10-21 11:55:36 +08:00

121 lines
4.7 KiB
C#

namespace Fleet.Rpc;
/// <summary>连接的哪一侧。节点面:服务端和节点;对等面:拨号方和监听方。</summary>
public enum RpcSide
{
Server,
Node,
Dialer,
Listener,
}
public enum RpcPlane
{
Node,
Peer,
}
public enum MethodKind
{
Request,
Notification,
}
public static class RpcSides
{
public static RpcPlane PlaneOf(RpcSide side) => side is RpcSide.Server or RpcSide.Node ? RpcPlane.Node : RpcPlane.Peer;
public static RpcSide Opposite(RpcSide side) => side switch
{
RpcSide.Server => RpcSide.Node,
RpcSide.Node => RpcSide.Server,
RpcSide.Dialer => RpcSide.Listener,
_ => RpcSide.Dialer,
};
}
/// <summary>
/// 一个方法的声明:<b>只有 <see cref="From"/> 能发起它</b>(不存在双向方法),类型是请求还是通知,哪些节点角色可以参与。
/// <see cref="Inline"/> 表示它在认证或握手阶段由连接代码直接处理,不经过路由器。
/// </summary>
public sealed record MethodSpec(string Name, RpcSide From, MethodKind Kind, IReadOnlyList<string>? Roles = null, bool Inline = false)
{
public RpcPlane Plane => RpcSides.PlaneOf(From);
/// <summary>接收方。</summary>
public RpcSide To => RpcSides.Opposite(From);
public bool AllowsRole(string? role) => Roles is null || (role is not null && Roles.Contains(role));
public static MethodSpec Request(string name, RpcSide from, params string[] roles) => new(name, from, MethodKind.Request, roles.Length == 0 ? null : roles);
public static MethodSpec Notification(string name, RpcSide from, params string[] roles) => new(name, from, MethodKind.Notification, roles.Length == 0 ? null : roles);
public MethodSpec AsInline() => this with { Inline = true };
}
/// <summary>二进制通道的声明:谁能发、哪些角色可以发。通道 0–15 保留给框架,产品从 16 开始。</summary>
public sealed record ChannelSpec(byte Channel, RpcSide From, IReadOnlyList<string>? Roles = null)
{
public RpcPlane Plane => RpcSides.PlaneOf(From);
public bool AllowsRole(string? role) => Roles is null || (role is not null && Roles.Contains(role));
}
/// <summary>
/// 协议方法和二进制通道的声明表,单一真源。路由器用它在注册时拒绝方向、类型、角色不符的处理函数,
/// 在收发时拒绝没有声明或不该由对方发起的方法。同一个方法名在不同平面里可以有不同的发起方,所以按(平面,名字)查找。
/// </summary>
public sealed class MethodTable
{
private readonly Dictionary<(RpcPlane, string), MethodSpec> _methods = [];
private readonly Dictionary<(RpcPlane, byte), ChannelSpec> _channels = [];
public MethodTable(IEnumerable<MethodSpec> methods, IEnumerable<ChannelSpec>? channels = null)
{
foreach (MethodSpec spec in methods)
{
if (!_methods.TryAdd((spec.Plane, spec.Name), spec))
{
throw new ArgumentException($"method {spec.Name} is declared twice in the {spec.Plane} plane");
}
}
foreach (ChannelSpec spec in channels ?? [])
{
if (!_channels.TryAdd((spec.Plane, spec.Channel), spec))
{
throw new ArgumentException($"channel {spec.Channel} is declared twice in the {spec.Plane} plane");
}
}
}
public IEnumerable<MethodSpec> Methods => _methods.Values;
public IEnumerable<ChannelSpec> Channels => _channels.Values;
public MethodSpec? Find(RpcPlane plane, string method) => _methods.GetValueOrDefault((plane, method));
public ChannelSpec? FindChannel(RpcPlane plane, byte channel) => _channels.GetValueOrDefault((plane, channel));
/// <summary>合并多张表(框架的加产品的)。</summary>
public static MethodTable Combine(params MethodTable[] tables) =>
new(tables.SelectMany(t => t.Methods), tables.SelectMany(t => t.Channels));
/// <summary>
/// 对账:<paramref name="receiver"/> 这一侧、<paramref name="role"/> 这个角色应该处理、但没有注册处理函数的方法
/// (认证阶段直接处理的 <see cref="MethodSpec.Inline"/> 方法除外)。<paramref name="role"/> 为空表示不限角色(服务端要处理所有角色发来的)。
/// </summary>
public IReadOnlyList<string> MissingHandlers(RpcSide receiver, string? role, IEnumerable<string> registered)
{
HashSet<string> have = [.. registered];
return
[
.. Methods
.Where(m => m.To == receiver && !m.Inline && (role is null || m.AllowsRole(role)) && !have.Contains(m.Name))
.Select(m => m.Name)
.Order(StringComparer.Ordinal),
];
}
}