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.
This commit is contained in:
1 parent
d5ebf29e57
commit
ad5efc62ff
206 files changed
+4202
-4964
No files matched your search
@@ -1,4 +1,5 @@
|
||||
using Fleet.Core;
|
||||
using Fleet.Server;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
@@ -7,7 +8,10 @@ using MoShiro.Protocol;
|
||||
|
||||
namespace MoShiro.Backend.Api;
|
||||
|
||||
/// <summary>线路开关:哪些(Probe,Agent)要连。自动连接的总开关是设置 <c>fleet.auto_link</c>;这里看每一对的结果,并且手动强制开、关或取消手动设置。</summary>
|
||||
/// <summary>
|
||||
/// 线路开关:哪些(拨号方,监听方)要连。自动连接的总开关是设置 <c>fleet.auto_link</c>(不监听的节点自动拨所有监听节点);
|
||||
/// 这里看每一对的结果,并且手动强制开、关或取消手动设置。把一个节点拖到另一个监听节点上建立线路,就是 <c>PUT</c> 一个 <c>enabled: true</c>。
|
||||
/// </summary>
|
||||
public static class LinkEndpoints
|
||||
{
|
||||
private const string Tag = "admin-links";
|
||||
@@ -15,110 +19,139 @@ public static class LinkEndpoints
|
||||
public static void Map(RouteGroupBuilder group)
|
||||
{
|
||||
group.MapGet("/links", ListAsync)
|
||||
.Api("links_list", "Every probe/agent pair: whether it is linked, the manual override and whether it is connected", Tag).Returns<LinkMatrixDto>().ProducesProblems(401, 403)
|
||||
.Api("links_list", "Every possible dialer/listener pair: whether it is linked, the manual override and whether it is connected; plus the nodes and which of them listen", Tag).Returns<LinkMatrixDto>().ProducesProblems(401, 403)
|
||||
.Requires(Capability.FleetRead);
|
||||
group.MapPut("/links/{probe_id}/{agent_id}", SetAsync)
|
||||
.Api("links_set", "Force a link on or off, or give it back to the automatic setting (enabled: null)", Tag).Returns<LinkDto>().ProducesProblems(400, 401, 403, 404)
|
||||
group.MapPut("/links/{dialer_id}/{listener_id}", SetAsync)
|
||||
.Api("links_set", "Connect a node to a listening node (enabled: true), force the link off (false), or give it back to the automatic setting (null)", Tag).Returns<LinkDto>().ProducesProblems(400, 401, 403, 404, 409, 422)
|
||||
.Requires(Capability.FleetWrite);
|
||||
group.MapPost("/links/{probe_id}/{agent_id}/retry", RetryAsync)
|
||||
.Api("links_retry", "Enable and immediately retry the Probe-to-Agent connection", Tag).Returns<LinkRetryDto>().ProducesProblems(401, 403, 404)
|
||||
group.MapPost("/links/{dialer_id}/{listener_id}/retry", RetryAsync)
|
||||
.Api("links_retry", "Enable and immediately retry the dialer-to-listener connection", Tag).Returns<LinkRetryDto>().ProducesProblems(401, 403, 404, 409, 422)
|
||||
.Requires(Capability.FleetWrite);
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListAsync(AgentService agents, ProbeService probes, LinkService links, ProbeQuicRuntime quic, TimeProvider time, CancellationToken ct)
|
||||
private static async Task<IResult> ListAsync(AgentService agents, LinkService links, LinkRuntime runtime, TimeProvider time, CancellationToken ct)
|
||||
{
|
||||
LinkSnapshot snapshot = await links.SnapshotAsync(ct);
|
||||
IReadOnlyList<AgentView> agentViews = await agents.ListAsync(ct);
|
||||
AgentView[] nodes = [.. (await agents.ListAsync(ct)).Where(n => n.Agent.Status == NodeStatus.Active).OrderBy(n => n.Agent.Name, StringComparer.Ordinal)];
|
||||
var items = new List<LinkDto>();
|
||||
foreach (ProbeView probe in (await probes.ListAsync(ct)).Where(p => !p.Revoked).OrderBy(p => p.Probe.Name, StringComparer.Ordinal))
|
||||
foreach (AgentView dialer in nodes)
|
||||
{
|
||||
foreach (AgentView agent in agentViews.OrderBy(a => a.Agent.Name, StringComparer.Ordinal))
|
||||
foreach (AgentView listener in nodes.Where(n => n.Agent.Listens && n.Agent.Id != dialer.Agent.Id))
|
||||
{
|
||||
items.Add(ToDto(snapshot, probe, agent, quic, time));
|
||||
items.Add(ToDto(snapshot, dialer, listener, runtime, time));
|
||||
}
|
||||
}
|
||||
|
||||
return TypedResults.Json(new LinkMatrixDto(snapshot.Auto, items), ApiJsonContext.Default.LinkMatrixDto);
|
||||
LinkNodeDto[] nodeItems = [.. nodes.Select(n => new LinkNodeDto(n.Agent.Id.Value, n.Agent.Name, n.Agent.Listens, n.Online, n.RuntimeStatus))];
|
||||
return TypedResults.Json(new LinkMatrixDto(snapshot.Auto, nodeItems, items), ApiJsonContext.Default.LinkMatrixDto);
|
||||
}
|
||||
|
||||
private static async Task<IResult> SetAsync(HttpContext http, AgentService agents, ProbeService probes, LinkService links, ProbeQuicRuntime quic, TimeProvider time, string probe_id, string agent_id, SetLinkRequest request, CancellationToken ct)
|
||||
private static async Task<IResult> SetAsync(HttpContext http, AgentService agents, LinkService links, LinkRuntime runtime, TimeProvider time, string dialer_id, string listener_id, SetLinkRequest request, CancellationToken ct)
|
||||
{
|
||||
if (await probes.GetAsync(probe_id, ct) is not { Revoked: false } probe)
|
||||
(AgentView? dialer, AgentView? listener, IResult? problem) = await ResolveAsync(http, agents, dialer_id, listener_id, request.Enabled == true, ct);
|
||||
if (problem is not null)
|
||||
{
|
||||
return Problems.Create(http, MoShiroErrors.ProbeNotFound, "probe not found");
|
||||
return problem;
|
||||
}
|
||||
|
||||
if (await agents.GetAsync(new NodeId(agent_id), ct) is not { } agent)
|
||||
{
|
||||
return Problems.Create(http, MoShiroErrors.AgentNotFound, "agent not found");
|
||||
}
|
||||
|
||||
await links.SetAsync(probe_id, agent_id, request.Enabled, ct);
|
||||
await agents.AfterAgentSetChangedAsync(ct); // 重新下发:Probe 的 Agent 列表、Agent 允许的 Probe
|
||||
await links.SetAsync(dialer_id, listener_id, request.Enabled, ct);
|
||||
await agents.AfterAgentSetChangedAsync(ct); // 重新下发:拨号方的拨号目标、监听方允许的拨号方
|
||||
if (request.Enabled == true)
|
||||
{
|
||||
await agents.NotifyProbeLinkRetryAsync(new NodeId(probe_id), agent.Agent.Id, ct);
|
||||
await agents.NotifyLinkRetryAsync(dialer!.Agent.Id, listener!.Agent.Id, ct);
|
||||
}
|
||||
return TypedResults.Json(ToDto(await links.SnapshotAsync(ct), probe, agent, quic, time), ApiJsonContext.Default.LinkDto);
|
||||
|
||||
return TypedResults.Json(ToDto(await links.SnapshotAsync(ct), dialer!, listener!, runtime, time), ApiJsonContext.Default.LinkDto);
|
||||
}
|
||||
|
||||
private static async Task<IResult> RetryAsync(HttpContext http, AgentService agents, ProbeService probes, LinkService links, ProbeQuicRuntime quic, TimeProvider time, string probe_id, string agent_id, CancellationToken ct)
|
||||
private static async Task<IResult> RetryAsync(HttpContext http, AgentService agents, LinkService links, LinkRuntime runtime, TimeProvider time, string dialer_id, string listener_id, CancellationToken ct)
|
||||
{
|
||||
if (await probes.GetAsync(probe_id, ct) is not { Revoked: false } probe)
|
||||
(AgentView? dialer, AgentView? listener, IResult? problem) = await ResolveAsync(http, agents, dialer_id, listener_id, requireListening: true, ct);
|
||||
if (problem is not null)
|
||||
{
|
||||
return Problems.Create(http, MoShiroErrors.ProbeNotFound, "probe not found");
|
||||
}
|
||||
if (await agents.GetAsync(new NodeId(agent_id), ct) is not { } agent)
|
||||
{
|
||||
return Problems.Create(http, MoShiroErrors.AgentNotFound, "agent not found");
|
||||
return problem;
|
||||
}
|
||||
|
||||
await links.SetAsync(probe_id, agent_id, true, ct);
|
||||
await links.SetAsync(dialer_id, listener_id, true, ct);
|
||||
await agents.AfterAgentSetChangedAsync(ct);
|
||||
bool queued = await agents.NotifyProbeLinkRetryAsync(new NodeId(probe_id), agent.Agent.Id, ct);
|
||||
LinkDto link = ToDto(await links.SnapshotAsync(ct), probe, agent, quic, time);
|
||||
bool queued = await agents.NotifyLinkRetryAsync(dialer!.Agent.Id, listener!.Agent.Id, ct);
|
||||
LinkDto link = ToDto(await links.SnapshotAsync(ct), dialer, listener, runtime, time);
|
||||
return TypedResults.Json(new LinkRetryDto(queued, link), ApiJsonContext.Default.LinkRetryDto, statusCode: StatusCodes.Status202Accepted);
|
||||
}
|
||||
|
||||
private static LinkDto ToDto(LinkSnapshot snapshot, ProbeView probe, AgentView agent, ProbeQuicRuntime quicRuntime, TimeProvider time)
|
||||
/// <summary>两端都得是存在且激活的节点,不能连自己;开线路时监听方必须在监听。</summary>
|
||||
private static async Task<(AgentView? Dialer, AgentView? Listener, IResult? Problem)> ResolveAsync(HttpContext http, AgentService agents, string dialerId, string listenerId, bool requireListening, CancellationToken ct)
|
||||
{
|
||||
bool linked = snapshot.IsLinked(probe.Probe.Id, agent.Agent.Id.Value);
|
||||
ProbeQuicLinkSnapshot? quicSnapshot = quicRuntime.Get(probe.Probe.Id, agent.Agent.Id.Value, IpVersions.V4);
|
||||
ProbeQuicLinkHealth? health = quicSnapshot?.Health;
|
||||
bool fresh = quicSnapshot is not null && time.GetUtcNow().ToUnixTimeMilliseconds() - quicSnapshot.AtMs <= 30_000;
|
||||
if (dialerId == listenerId)
|
||||
{
|
||||
return (null, null, Problems.Create(http, MoShiroErrors.SelfLink, "a node cannot connect to itself"));
|
||||
}
|
||||
|
||||
if (await agents.GetAsync(new NodeId(dialerId), ct) is not { } dialer)
|
||||
{
|
||||
return (null, null, Problems.Create(http, MoShiroErrors.AgentNotFound, "dialer not found"));
|
||||
}
|
||||
|
||||
if (await agents.GetAsync(new NodeId(listenerId), ct) is not { } listener)
|
||||
{
|
||||
return (null, null, Problems.Create(http, MoShiroErrors.AgentNotFound, "listener not found"));
|
||||
}
|
||||
|
||||
if (requireListening && !listener.Agent.Listens)
|
||||
{
|
||||
return (null, null, Problems.Create(http, MoShiroErrors.NotListening, "the listener does not accept connections: only nodes that listen can be connected to"));
|
||||
}
|
||||
|
||||
return (dialer, listener, null);
|
||||
}
|
||||
|
||||
private static LinkDto ToDto(LinkSnapshot snapshot, AgentView dialer, AgentView listener, LinkRuntime runtime, TimeProvider time)
|
||||
{
|
||||
bool linked = snapshot.IsLinked(dialer.Agent, listener.Agent);
|
||||
ReportedLink? reported = runtime.Get(dialer.Agent.Id.Value, listener.Agent.Id.Value, IpVersions.V4);
|
||||
LinkHealth? health = reported?.Health;
|
||||
bool fresh = reported is not null && time.GetUtcNow().ToUnixTimeMilliseconds() - reported.AtMs <= 30_000;
|
||||
string state;
|
||||
string? reason = null;
|
||||
if (!linked) { state = "disabled"; reason = "线路策略已关闭"; }
|
||||
else if (probe.RuntimeStatus != NodeRuntimeStatus.Running) { state = "probe_unavailable"; reason = $"Probe 状态:{probe.RuntimeStatus}"; }
|
||||
else if (agent.RuntimeStatus != NodeRuntimeStatus.Running) { state = "agent_unavailable"; reason = $"Agent 状态:{agent.RuntimeStatus}"; }
|
||||
else if (dialer.RuntimeStatus != NodeRuntimeStatus.Running) { state = "dialer_unavailable"; reason = $"拨号节点状态:{dialer.RuntimeStatus}"; }
|
||||
else if (listener.RuntimeStatus != NodeRuntimeStatus.Running) { state = "listener_unavailable"; reason = $"监听节点状态:{listener.RuntimeStatus}"; }
|
||||
else if (health is null || !fresh)
|
||||
{
|
||||
state = "waiting_report";
|
||||
reason = health is null ? "Probe 尚未上报此线路的 QUIC 拨号状态" : "Probe 的线路状态上报已过期";
|
||||
reason = health is null ? "拨号节点尚未上报此线路的状态" : "拨号节点的线路状态上报已过期";
|
||||
}
|
||||
else if (health.Connected && agent.Probes.Any(p => p.Id == probe.Probe.Id))
|
||||
else if (health.Control.State == ControlState.Backoff)
|
||||
{
|
||||
state = "control_down";
|
||||
reason = health.Control.LastError is { } fault ? $"与监听节点失联:{fault}" : "与监听节点失联,等待自动重试";
|
||||
}
|
||||
else if (health.Control.State != ControlState.Up || listener.Dialers.All(d => d.Id != dialer.Agent.Id.Value))
|
||||
{
|
||||
state = "control_connecting";
|
||||
reason = "正在建立到监听节点的控制连接";
|
||||
}
|
||||
else if (health.Task.Quic.State == QuicState.Ready)
|
||||
{
|
||||
state = "connected";
|
||||
}
|
||||
else if (health.State == ProbeLinkAttemptState.RetryWait)
|
||||
else if (health.Task.Quic.State == QuicState.Retrying)
|
||||
{
|
||||
state = "retry_wait";
|
||||
reason = health.LastError is { Length: > 0 } error ? $"QUIC 连接失败:{error}" : "QUIC 连接失败,等待自动重试";
|
||||
}
|
||||
else if (agent.Probes.All(p => p.Id != probe.Probe.Id))
|
||||
{
|
||||
state = "control_disconnected";
|
||||
reason = health.LastError ?? "Probe 尚未建立到 Agent 的测量控制会话";
|
||||
state = "task_retrying";
|
||||
reason = health.Task.Quic.LastError is { } fault ? $"控制连接正常,QUIC 数据通道重试中:{fault}" : "控制连接正常,QUIC 数据通道重试中";
|
||||
}
|
||||
else
|
||||
{
|
||||
state = "quic_connecting";
|
||||
reason = health.LastError ?? "控制会话已建立,正在建立 QUIC 测量会话";
|
||||
state = "task_connecting";
|
||||
reason = "控制连接正常,正在建立 QUIC 数据通道";
|
||||
}
|
||||
bool connected = state == "connected";
|
||||
return new LinkDto(probe.Probe.Id, probe.Probe.Name, agent.Agent.Id.Value, agent.Agent.Name, linked,
|
||||
snapshot.OverrideOf(probe.Probe.Id, agent.Agent.Id.Value), connected, probe.RuntimeStatus, agent.RuntimeStatus,
|
||||
state, reason, health?.LastError, health?.RetryAttempt ?? 0, health?.RetryInMs);
|
||||
|
||||
bool inBackoff = health?.Control.State == ControlState.Backoff;
|
||||
return new LinkDto(dialer.Agent.Id.Value, dialer.Agent.Name, listener.Agent.Id.Value, listener.Agent.Name, linked,
|
||||
snapshot.OverrideOf(dialer.Agent.Id.Value, listener.Agent.Id.Value), state == "connected", dialer.RuntimeStatus, listener.RuntimeStatus,
|
||||
state, reason, (inBackoff ? health!.Control.LastError : health?.Task.Quic.LastError)?.ToString(),
|
||||
health is null ? 0 : inBackoff ? health.Control.RetryAttempt : health.Task.Quic.RetryAttempt,
|
||||
health is null ? null : inBackoff ? health.Control.RetryInMs : health.Task.Quic.RetryInMs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user