feat(runtime): track node lifecycle and QUIC link state

This commit is contained in:
justme committed 2026-10-10 14:16:57 +08:00
1 parent 238046d959
commit 97ab3cc740
37 files changed
+864 -35

No files matched your search

@@ -20,9 +20,12 @@ public static class LinkEndpoints
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)
.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)
.Requires(Capability.FleetWrite);
}
private static async Task<IResult> ListAsync(AgentService agents, ProbeService probes, LinkService links, CancellationToken ct)
private static async Task<IResult> ListAsync(AgentService agents, ProbeService probes, LinkService links, ProbeQuicRuntime quic, TimeProvider time, CancellationToken ct)
{
LinkSnapshot snapshot = await links.SnapshotAsync(ct);
IReadOnlyList<AgentView> agentViews = await agents.ListAsync(ct);
@@ -31,14 +34,14 @@ public static class LinkEndpoints
{
foreach (AgentView agent in agentViews.OrderBy(a => a.Agent.Name, StringComparer.Ordinal))
{
items.Add(ToDto(snapshot, probe, agent));
items.Add(ToDto(snapshot, probe, agent, quic, time));
}
}
return TypedResults.Json(new LinkMatrixDto(snapshot.Auto, items), ApiJsonContext.Default.LinkMatrixDto);
}
private static async Task<IResult> SetAsync(HttpContext http, AgentService agents, ProbeService probes, LinkService links, string probe_id, string agent_id, SetLinkRequest request, CancellationToken ct)
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)
{
if (await probes.GetAsync(probe_id, ct) is not { Revoked: false } probe)
{
@@ -52,10 +55,71 @@ public static class LinkEndpoints
await links.SetAsync(probe_id, agent_id, request.Enabled, ct);
await agents.AfterAgentSetChangedAsync(ct); // 重新下发:Probe 的 Agent 列表、Agent 允许的 Probe
return TypedResults.Json(ToDto(await links.SnapshotAsync(ct), probe, agent), ApiJsonContext.Default.LinkDto);
if (request.Enabled == true)
{
await agents.NotifyProbeLinkRetryAsync(new NodeId(probe_id), agent.Agent.Id, ct);
}
return TypedResults.Json(ToDto(await links.SnapshotAsync(ct), probe, agent, quic, time), ApiJsonContext.Default.LinkDto);
}
private static LinkDto ToDto(LinkSnapshot snapshot, ProbeView probe, AgentView agent) =>
new(probe.Probe.Id, probe.Probe.Name, agent.Agent.Id.Value, agent.Agent.Name, snapshot.IsLinked(probe.Probe.Id, agent.Agent.Id.Value), snapshot.OverrideOf(probe.Probe.Id, agent.Agent.Id.Value),
agent.Probes.Any(p => p.Id == probe.Probe.Id));
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)
{
if (await probes.GetAsync(probe_id, ct) is not { Revoked: false } probe)
{
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");
}
await links.SetAsync(probe_id, agent_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);
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)
{
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;
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 (health is null || !fresh)
{
state = "waiting_report";
reason = health is null ? "Probe 尚未上报此线路的 QUIC 拨号状态" : "Probe 的线路状态上报已过期";
}
else if (health.Connected && agent.Probes.Any(p => p.Id == probe.Probe.Id))
{
state = "connected";
}
else if (health.State == ProbeLinkAttemptState.RetryWait)
{
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 的测量控制会话";
}
else
{
state = "quic_connecting";
reason = health.LastError ?? "控制会话已建立,正在建立 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);
}
}
public sealed record LinkRetryDto(bool Queued, LinkDto Link);
@@ -46,7 +46,12 @@ public static class Mappers
health.Connected, health.HandshakeAttempts, health.HandshakeSuccesses, health.HandshakeFailures, health.HandshakeTimeouts,
Rate(health.HandshakeSuccesses), Rate(health.HandshakeFailures), Rate(health.HandshakeTimeouts), health.HandshakeP95Ms, health.HandshakeSampleCount,
health.TransportPacketLossRate, health.TransportPacketsSent, health.TransportCongestionEvents,
health.TransportEcnCongestionEvents, health.PathMtu, Time(snapshot.AtMs));
health.TransportEcnCongestionEvents, health.PathMtu, Time(snapshot.AtMs), health.State switch
{
ProbeLinkAttemptState.Connected => "connected",
ProbeLinkAttemptState.RetryWait => "retry_wait",
_ => "connecting",
}, health.RetryAttempt, health.RetryInMs);
}
public static HostDto ToDto(HostSnapshot h) =>
@@ -57,7 +62,7 @@ public static class Mappers
public static ProbeDto ToDto(ProbeView v) =>
new(
v.Probe.Id, v.Probe.Name, Time(v.Probe.CreatedAtMs), Time(v.Probe.RevokedAtMs), v.Revoked, v.Online,
[.. v.Connections.Select(c => new ProbeConnectionDto(c.AgentId.Value, c.AgentName, c.AgentOnline, c.Version, c.Remote, Time(c.SinceMs), c.IpVersion))], ProbeIpv6Of(v.Probe), v.Probe.AddedBy);
[.. v.Connections.Select(c => new ProbeConnectionDto(c.AgentId.Value, c.AgentName, c.AgentOnline, c.Version, c.Remote, Time(c.SinceMs), c.IpVersion))], v.RuntimeStatus, ProbeIpv6Of(v.Probe), v.Probe.AddedBy);
public static SeriesDto ToDto(SeriesResult r, bool live) =>
new(
@@ -119,7 +124,7 @@ public static class Mappers
: null;
return new AgentDto(
a.Id.Value, a.Name, StatusName(a.Status), view.Online, view.ProbeConnected, [.. view.Probes.Select(ToDto)], view.Host is null ? null : ToDto(view.Host), a.PublicHost, a.ProbeTcpPort, a.ProbeUdpPorts, a.ProbeUdpPorts, a.Labels, a.PublicVisible,
a.Kinds, a.AgentVersion, limits, a.TaskVersion, a.AppliedTaskVersion, rejected, heartbeat, view.EndpointChanges, Time(a.CreatedAtMs), Time(a.LastSeenAtMs), a.Ipv6Host, a.Ipv6Supported, Ipv6Of(view), !a.PublicHostManual, !a.Ipv6HostManual, a.AddedBy);
a.Kinds, a.AgentVersion, limits, a.TaskVersion, a.AppliedTaskVersion, rejected, heartbeat, view.EndpointChanges, view.RuntimeStatus, Time(a.CreatedAtMs), Time(a.LastSeenAtMs), a.Ipv6Host, a.Ipv6Supported, Ipv6Of(view), !a.PublicHostManual, !a.Ipv6HostManual, a.AddedBy);
}
public static Ipv6DiagnosticDto Ipv6Of(AgentView view)
@@ -1,5 +1,6 @@
using Fleet.Core;
using System.Text.Json;
using MoShiro.Protocol;
namespace MoShiro.Backend.Api;
@@ -53,6 +54,7 @@ public sealed record AgentDto(
IReadOnlyList<RejectedDto> Rejected,
HeartbeatDto? Heartbeat,
int EndpointChanges,
NodeRuntimeStatus RuntimeStatus,
DateTimeOffset CreatedAt,
DateTimeOffset? LastSeenAt,
string Ipv6Host = "",
@@ -123,10 +125,10 @@ public sealed record ConnectedProbeDto(string ProbeId, string? Version, string?
/// <summary>某个 Probe 现在连着的一个 Agent。</summary>
public sealed record ProbeConnectionDto(string AgentId, string AgentName, bool AgentOnline, string? Version, string? Remote, DateTimeOffset ConnectedSince, string IpVersion = "ipv4");
public sealed record ProbeDto(string Id, string Name, DateTimeOffset CreatedAt, DateTimeOffset? RevokedAt, bool Revoked, bool Online, IReadOnlyList<ProbeConnectionDto> Connections, Ipv6DiagnosticDto? Ipv6 = null, string? AddedBy = null);
public sealed record ProbeDto(string Id, string Name, DateTimeOffset CreatedAt, DateTimeOffset? RevokedAt, bool Revoked, bool Online, IReadOnlyList<ProbeConnectionDto> Connections, NodeRuntimeStatus RuntimeStatus, Ipv6DiagnosticDto? Ipv6 = null, string? AddedBy = null);
/// <summary>一对(Probe,Agent)的线路开关:<c>linked</c> 是最终结果,<c>override</c> 是手动设置(<c>null</c> = 跟随自动连接),<c>connected</c> 是现在有没有连着。</summary>
public sealed record LinkDto(string ProbeId, string ProbeName, string AgentId, string AgentName, bool Linked, bool? Override, bool Connected);
public sealed record LinkDto(string ProbeId, string ProbeName, string AgentId, string AgentName, bool Linked, bool? Override, bool Connected, NodeRuntimeStatus ProbeRuntimeStatus, NodeRuntimeStatus AgentRuntimeStatus, string ConnectionState = "connecting", string? ConnectionReason = null, string? LastError = null, int RetryAttempt = 0, long? RetryInMs = null);
public sealed record LinkMatrixDto(bool AutoLink, IReadOnlyList<LinkDto> Items);
@@ -41,6 +41,7 @@ namespace MoShiro.Backend.Api;
[JsonSerializable(typeof(AgentPatchRequest))]
[JsonSerializable(typeof(LinkDto))]
[JsonSerializable(typeof(LinkMatrixDto))]
[JsonSerializable(typeof(LinkRetryDto))]
[JsonSerializable(typeof(SetLinkRequest))]
[JsonSerializable(typeof(BuildDto))]
[JsonSerializable(typeof(ReleaseDto))]
@@ -48,7 +48,10 @@ public sealed record QuicLineHealthDto(
uint? TransportCongestionEvents,
uint? TransportEcnCongestionEvents,
ushort? PathMtu,
DateTimeOffset At);
DateTimeOffset At,
string State = "connecting",
int RetryAttempt = 0,
long? RetryInMs = null);
public sealed record OverviewDto(string Range, DateTimeOffset From, DateTimeOffset To, string Step, string Source, bool Degraded, IReadOnlyList<OverviewLineDto> Lines);
@@ -13,6 +13,7 @@ public sealed class AgentRuntimeState
private readonly object _gate = new();
private int _endpointChanges;
private IReadOnlyList<ConnectedProbeInfo> _probes = [];
private string? _lastDisconnectReason;
/// <summary>当前连着这个 Agent 的 Probe(每个 Probe 一项)。</summary>
public IReadOnlyList<ConnectedProbeInfo> Probes => Volatile.Read(ref _probes);
@@ -60,6 +61,17 @@ public sealed class AgentRuntimeState
public int EndpointChanges => Volatile.Read(ref _endpointChanges);
public string? LastDisconnectReason => Volatile.Read(ref _lastDisconnectReason);
public void MarkOnline()
{
Volatile.Write(ref _lastDisconnectReason, null);
LastHeartbeat = null;
LastHeartbeatAtMs = 0;
}
public void MarkOffline(string? reason) => Volatile.Write(ref _lastDisconnectReason, reason);
public void CountEndpointChange() => Interlocked.Increment(ref _endpointChanges);
}
@@ -7,10 +7,46 @@ using MoShiro.Protocol;
namespace MoShiro.Backend.Application;
public sealed record AgentView(AgentRecord Agent, bool Online, IReadOnlyList<ConnectedProbeInfo> Probes, AgentHeartbeatExtra? Heartbeat, int EndpointChanges, long? HeartbeatAtMs = null, HostSnapshot? Host = null)
public sealed record AgentView(AgentRecord Agent, bool Online, IReadOnlyList<ConnectedProbeInfo> Probes, AgentHeartbeatExtra? Heartbeat, int EndpointChanges, long? HeartbeatAtMs = null, HostSnapshot? Host = null, string? LastDisconnectReason = null)
{
/// <summary>至少有一个 Probe 连着。</summary>
public bool ProbeConnected => Probes.Count > 0;
public NodeRuntimeStatus RuntimeStatus
{
get
{
if (Agent.Status == NodeStatus.Disabled)
{
return NodeRuntimeStatus.Maintenance;
}
if (Agent.Status == NodeStatus.Pending)
{
return NodeRuntimeStatus.Initializing;
}
if (!Online)
{
if (LastDisconnectReason == "updating")
{
return NodeRuntimeStatus.Updating;
}
if (LastDisconnectReason == "stopping")
{
return Heartbeat?.Update?.State == "applying" ? NodeRuntimeStatus.Updating : NodeRuntimeStatus.Stopping;
}
return NodeRuntimeStatus.Offline;
}
if (Heartbeat?.Update?.State == "applying")
{
return NodeRuntimeStatus.Updating;
}
if (Heartbeat is null)
{
return NodeRuntimeStatus.Initializing;
}
return Heartbeat.RuntimeStatus;
}
}
}
/// <summary>Agent 的管理:列表、修改、审批、启停、接管、删除,以及 Probe 需要的 Agent 列表。</summary>
@@ -53,9 +89,12 @@ public sealed partial class AgentService
public AgentView View(AgentRecord agent)
{
_runtime.TryGet(agent.Id, out AgentRuntimeState? state);
return new AgentView(agent, _gateway.IsOnline(agent.Id), state?.Probes ?? [], state?.LastHeartbeat, state?.EndpointChanges ?? 0, state?.LastHeartbeatAtMs, state?.Host);
return new AgentView(agent, _gateway.IsOnline(agent.Id), state?.Probes ?? [], state?.LastHeartbeat, state?.EndpointChanges ?? 0, state?.LastHeartbeatAtMs, state?.Host, state?.LastDisconnectReason);
}
public ValueTask<bool> NotifyProbeLinkRetryAsync(NodeId probe, NodeId agent, CancellationToken ct) =>
_gateway.NotifyProbeLinkRetryAsync(probe, agent, ct);
private static bool ValidHost(string host, string ipVersion)
{
if (IPAddress.TryParse(host, out IPAddress? address))
@@ -71,11 +71,13 @@ public sealed class MoShiroNodeEventSink : INodeEventSink
private readonly IProbeStore _probes;
private readonly NodeOwnerService _owners;
private readonly ProbeQuicRuntime _quicRuntime;
private readonly ProbeRuntime _probeRuntime;
public MoShiroNodeEventSink(IAgentStore agents, AgentRuntime runtime, IEventStore events, LiveHub live, AgentService service, NodeInfoService nodes, ReleaseService releases, ScheduleService schedule, IProbeStore probes, NodeOwnerService owners, ProbeQuicRuntime quicRuntime)
public MoShiroNodeEventSink(IAgentStore agents, AgentRuntime runtime, IEventStore events, LiveHub live, AgentService service, NodeInfoService nodes, ReleaseService releases, ScheduleService schedule, IProbeStore probes, NodeOwnerService owners, ProbeQuicRuntime quicRuntime, ProbeRuntime probeRuntime)
{
_owners = owners;
_quicRuntime = quicRuntime;
_probeRuntime = probeRuntime;
_releases = releases;
_schedule = schedule;
_probes = probes;
@@ -99,10 +101,12 @@ public sealed class MoShiroNodeEventSink : INodeEventSink
switch (nodeEvent.Kind)
{
case NodeEventKind.Online:
_runtime.Get(id).MarkOnline();
_live.Publish(new NodeSignal("agent", id.Value, true, nodeEvent.AtMs));
await _events.AddAsync(id, "agent_online", nodeEvent.AtMs, null, ct);
break;
case NodeEventKind.Offline:
_runtime.Get(id).MarkOffline(nodeEvent.Reason);
_runtime.MarkOffline(id);
_releases.RecordStatus(id, null);
_live.Publish(new NodeSignal("agent", id.Value, false, nodeEvent.AtMs));
@@ -143,6 +147,7 @@ public sealed class MoShiroNodeEventSink : INodeEventSink
switch (nodeEvent.Kind)
{
case NodeEventKind.Online:
_probeRuntime.MarkOnline(id.Value);
_live.Publish(new NodeSignal("probe", id.Value, true, nodeEvent.AtMs));
break;
case NodeEventKind.Hello:
@@ -170,6 +175,7 @@ public sealed class MoShiroNodeEventSink : INodeEventSink
case NodeEventKind.Offline:
_live.Publish(new NodeSignal("probe", id.Value, false, nodeEvent.AtMs));
_releases.RecordStatus(id, null);
_probeRuntime.MarkOffline(id.Value, nodeEvent.Reason);
_quicRuntime.MarkProbeOffline(id.Value, nodeEvent.AtMs);
break;
case NodeEventKind.Heartbeat when nodeEvent.Extra is { } extra:
@@ -177,6 +183,10 @@ public sealed class MoShiroNodeEventSink : INodeEventSink
{
ProbeHeartbeatExtra? heartbeat = extra.Deserialize(MoShiroJsonContext.Default.ProbeHeartbeatExtra);
_releases.RecordStatus(id, heartbeat?.Update);
if (heartbeat is not null)
{
_probeRuntime.RecordHeartbeat(id.Value, nodeEvent.AtMs, heartbeat);
}
if (heartbeat?.QuicLinks is { } quicLinks)
{
_quicRuntime.Replace(id.Value, nodeEvent.AtMs, quicLinks);
@@ -39,6 +39,10 @@ public sealed class ProbeQuicRuntime
AgentId = old.AgentId,
IpVersion = old.IpVersion,
Connected = false,
State = ProbeLinkAttemptState.RetryWait,
LastError = "Probe 与 Backend 的连接中断",
RetryAttempt = old.RetryAttempt,
RetryInMs = old.RetryInMs,
HandshakeAttempts = old.HandshakeAttempts,
HandshakeSuccesses = old.HandshakeSuccesses,
HandshakeFailures = old.HandshakeFailures,
@@ -0,0 +1,22 @@
using System.Collections.Concurrent;
using MoShiro.Protocol;
namespace MoShiro.Backend.Application;
/// <summary>Volatile runtime status reported by each Probe's heartbeat and Fleet connection lifecycle.</summary>
public sealed class ProbeRuntime
{
private readonly ConcurrentDictionary<string, ProbeRuntimeSnapshot> _probes = new(StringComparer.Ordinal);
public ProbeRuntimeSnapshot? Get(string probeId) => _probes.TryGetValue(probeId, out ProbeRuntimeSnapshot? snapshot) ? snapshot : null;
public void RecordHeartbeat(string probeId, long atMs, ProbeHeartbeatExtra heartbeat) =>
_probes[probeId] = new ProbeRuntimeSnapshot(heartbeat, atMs, null);
public void MarkOnline(string probeId) => _probes[probeId] = new ProbeRuntimeSnapshot(null, 0, null);
public void MarkOffline(string probeId, string? reason) =>
_probes.AddOrUpdate(probeId, _ => new ProbeRuntimeSnapshot(null, 0, reason), (_, current) => current with { DisconnectReason = reason });
}
public sealed record ProbeRuntimeSnapshot(ProbeHeartbeatExtra? Heartbeat, long AtMs, string? DisconnectReason);
@@ -8,9 +8,41 @@ namespace MoShiro.Backend.Application;
/// <summary>某个 Probe 现在连着的一个 Agent(Agent 上报)。</summary>
public sealed record ProbeConnection(NodeId AgentId, string AgentName, bool AgentOnline, string? Version, string? Remote, long SinceMs, string IpVersion = IpVersions.V4);
public sealed record ProbeView(ProbeRecord Probe, bool Online, IReadOnlyList<ProbeConnection> Connections)
public sealed record ProbeView(ProbeRecord Probe, bool Online, IReadOnlyList<ProbeConnection> Connections, ProbeRuntimeSnapshot? Runtime = null)
{
public bool Revoked => Probe.RevokedAtMs is not null;
public NodeRuntimeStatus RuntimeStatus
{
get
{
if (Revoked)
{
return NodeRuntimeStatus.Maintenance;
}
if (!Online)
{
if (Runtime?.DisconnectReason == "updating")
{
return NodeRuntimeStatus.Updating;
}
if (Runtime?.DisconnectReason == "stopping")
{
return Runtime.Heartbeat?.Update?.State == "applying" ? NodeRuntimeStatus.Updating : NodeRuntimeStatus.Stopping;
}
return NodeRuntimeStatus.Offline;
}
if (Runtime?.Heartbeat?.Update?.State == "applying")
{
return NodeRuntimeStatus.Updating;
}
if (Runtime?.Heartbeat is null)
{
return NodeRuntimeStatus.Initializing;
}
return Runtime.Heartbeat.RuntimeStatus;
}
}
}
/// <summary>
@@ -28,13 +60,15 @@ public sealed partial class ProbeService
private readonly TimeProvider _time;
private readonly NodeInfoService _nodes;
private readonly LinkService _links;
private readonly ProbeRuntime _probeRuntime;
private readonly NodeOwnerService _owners;
public ProbeService(NodeOwnerService owners, IProbeStore probes, IAgentStore agents, AgentRuntime runtime, IAgentGateway gateway, TimeProvider time, NodeInfoService nodes, LinkService links)
public ProbeService(NodeOwnerService owners, IProbeStore probes, IAgentStore agents, AgentRuntime runtime, IAgentGateway gateway, TimeProvider time, NodeInfoService nodes, LinkService links, ProbeRuntime probeRuntime)
{
_owners = owners;
_links = links;
_probeRuntime = probeRuntime;
_nodes = nodes;
_probes = probes;
_agents = agents;
@@ -133,7 +167,7 @@ public sealed partial class ProbeService
}
}
return new ProbeView(probe, _gateway.IsOnline(new NodeId(probe.Id)), [.. connections.OrderBy(c => c.AgentName, StringComparer.Ordinal)]);
return new ProbeView(probe, _gateway.IsOnline(new NodeId(probe.Id)), [.. connections.OrderBy(c => c.AgentName, StringComparer.Ordinal)], _probeRuntime.Get(probe.Id));
}
[GeneratedRegex(@"^[\p{L}\p{M}\p{N}._-]{1,64}$")]
@@ -16,6 +16,7 @@ public static class ApplicationServiceCollectionExtensions
services.AddHostedService<StartupTasks>();
services.AddSingleton<AgentRuntime>();
services.AddSingleton<ProbeRuntime>();
services.AddSingleton<ProbeQuicRuntime>();
services.AddSingleton<LiveHub>();
services.AddSingleton<HotLayer>();
@@ -30,6 +30,9 @@ public interface IAgentGateway
ValueTask DisconnectAsync(NodeId agent, string reason, CancellationToken ct);
/// <summary>Notify an online Probe to immediately retry all IP paths to an Agent.</summary>
ValueTask<bool> NotifyProbeLinkRetryAsync(NodeId probe, NodeId agent, CancellationToken ct) => ValueTask.FromResult(false);
/// <summary>把发布清单和签名原样交给节点(通知,不等回复)。</summary>
ValueTask NotifyUpdateOfferAsync(NodeId node, string manifest, string signature, CancellationToken ct);
@@ -64,6 +64,9 @@ public sealed class FleetGateway : IAgentGateway
public ValueTask DisconnectAsync(NodeId agent, string reason, CancellationToken ct) => Connections.DisconnectAsync(agent, reason, ct);
public ValueTask<bool> NotifyProbeLinkRetryAsync(NodeId probe, NodeId agent, CancellationToken ct) =>
Connections.NotifyAsync(probe, MoShiroMethods.ProbeLinkRetry, new ProbeLinkRetryParams { AgentId = agent }, MoShiroJsonContext.Default.ProbeLinkRetryParams, ct);
public async ValueTask NotifyUpdateOfferAsync(NodeId node, string manifest, string signature, CancellationToken ct) =>
await Connections.NotifyAsync(node, MoShiroMethods.UpdateOffer, new UpdateOfferParams { Manifest = manifest, Signature = signature }, MoShiroJsonContext.Default.UpdateOfferParams, ct);