From a0c6850d85e1f8f1d9613a323df12d1c94cce8f6 Mon Sep 17 00:00:00 2001 From: tv Date: Sat, 31 Oct 2026 21:12:50 +0100 Subject: links report down --- src/api/dashboard/clocks.rs | 1 + src/api/dashboard/dashboard.html | 27 +++++++++++++++++---------- src/api/dashboard/mod.rs | 1 + src/swarm/links.rs | 10 ++++++++++ src/transport/mod.rs | 3 +++ src/transport/wire.rs | 3 ++- 6 files changed, 34 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/api/dashboard/clocks.rs b/src/api/dashboard/clocks.rs index 313b78e..9f069be 100644 --- a/src/api/dashboard/clocks.rs +++ b/src/api/dashboard/clocks.rs @@ -96,6 +96,7 @@ mod tests { peer: peer.into(), transport: None, relayed: false, + down: false, rtt_us: None, dormant: false, others: Vec::new(), diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index f45ded3..d2935f6 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -404,7 +404,7 @@ function renderLegend(measured) { el("span", { class: "other" }, "other transport"), el("span", { class: "direct" }, "direct"), el("span", { class: "relayed" }, "relayed"), - el("span", { class: "dormant" }, "dormant"), + el("span", { class: "dormant" }, "dormant or down"), el("span", { class: "silent" }, "an end reports no links")); document.getElementById("legend").replaceChildren(...items); } @@ -427,15 +427,19 @@ function edges() { if (!known || (timed && !knownTimed) || (timed === knownTimed && report.secsAgo < known.secsAgo)) { measured.set(key, { a, b: link.peer, link, secsAgo: report.secsAgo, - dormant: link.dormant || (known && known.dormant) }); - } else if (link.dormant) { - known.dormant = true; + dormant: link.dormant || (known && known.dormant), + down: link.down || (known && known.down) }); + } else { + if (link.dormant) known.dormant = true; + if (link.down) known.down = true; } } } - // Dormant if either end holds the other so, whichever report is drawn. + // Dormant, or down, if either end says so, whichever report is drawn. for (const edge of measured.values()) { - if (edge.dormant && !edge.link.dormant) edge.link = { ...edge.link, dormant: true }; + if ((edge.dormant && !edge.link.dormant) || (edge.down && !edge.link.down)) { + edge.link = { ...edge.link, dormant: !!edge.dormant, down: !!edge.down }; + } } return { intended, measured }; } @@ -615,8 +619,10 @@ function renderGraph() { const nx = -(q.y - p.y) / length, ny = (q.x - p.x) / length; // An end that reports no links (any more): what's drawn may be out of date. const silent = !(a in state.graph.measured) || !(b in state.graph.measured); - // Dormant: a red underlay as wide as the lines, as a dormant node's red outline. - if (link.dormant) { + // Dormant, or down (no path answers, whatever its connection was): a red underlay + // as wide as the lines, as a dormant node's red outline. + const broken = link.dormant || link.down; + if (broken) { children.push(drawing("underlay " + key, "line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y, stroke: "var(--bad)", "stroke-opacity": .5, "stroke-width": width + 4, "stroke-linecap": "round" })); @@ -627,8 +633,8 @@ function renderGraph() { const ox = nx * side, oy = ny * side; const attrs = { x1: p.x + ox, y1: p.y + oy, x2: q.x + ox, y2: q.y + oy, stroke: colourOf(line.transport), - // Narrow where dormant (over its underlay). - "stroke-width": link.dormant ? (line.used ? 1.5 : 0.8) : line.used ? 3 : 1.2 }; + // Narrow where dormant or down (over its underlay). + "stroke-width": broken ? (line.used ? 1.5 : 0.8) : line.used ? 3 : 1.2 }; // Dotted where an end reports nothing, dashed where relayed, solid where direct. if (silent) { attrs["stroke-dasharray"] = line.used ? "1 5" : "1 3"; @@ -640,6 +646,7 @@ function renderGraph() { const description = name(a) + " – " + name(b) + (line.transport ? " via " + line.transport : "") + (line.used ? " (in use)" : "") + ((line.used ? link.relayed : line.relayed) ? ", relayed" : "") + (link.dormant ? ", dormant" : "") + + (line.used && link.down ? ", down" : "") + (silent ? ", one end reports no links" : "") + (typeof line.rttMs === "number" ? ", " + line.rttMs.toFixed(1) + " ms" : "") + ", measured " + age(secsAgo + elapsed()) + " ago"; diff --git a/src/api/dashboard/mod.rs b/src/api/dashboard/mod.rs index 310846a..0e7b442 100644 --- a/src/api/dashboard/mod.rs +++ b/src/api/dashboard/mod.rs @@ -331,6 +331,7 @@ fn links_json(links: &[Link]) -> JsonValue { "peer": link.peer, "transport": link.transport, "relayed": link.relayed, + "down": link.down, "rttMs": link.rtt_us.map(|us| f64::from(us) / 1000.0), "dormant": link.dormant, "clock": link.clock, diff --git a/src/swarm/links.rs b/src/swarm/links.rs index 3e4d717..e3c1b56 100644 --- a/src/swarm/links.rs +++ b/src/swarm/links.rs @@ -122,6 +122,11 @@ pub(crate) async fn own_links(state: &AppState) -> Vec { relayed: destination .as_ref() .is_some_and(|destination| destination.connection == Some(Connection::Relayed)), + // Ranked up before down: the first is down only if all are. + down: destination + .as_ref() + .and_then(|destination| destination.paths.first()) + .is_some_and(|path| path.down), // The path in use's (`Transport::rtt`, the first of `destination.paths`). rtt_us: state .transport @@ -390,6 +395,7 @@ fn changed(old: &[Link], new: &[Link]) -> bool { a.peer != b.peer || a.transport != b.transport || a.relayed != b.relayed + || a.down != b.down || a.dormant != b.dormant || moved(a.rtt_us, b.rtt_us) || clock_moved(a.clock, b.clock) @@ -411,6 +417,7 @@ mod tests { peer: peer.into(), transport: Some("libp2p".into()), relayed: false, + down: false, rtt_us, dormant: false, others: Vec::new(), @@ -429,6 +436,9 @@ mod tests { let mut dormant = link("a", Some(10_000)); dormant.dormant = true; assert!(changed(&old, &[dormant])); + let mut down = link("a", Some(10_000)); + down.down = true; + assert!(changed(&old, &[down])); // Small round trips: a millisecond's wobble isn't news. assert!(!changed(&[link("a", Some(400))], &[link("a", Some(1_300))])); // A clock: news when measured, and when it moved beyond both bounds. diff --git a/src/transport/mod.rs b/src/transport/mod.rs index 2093e3d..705e473 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -435,6 +435,9 @@ pub(crate) struct Link { pub(crate) transport: Option, /// Only through a relay (libp2p). pub(crate) relayed: bool, + /// The path in use is down: none to the peer answers (`paths`), whatever its + /// connection was. + pub(crate) down: bool, /// The round trip on that path, in microseconds, if measured. pub(crate) rtt_us: Option, /// Silent for long (`peers.md`, dormancy). diff --git a/src/transport/wire.rs b/src/transport/wire.rs index 26794a1..0ce8c33 100644 --- a/src/transport/wire.rs +++ b/src/transport/wire.rs @@ -14,7 +14,7 @@ use super::{EncodedMessage, MessageKind, ParsedMessage}; /// The wire format's version, the first byte of every payload. Bump it with every change /// to a message type or a codec (`golden_bytes` fails then). -pub(crate) const VERSION: u8 = 13; +pub(crate) const VERSION: u8 = 14; /// What the second byte of a payload says. fn tag(kind: MessageKind) -> u8 { @@ -437,6 +437,7 @@ pub(crate) fn samples() -> Vec { peer: "p".into(), transport: Some("libp2p".into()), relayed: true, + down: false, rtt_us: Some(1500), dormant: false, others: vec![crate::transport::OtherPath { -- cgit v1.3.1