aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/dashboard.md8
-rw-r--r--docs/openapi.yaml5
-rw-r--r--src/api/dashboard/clocks.rs2
-rw-r--r--src/api/dashboard/dashboard.html36
-rw-r--r--src/api/dashboard/mod.rs2
-rw-r--r--src/swarm/links.rs24
-rw-r--r--src/transport/mod.rs4
-rw-r--r--src/transport/wire.rs4
8 files changed, 74 insertions, 11 deletions
diff --git a/docs/dashboard.md b/docs/dashboard.md
index 1fe51dc..70397a0 100644
--- a/docs/dashboard.md
+++ b/docs/dashboard.md
@@ -99,6 +99,9 @@ else the fresher. Then:
* **Dotted**: one end reports no links: what is drawn may be out of date.
* **Narrow, over a red band**: either end holds the other dormant, or the path in use
is down (no path to the peer answers; `/peers` shows its connection as `null`).
+* **A chevron on the line in use**: the link is eager that way ([`peers.md`](peers.md)):
+ the end it points away from sends the other its changes as soon as it applies them,
+ rather than announcing them; one each way if both. Shown if either end says so.
Messages this node exchanges run along its links as dots, from sender to receiver, all
at one speed: over a longer link they arrive later.
@@ -327,7 +330,7 @@ another version of it (the state's `page`).
member (`swarm::topology`), as this node computes them. The measured one is every
member's links as it reports them in reply to a LINKS (sent to every member, at most
512, while the producer runs). The LINKS subscribes: the member replies again whenever
-its links change (a peer, a transport, relayed, dormant, or a round trip moved by a
+its links change (a peer, a transport, relayed, dormant, eager, or a round trip moved by a
fifth; it looks every 2 s), for 90 s, renewed every 30 s (each renewal answered), and
ended when the producer stops; a node that is gone stops it by not renewing. A reply
older than 90 s is left out. A member keeps 64 subscribers at most. With `links: every
@@ -364,7 +367,8 @@ every way has failed, all are tried again. Every hop does the same for what it a
The links a member reports: its neighbours and the peers it holds dormant (a neighbour
that goes dormant is one no more, but stays listed until it is heard from again), by
which transport, whether only through a relay, the round trip (as the paths layer
-measures it), whether it holds the peer dormant or the path is down, the peer's other
+measures it), whether it holds the peer dormant or the path is down, which way the link
+is eager, the peer's other
transports with a path up (each with its fastest path's round trip, and whether it goes
only through a relay), and the peer's clock against the member's ([`measurements.md`](measurements.md),
Per peer, `clock`). A member answers a LINKS from members only, at most every 5 s, and
diff --git a/docs/openapi.yaml b/docs/openapi.yaml
index c925dde..8205845 100644
--- a/docs/openapi.yaml
+++ b/docs/openapi.yaml
@@ -753,8 +753,9 @@ paths:
those of the member list, and any a link reported goes to: `alias`, `self`,
`dashboard`: its `--dashboard-url`), `intended` (by node ID: the neighbours the member list gives
it, as this node computes them), `measured` (by node ID: `secsAgo` and
- `links`, each with `peer`, `transport`, `relayed`, `down`, `rttMs`, `dormant` and
- `others` (the peer's other transports: `transport`, `rttMs`, `relayed`), as the member
+ `links`, each with `peer`, `transport`, `relayed`, `down`, `rttMs`, `dormant`,
+ `eagerTo` and `eagerFrom` (the member sends the peer its changes as it applies
+ them, and the peer the member: eager links) and `others` (the peer's other transports: `transport`, `rttMs`, `relayed`), as the member
answered a LINKS; ours always, the others' while fresh) and `live` (whether the
members' links are subscribed to, `/admin/dashboard/links`). One producer
per node makes it every 2 s while anyone looks (and for a minute after this
diff --git a/src/api/dashboard/clocks.rs b/src/api/dashboard/clocks.rs
index 9f069be..ac58676 100644
--- a/src/api/dashboard/clocks.rs
+++ b/src/api/dashboard/clocks.rs
@@ -99,6 +99,8 @@ mod tests {
down: false,
rtt_us: None,
dormant: false,
+ eager_to: false,
+ eager_from: false,
others: Vec::new(),
clock: Some(ClockOffset {
offset_ms,
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index 9132275..1b442ae 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -161,6 +161,9 @@
.legend .dormant::before { border-top-width: 1.5px; --colour: var(--muted);
box-shadow: 0 0 0 2.5px color-mix(in srgb, var(--bad) 50%, transparent); }
.legend .silent::before { border-top-style: dotted; --colour: var(--muted); }
+ /* A chevron on the line, as on the map. */
+ .legend .eager::before { content: "\203A"; width: auto; height: auto; border: none;
+ font-weight: bold; font-size: 15px; line-height: 0; }
.legend .hint::before { display: none; }
/* A kind of message: a dot in its colour, as on the map. */
.legend .message::before { width: 8px; height: 8px; border: none; border-radius: 50%;
@@ -607,6 +610,7 @@ function renderLegend(measured) {
el("span", { class: "direct" }, "direct"),
el("span", { class: "relayed" }, "relayed"),
el("span", { class: "dormant" }, "dormant or down"),
+ el("span", { class: "eager" }, "eager (changes sent on at once, this way)"),
el("span", { class: "silent" }, "an end reports no links"));
document.getElementById("legend").replaceChildren(...items);
}
@@ -637,6 +641,18 @@ function edges() {
}
}
}
+ // Eager, which way, if either end says so: "a>b" where a sends b its changes as it
+ // applies them (a's eagerTo, or b's eagerFrom).
+ const eager = new Set();
+ for (const [a, report] of Object.entries(state.graph.measured)) {
+ for (const link of report.links) {
+ if (link.eagerTo) eager.add(a + ">" + link.peer);
+ if (link.eagerFrom) eager.add(link.peer + ">" + a);
+ }
+ }
+ for (const edge of measured.values()) {
+ edge.eager = [eager.has(edge.a + ">" + edge.b), eager.has(edge.b + ">" + edge.a)];
+ }
// 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.down && !edge.link.down)) {
@@ -954,7 +970,7 @@ function renderGraph() {
tooltip(hit(key, p, q, 1), name(a) + " – " + name(b) + ": intended (member list), not measured");
}
const ms = rtt => rtt < 10 ? rtt.toFixed(1) : String(Math.round(rtt));
- for (const [key, { a, b, link, secsAgo }] of measured) {
+ for (const [key, { a, b, link, secsAgo, eager }] of measured) {
const p = positions.get(a), q = positions.get(b);
if (!p || !q) continue;
// The transport in use thick, in the middle; the others thin, beside it.
@@ -1004,6 +1020,8 @@ function renderGraph() {
(line.transport ? " via " + line.transport : "") + (line.used ? " (in use)" : "") +
((line.used ? link.relayed : line.relayed) ? ", relayed" : "") + (link.dormant ? ", dormant" : "") +
(line.used && link.down ? ", down" : "") +
+ (line.used && eager[0] ? ", eager " + name(a) + " → " + name(b) : "") +
+ (line.used && eager[1] ? ", eager " + name(b) + " → " + name(a) : "") +
(silent ? ", one end reports no links" : "") +
(typeof line.rttMs === "number" ? ", " + line.rttMs.toFixed(1) + " ms" : "") +
", measured " + age(secsAgo + elapsed()) + " ago";
@@ -1027,6 +1045,22 @@ function renderGraph() {
}
});
children.push(...drawnLines.reverse());
+ // Eager, each way it is: a chevron on the line in use pointing that way, past the
+ // middle towards the end it goes to (both ways: one each side of it).
+ const ux = (q.x - p.x) / length, uy = (q.y - p.y) / length;
+ [[eager[0], 1], [eager[1], -1]].forEach(([on, way]) => {
+ if (!on) return;
+ const t = eager[0] && eager[1] ? .5 + way * .06 : .5;
+ const mx = p.x + (q.x - p.x) * t, my = p.y + (q.y - p.y) * t;
+ const dx = ux * way * 5, dy = uy * way * 5;
+ const d = "M " + (mx - dx + nx * 5) + " " + (my - dy + ny * 5) + " L " + (mx + dx) +
+ " " + (my + dy) + " L " + (mx - dx - nx * 5) + " " + (my - dy - ny * 5);
+ const shape = { d, fill: "none", "stroke-linecap": "round", "stroke-linejoin": "round" };
+ children.push(drawing("eager under " + key + " " + way, "path",
+ { ...shape, stroke: "var(--panel)", "stroke-width": 5 }));
+ children.push(drawing("eager " + key + " " + way, "path",
+ { ...shape, stroke: colourOf(link.transport), "stroke-width": 2 }));
+ });
tooltip(covering, descriptions.join("\n"));
}
// Above every line, so none crosses them.
diff --git a/src/api/dashboard/mod.rs b/src/api/dashboard/mod.rs
index 8d954c0..14464d4 100644
--- a/src/api/dashboard/mod.rs
+++ b/src/api/dashboard/mod.rs
@@ -407,6 +407,8 @@ fn links_json(links: &[Link]) -> JsonValue {
"down": link.down,
"rttMs": link.rtt_us.map(|us| f64::from(us) / 1000.0),
"dormant": link.dormant,
+ "eagerTo": link.eager_to,
+ "eagerFrom": link.eager_from,
"clock": link.clock,
"others": link.others.iter().map(|other| json!({
"transport": other.transport,
diff --git a/src/swarm/links.rs b/src/swarm/links.rs
index c430d58..e0d483d 100644
--- a/src/swarm/links.rs
+++ b/src/swarm/links.rs
@@ -1,8 +1,9 @@
//! How members are linked, for a map of the swarm (the dashboard, `api::dashboard`): a
//! node asks a member with LINKS, and the member answers with a LINKS_REPLY listing its
//! neighbours and the peers it holds dormant (`own_links`), each with the transport its
-//! messages go over, whether only through a relay, the round trip and whether it is
-//! dormant: as the member measures them, which no other node can.
+//! messages go over, whether only through a relay, the round trip, whether it is
+//! dormant, and which way the link is eager (`swarm::relay`): as the member measures
+//! them, which no other node can.
//!
//! Every node answers, whatever its build: one with a dashboard asks any member, which
//! may have none. Only members (in the member list) are answered, as the links name
@@ -10,8 +11,8 @@
//! is written to the document: asked for while someone looks, forgotten after.
//!
//! A LINKS may subscribe (`subscribe_secs`): the member then sends a LINKS_REPLY again
-//! whenever its links change (`changed`: a peer, a transport, relayed, dormant, or a
-//! round trip that moved by a fifth), looked at every `WATCH_EVERY` while anyone
+//! whenever its links change (`changed`: a peer, a transport, relayed, dormant, eager,
+//! or a round trip that moved by a fifth), looked at every `WATCH_EVERY` while anyone
//! subscribes (round trips change without telling), until the subscription runs out
//! (the asker renews it with its next LINKS, so one that is gone stops it by not
//! renewing) or a LINKS with 0 ends it.
@@ -231,7 +232,14 @@ impl Links {
pub(crate) async fn own_links(state: &AppState) -> Vec<Link> {
let mut links = Vec::new();
for (peer, peer_state) in state.peers_snapshot().await {
- let dormant = peer_state.lock().await.liveness.is_dormant();
+ let (dormant, eager_to, eager_from) = {
+ let peer_state = peer_state.lock().await;
+ (
+ peer_state.liveness.is_dormant(),
+ peer_state.eager_to,
+ peer_state.eager_from,
+ )
+ };
if !dormant && !topology::is_neighbour(state, &peer) {
continue;
}
@@ -256,6 +264,8 @@ pub(crate) async fn own_links(state: &AppState) -> Vec<Link> {
.rtt(&peer)
.map(|rtt| u32::try_from(rtt.as_micros()).unwrap_or(u32::MAX)),
dormant,
+ eager_to,
+ eager_from,
clock: state.transport.clock_offset(&peer),
peer,
});
@@ -1010,6 +1020,8 @@ fn changed(old: &[Link], new: &[Link]) -> bool {
|| a.relayed != b.relayed
|| a.down != b.down
|| a.dormant != b.dormant
+ || a.eager_to != b.eager_to
+ || a.eager_from != b.eager_from
|| moved(a.rtt_us, b.rtt_us)
|| clock_moved(a.clock, b.clock)
|| a.others.len() != b.others.len()
@@ -1062,6 +1074,8 @@ mod tests {
down: false,
rtt_us,
dormant: false,
+ eager_to: false,
+ eager_from: false,
others: Vec::new(),
clock: None,
}
diff --git a/src/transport/mod.rs b/src/transport/mod.rs
index 7f1aa98..22782ef 100644
--- a/src/transport/mod.rs
+++ b/src/transport/mod.rs
@@ -469,6 +469,10 @@ pub(crate) struct Link {
pub(crate) rtt_us: Option<u32>,
/// Silent for long (`peers.md`, dormancy).
pub(crate) dormant: bool,
+ /// Eager links (`swarm::relay`): the member sends the peer its changes as it applies
+ /// them (the peer asked), and the peer the member (the member asked).
+ pub(crate) eager_to: bool,
+ pub(crate) eager_from: bool,
/// The peer's other transports (`swarm::links::MAX_OTHERS` at most): where else
/// it proved itself, those paths up.
pub(crate) others: Vec<OtherPath>,
diff --git a/src/transport/wire.rs b/src/transport/wire.rs
index 40976ee..fe690a2 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 = 17;
+pub(crate) const VERSION: u8 = 18;
/// What the second byte of a payload says.
fn tag(kind: MessageKind) -> u8 {
@@ -445,6 +445,8 @@ pub(crate) fn samples() -> Vec<ParsedMessage> {
down: false,
rtt_us: Some(1500),
dormant: false,
+ eager_to: true,
+ eager_from: false,
others: vec![crate::transport::OtherPath {
transport: "mycelium".into(),
rtt_us: None,