aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-04 20:10:08 +0100
committertv <tv@krebsco.de>2026-11-04 20:10:08 +0100
commit6487281ad8e33138173cdb934bc6caf9333a3721 (patch)
tree7e950fa3e020a26390708d932225d08089a28375 /src
parentc3faf6ecde8157154a01daa2ddc0ac02e6c761f1 (diff)
slow round back off
Diffstat (limited to 'src')
-rw-r--r--src/node.rs1
-rw-r--r--src/state/mod.rs2
-rw-r--r--src/state/peer.rs3
-rw-r--r--src/swarm/peer/inbound.rs5
-rw-r--r--src/swarm/relay.rs86
5 files changed, 92 insertions, 5 deletions
diff --git a/src/node.rs b/src/node.rs
index b376baa..5bfce31 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -142,6 +142,7 @@ impl Node {
pulling: Default::default(),
announce_pending: Default::default(),
announce_slow: Default::default(),
+ slow_rounds: Default::default(),
reach: Default::default(),
trees: Default::default(),
rtt: Default::default(),
diff --git a/src/state/mod.rs b/src/state/mod.rs
index 53d8178..e127877 100644
--- a/src/state/mod.rs
+++ b/src/state/mod.rs
@@ -154,6 +154,8 @@ pub(crate) struct AppState {
/// The slow round after changes passed on along writers' trees is scheduled
/// (`swarm::relay`).
pub(crate) announce_slow: Arc<std::sync::Mutex<Option<crate::swarm::relay::Pending>>>,
+ /// How long the next slow round waits.
+ pub(crate) slow_rounds: Arc<std::sync::Mutex<crate::swarm::relay::SlowRounds>>,
/// Distances in writers' trees (`swarm::tree`).
pub(crate) trees: Arc<std::sync::Mutex<crate::swarm::tree::Trees>>,
/// Whom we reach, as told our neighbours (`swarm::reach`).
diff --git a/src/state/peer.rs b/src/state/peer.rs
index 32b35fc..8f79886 100644
--- a/src/state/peer.rs
+++ b/src/state/peer.rs
@@ -153,6 +153,8 @@ pub(crate) struct PeerState {
/// with its epoch: once they are ours, it has every change we would announce to it
/// (`swarm::relay`).
pub(crate) last_heads: Option<(transport::Epoch, transport::HeadsSummary)>,
+ /// When we last told it our heads in a slow round (`swarm::relay::SlowRounds`).
+ pub(crate) slow_announced_at: Option<Instant>,
/// Whom it said it reaches (`swarm::reach`), by pair key, and when.
pub(crate) reaches: Option<(Instant, Vec<u64>)>,
/// Whose change the next sync message to it passes on along the writer's tree
@@ -286,6 +288,7 @@ impl PeerState {
syncs_with_us: None,
reported_heads: None,
last_heads: None,
+ slow_announced_at: None,
reaches: None,
tree_writer: None,
left: false,
diff --git a/src/swarm/peer/inbound.rs b/src/swarm/peer/inbound.rs
index cfafbd3..53f1aa3 100644
--- a/src/swarm/peer/inbound.rs
+++ b/src/swarm/peer/inbound.rs
@@ -190,6 +190,11 @@ pub(crate) async fn handle_message(
}
}
transport::ParsedMessage::StatusReply(remote) => {
+ // An answer to our slow round's announcement: a tree missed.
+ let announced = peer_state.lock().await.slow_announced_at.take();
+ if let Some(at) = announced {
+ crate::swarm::relay::slow_round_answered(state, at.elapsed());
+ }
on_status(state, &peer, &remote, reactivated).await;
}
transport::ParsedMessage::Links(request) => {
diff --git a/src/swarm/relay.rs b/src/swarm/relay.rs
index 98d4ca0..58196d3 100644
--- a/src/swarm/relay.rs
+++ b/src/swarm/relay.rs
@@ -26,9 +26,9 @@
//! go at once to the receiver's children in the writer's tree (`swarm::tree`). Then the
//! tree carries them, and the round after `ANNOUNCE_DELAY` goes only to the neighbours
//! the tree leaves out (`Plan::uncovered`: no member they reach comes before them). The
-//! others are told in a slow round (`SLOW_ANNOUNCE_DELAY`): in case their parent missed
-//! the change (it went away, or the nodes' views of the tree differed), coalescing all
-//! that came meanwhile. Changes that don't say whose (pulled, a delta, from a node that
+//! others are told in a slow round (`SLOW_ANNOUNCE_DELAY`, longer while nobody lacks
+//! anything when told: `SlowRounds`): in case their parent missed the change (it went
+//! away, or the nodes' views of the tree differed), coalescing all that came meanwhile. Changes that don't say whose (pulled, a delta, from a node that
//! has no part in the writer's tree) are announced to all, as before.
//!
//! In a full mesh too: the member list makes every member the writer's neighbour, but
@@ -69,8 +69,63 @@ const ANNOUNCE_JITTER: Duration = Duration::from_millis(200);
/// How long the slow round after changes passed on along writers' trees waits: what a
/// tree misses arrives this much later, and while changes keep coming, every neighbour
-/// is told at most this often.
+/// is told at most this often. Doubles with every slow round that nobody lacked
+/// anything of (`SlowRounds`), up to `SLOW_ANNOUNCE_MAX`.
const SLOW_ANNOUNCE_DELAY: Duration = Duration::from_secs(2);
+const SLOW_ANNOUNCE_MAX: Duration = Duration::from_secs(16);
+
+/// A STATUS_REPLY this soon after our slow round's announcement to the peer answers it.
+const SLOW_ANSWER_WITHIN: Duration = Duration::from_secs(5);
+
+/// How long the slow round waits. Since a neighbour answers an announcement only if it
+/// lacks something (`swarm::peer::inbound`), an answer to the slow round means a tree
+/// missed: then it is back to `SLOW_ANNOUNCE_DELAY`. While nobody answers, the trees
+/// work, and the round waits twice as long each time, up to `SLOW_ANNOUNCE_MAX`: it is
+/// a safety net, and most of its announcements go to neighbours that have everything.
+/// (A lost message along a tree is noticed sooner by the next one, which its receiver
+/// can't apply and asks after; the slow round is for the last of a run of writes, a
+/// parent gone away, views of the tree that differ.)
+pub(crate) struct SlowRounds {
+ delay: Duration,
+ /// An answer came since the last slow round was scheduled.
+ answered: bool,
+}
+
+impl Default for SlowRounds {
+ fn default() -> Self {
+ // The first round is the short one.
+ Self {
+ delay: SLOW_ANNOUNCE_DELAY,
+ answered: true,
+ }
+ }
+}
+
+impl SlowRounds {
+ /// The wait for the slow round being scheduled now: as long as the last if it was
+ /// answered (or the first), else twice that (up to the most).
+ fn next(&mut self) -> Duration {
+ self.delay = if std::mem::take(&mut self.answered) {
+ SLOW_ANNOUNCE_DELAY
+ } else {
+ (self.delay * 2).min(SLOW_ANNOUNCE_MAX)
+ };
+ self.delay
+ }
+}
+
+/// A STATUS_REPLY from `peer`, which we told of our heads in a slow round
+/// `announced_ago`: it lacked something, so a tree missed; the next slow round waits
+/// `SLOW_ANNOUNCE_DELAY` again.
+pub(crate) fn slow_round_answered(state: &AppState, announced_ago: Duration) {
+ if announced_ago < SLOW_ANSWER_WITHIN {
+ let mut rounds = state.slow_rounds.lock_or_recover();
+ if !rounds.answered {
+ debug!("A slow round's announcement was answered; next one in 2 s");
+ }
+ rounds.answered = true;
+ }
+}
/// An announcement round scheduled: for changes from these peers, and the neighbours
/// that were sent them along a writer's tree meanwhile (`swarm::tree`).
@@ -139,7 +194,8 @@ pub(crate) async fn announce(state: &AppState, from: &str, writers: &[u64], unau
schedule
};
if schedule {
- round(state, &state.announce_slow, SLOW_ANNOUNCE_DELAY, true);
+ let delay = state.slow_rounds.lock_or_recover().next();
+ round(state, &state.announce_slow, delay, true);
}
}
@@ -177,6 +233,9 @@ async fn announce_now(state: &AppState, pending: &Pending, slow: bool) {
}
state.stats.announce_round(pending.all, slow);
for peer in neighbours {
+ if slow && let Some(peer_state) = state.get_peer_state(&peer).await {
+ peer_state.lock().await.slow_announced_at = Some(Instant::now());
+ }
if let Err(error) = announce_heads(state, &peer, false).await {
debug!(peer = %peer, error = %format!("{error:#}"), "Announcing our heads failed");
}
@@ -383,3 +442,20 @@ async fn pull_next(state: &AppState) {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ /// The slow round waits twice as long after each one nobody answered, up to the
+ /// most, and the short time again after an answer.
+ #[test]
+ fn slow_rounds_back_off_while_unanswered() {
+ let mut rounds = SlowRounds::default();
+ let waits: Vec<u64> = (0..6).map(|_| rounds.next().as_secs()).collect();
+ assert_eq!(waits, [2, 4, 8, 16, 16, 16]);
+ rounds.answered = true;
+ assert_eq!(rounds.next(), SLOW_ANNOUNCE_DELAY);
+ assert_eq!(rounds.next(), SLOW_ANNOUNCE_DELAY * 2);
+ }
+}