[CI] [SKIP TESTS] Add detailed Probe-Agent connection diagnostics
ci/woodpecker/push/verify Pipeline was successful
ci/woodpecker/push/web-build Pipeline was successful
ci/woodpecker/push/dev-image Pipeline was successful

This commit is contained in:
justme committed 2026-10-20 19:12:11 +08:00
1 parent 0fd38b2367
commit a7ebf61551
16 files changed
+376 -51

No files matched your search

@@ -56,6 +56,10 @@ public sealed class ProbeSession : DataSession
public string ProbeId => Info.PeerId;
public override string DiagnosticPeerId => ProbeId;
public string? CloseReason { get; set; }
public string IpVersion => Info.Remote is IPEndPoint endpoint && endpoint.Address.AddressFamily == AddressFamily.InterNetworkV6 && !endpoint.Address.IsIPv4MappedToIPv6 ? IpVersions.V6 : IpVersions.V4;
public string PathKey => ProbeId + "~" + IpVersion;
@@ -112,6 +112,7 @@ public sealed class PeerHub
public async Task OnSessionAsync(PeerSessionInfo info, IRpcTransport transport, CancellationToken ct)
{
var probe = new ProbeSession(info) { ConnectedAtUtcMs = _time.GetUtcNow().ToUnixTimeMilliseconds() };
var log = new ContextNodeLog(_log, ("probe", probe.ProbeId), ("ip_version", probe.IpVersion), ("control_id", Guid.NewGuid().ToString("N")));
var rpc = new RpcSession<PeerContext>(transport, _router, new PeerContext(probe, _clock), _time);
probe.Rpc = rpc;
@@ -151,6 +152,8 @@ public sealed class PeerHub
ProbeSession? previous = _sessions.Replace(probe);
if (previous?.Rpc is { } old)
{
previous.CloseReason = "replaced";
log.Info("replacing previous probe control session");
await old.CloseAsync("replaced", CancellationToken.None);
}
@@ -159,8 +162,8 @@ public sealed class PeerHub
using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(ct);
Task run = rpc.RunAsync(linked.Token);
Task watchdog = WatchIdleAsync(probe, rpc, linked.Token);
_log.Info("probe connected", ("probe", info.PeerId), ("version", info.Version ?? ""), ("remote", probe.RemoteAddress ?? ""));
Task watchdog = WatchIdleAsync(probe, rpc, log, linked.Token);
log.Info("probe connected", ("phase", "control_session"), ("version", info.Version ?? ""), ("remote", info.Remote));
await _uplink.EmitEventAsync(AgentEventKinds.ProbeConnected, info.PeerId, Describe(probe));
await PushAsync(probe, initial, linked.Token);
try
@@ -171,6 +174,12 @@ public sealed class PeerHub
{
// 停止
}
catch (Exception ex)
{
probe.CloseReason ??= "control_error";
log.Warn("probe control session failed", ("exception", ex.GetType().Name), ("err", ex.Message));
throw;
}
finally
{
_sessions.Remove(probe);
@@ -178,7 +187,10 @@ public sealed class PeerHub
await watchdog;
await rpc.DisposeAsync();
await _tasks.CloseSessionAsync(probe);
_log.Info("probe disconnected", ("probe", info.PeerId));
log.Info("probe disconnected", ("phase", "control_session"),
("reason", probe.CloseReason ?? (ct.IsCancellationRequested ? "service_stopping" : "peer_closed")),
("duration_ms", _time.GetUtcNow().ToUnixTimeMilliseconds() - probe.ConnectedAtUtcMs),
("datagram_bound", probe.DatagramConnection is not null), ("data_remote", probe.DatagramRemoteEndPoint));
OnProbeLost?.Invoke(probe);
await _uplink.EmitEventAsync(AgentEventKinds.ProbeDisconnected, info.PeerId, Describe(probe));
}
@@ -188,8 +200,9 @@ public sealed class PeerHub
/// Probe 每 30 秒发一次时钟同步请求,所以会话里至少每 30 秒有一帧:超过 <see cref="IdleLimit"/> 没有收到任何东西,
/// 说明 Probe 已经静默死亡(断电、进程被冻结),关闭会话,让 Backend 看到 probe_disconnected,而不是一直显示在线。
/// </summary>
private async Task WatchIdleAsync(ProbeSession probe, RpcSession<PeerContext> rpc, CancellationToken ct)
private async Task WatchIdleAsync(ProbeSession probe, RpcSession<PeerContext> rpc, INodeLog log, CancellationToken ct)
{
long lastSummary = _time.GetTimestamp();
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10), _time);
try
{
@@ -197,10 +210,18 @@ public sealed class PeerHub
{
if (rpc.IdleFor > IdleLimit)
{
_log.Warn("probe session is silent, closing it", ("probe", probe.ProbeId), ("idle_s", (long)rpc.IdleFor.TotalSeconds));
probe.CloseReason = "idle_timeout";
log.Warn("probe session is silent, closing it", ("phase", "control_session"), ("idle_s", (long)rpc.IdleFor.TotalSeconds), ("limit_s", IdleLimit.TotalSeconds));
await rpc.CloseAsync("idle", CancellationToken.None);
return;
}
if (_time.GetElapsedTime(lastSummary) >= TimeSpan.FromSeconds(60))
{
lastSummary = _time.GetTimestamp();
log.Info("probe session summary", ("phase", "control_session"), ("remote", probe.Info.Remote),
("control_idle_s", (long)rpc.IdleFor.TotalSeconds), ("datagram_bound", probe.DatagramConnection is not null),
("data_remote", probe.DatagramRemoteEndPoint));
}
}
}
catch (OperationCanceledException)
@@ -225,6 +246,7 @@ public sealed class PeerHub
{
if (probe.ProbeId == probeId && probe.Rpc is { } rpc)
{
probe.CloseReason = "disconnected";
await rpc.CloseAsync("disconnected", CancellationToken.None);
disconnected = true;
}
@@ -240,6 +262,7 @@ public sealed class PeerHub
{
if (!allowed.Contains(probe.ProbeId) && probe.Rpc is { } rpc)
{
probe.CloseReason = "revoked";
_log.Info("probe is no longer allowed, closing its session", ("probe", probe.ProbeId));
_ = rpc.CloseAsync("revoked", CancellationToken.None).AsTask();
}