aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-03 02:00:58 +0100
committertv <tv@krebsco.de>2026-11-03 02:00:58 +0100
commitadecfcff1a818cb6d80fd8e24412830ef0988e65 (patch)
treeeca30977e4308db344eeb4231efa8959e3199355
parent02989b6a22a7bbd2151d6a4ea4fe6dddc9f2995b (diff)
probe list members
-rw-r--r--docs/peers.md4
-rw-r--r--src/state/peer.rs11
-rw-r--r--src/swarm/peer/handshake.rs11
-rw-r--r--src/swarm/peer/worker.rs2
-rw-r--r--src/swarm/topology/mod.rs10
5 files changed, 28 insertions, 10 deletions
diff --git a/docs/peers.md b/docs/peers.md
index 3b107bf..4f10f6f 100644
--- a/docs/peers.md
+++ b/docs/peers.md
@@ -118,7 +118,9 @@ Retries: an unanswered HELLO is resent after 1 s, doubling up to 30 s. To an
unreachable peer (dormant, or three unanswered HELLOs) it is a probe: after
`dormant_probe`, doubling up to 6 × `dormant_probe` (or `--dormant-probe-max`, which is
every wait if shorter); members get probes even when they
-aren't neighbours. A member a HELLO_REPLY lists among the sender's active neighbours is
+aren't neighbours: those in the member list, as well as those whose HELLO_REPLY said so.
+After a restart none has answered yet, and members that restart together may each fail
+to reach the others at first (not up yet): without probes, none would try again. A member a HELLO_REPLY lists among the sender's active neighbours is
probed at once: the sender reaches it, so after a partition the first pair to find each
other again brings the others' probes forward, and the sides reconnect within seconds
rather than pair by pair. So are all unreachable members once this node becomes
diff --git a/src/state/peer.rs b/src/state/peer.rs
index 063b72b..ba8379a 100644
--- a/src/state/peer.rs
+++ b/src/state/peer.rs
@@ -146,7 +146,8 @@ pub(crate) struct PeerState {
pub(crate) hello_attempts: u32,
/// Since when we consider the peer unreachable (three unanswered HELLOs, or
/// dormant). Only a message from it clears it (`mark_seen`): until then it is no
- /// neighbour candidate, and a member gets probes (`probe_backoff`) instead.
+ /// neighbour candidate, and a member (by its HELLO_REPLY, or by the member list) gets
+ /// probes (`probe_backoff`) instead.
pub(crate) unreachable_since: Option<Instant>,
/// What went wrong with this peer so far (counted only, for /peers and /admin/stats).
@@ -352,8 +353,12 @@ impl PeerState {
/// No more HELLO retries (the peer isn't our neighbour any more). A HELLO in flight
/// finishes; a later handshake starts over. An unreachable member keeps its probes:
/// it is no neighbour because it is unreachable, and only its answer can change that.
- pub(crate) fn stop_hello_retries(&mut self) {
- let probed = self.is_unreachable() && matches!(self.standing, Standing::Member { .. });
+ /// A member is one whose HELLO_REPLY said so, or one `listed` in the member list: after
+ /// a restart, none has answered yet, and members that restart together may each fail
+ /// to reach the others at first; without probes, none would try again.
+ pub(crate) fn stop_hello_retries(&mut self, listed: bool) {
+ let probed =
+ self.is_unreachable() && (listed || matches!(self.standing, Standing::Member { .. }));
if !probed && matches!(self.hello_state, HelloState::AwaitingReply { .. }) {
self.hello_state = HelloState::NotSent;
}
diff --git a/src/swarm/peer/handshake.rs b/src/swarm/peer/handshake.rs
index 072545b..1ca955b 100644
--- a/src/swarm/peer/handshake.rs
+++ b/src/swarm/peer/handshake.rs
@@ -330,15 +330,16 @@ pub(crate) async fn probe_when_reachable(state: AppState, reachable: Arc<tokio::
/// Brings `peer`'s next probe forward to now, if it is a member we can't reach (dormant,
/// or unanswered HELLOs) whose probe is pending: the peer worker sends it (woken for
-/// it). Whether it was.
+/// it). A member by its HELLO_REPLY, or by the member list (`stop_hello_retries`).
+/// Whether it was.
async fn probe_now(state: &AppState, peer: &str) -> bool {
let Some(peer_state) = state.get_peer_state(peer).await else {
return false;
};
let mut peer_state = peer_state.lock().await;
- if !matches!(peer_state.standing, Standing::Member { .. })
- || !(peer_state.liveness.is_dormant() || peer_state.is_unreachable())
- {
+ let member =
+ matches!(peer_state.standing, Standing::Member { .. }) || topology::is_listed(state, peer);
+ if !member || !(peer_state.liveness.is_dormant() || peer_state.is_unreachable()) {
return false;
}
match &mut peer_state.hello_state {
@@ -372,7 +373,7 @@ pub(crate) async fn apply_neighbour_change(state: &AppState, change: topology::N
for peer in &change.removed {
if let Some(peer_state) = state.get_peer_state(peer).await {
let mut peer_state = peer_state.lock().await;
- peer_state.stop_hello_retries();
+ peer_state.stop_hello_retries(topology::is_listed(state, peer));
peer_state.reset_sync();
}
}
diff --git a/src/swarm/peer/worker.rs b/src/swarm/peer/worker.rs
index 6196571..a803922 100644
--- a/src/swarm/peer/worker.rs
+++ b/src/swarm/peer/worker.rs
@@ -99,7 +99,7 @@ pub(crate) async fn run_peer_worker(state: AppState) {
None
};
if !neighbour {
- peer_state.stop_hello_retries();
+ peer_state.stop_hello_retries(topology::is_listed(&state, peer));
}
let unanswered = peer_state.take_overdue_sync_reply(now);
(became_dormant, keepalive, unanswered)
diff --git a/src/swarm/topology/mod.rs b/src/swarm/topology/mod.rs
index 992c448..e5dacdd 100644
--- a/src/swarm/topology/mod.rs
+++ b/src/swarm/topology/mod.rs
@@ -113,6 +113,16 @@ pub(crate) fn summary(state: &AppState) -> Option<Summary> {
})
}
+/// Whether `peer` is in the member list the neighbours were computed from (false until
+/// the view is known).
+pub(crate) fn is_listed(state: &AppState, peer: &str) -> bool {
+ state
+ .neighbours
+ .read_or_recover()
+ .as_ref()
+ .is_some_and(|view| view.members.iter().any(|member| member == peer))
+}
+
/// Whether we initiate contact with `peer`: HELLOs, keepalives, dormancy checks, sync
/// openings. Everyone is a neighbour until the view is known (while joining, or before
/// any member registered), so joining works as in a full mesh. We always *answer*