diff options
| author | tv <tv@krebsco.de> | 2026-11-03 01:19:05 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-03 01:19:05 +0100 |
| commit | c12ce1a67e387474d17419bb252211bf04b2e443 (patch) | |
| tree | 61157a186b9a7c884859e53e8cdea3155e92ded5 /src | |
| parent | 72d05e54de4a051148578d8445ad4527550052ff (diff) | |
always announce new heads
Diffstat (limited to 'src')
| -rw-r--r-- | src/simtest/relay.rs | 53 | ||||
| -rw-r--r-- | src/swarm/relay.rs | 42 | ||||
| -rw-r--r-- | src/swarm/topology/mod.rs | 12 |
3 files changed, 62 insertions, 45 deletions
diff --git a/src/simtest/relay.rs b/src/simtest/relay.rs index 9b016b8..dadeed3 100644 --- a/src/simtest/relay.rs +++ b/src/simtest/relay.rs @@ -64,3 +64,56 @@ async fn changes_reach_members_the_writer_cant_reach_at_once() { .await; } } + +/// One transport, a full mesh by the member list, but the writer x reaches b alone (as +/// one behind NAT may): what x writes reaches c through b, at once, though the member +/// list makes c x's neighbour too. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn changes_reach_members_the_writer_fails_to_reach_at_once() { + let keepalive = Duration::from_secs(5); + let slow = move |config: &mut crate::node::NodeConfig| { + config.timing = Timing::new(keepalive, SYNC_REPLY_TIMEOUT, DELIVERY_LIMIT); + }; + let sim = Sim::with_identity(63); + // Nothing between x and c, either way. + let cut = crate::transport::sim::LinkParams { + loss: 1.0, + ..Default::default() + }; + sim.net.set_link("x", "c", cut.clone()); + sim.net.set_link("c", "x", cut); + let b = sim.start_on_with("b", &["sim"], &[], Start::Genesis, slow); + let c = sim.start_on_with("c", &["sim"], &["b"], Start::Join, slow); + let x = sim.start_on_with("x", &["sim"], &["b"], Start::Join, slow); + sim.wait_ready(&[&b, &c, &x], SOON).await; + let (c_id, x_id) = (c.local_id().to_owned(), x.local_id().to_owned()); + { + let (b, c_id, x_id) = (&b, c_id.clone(), x_id.clone()); + sim.wait_until( + "b knows its neighbours, a full mesh, x and c among them", + SOON, + move || { + let (c_id, x_id) = (c_id.clone(), x_id.clone()); + async move { + topology::view_known(b) + && topology::is_full_mesh(b) + && topology::is_neighbour(b, &c_id) + && topology::is_neighbour(b, &x_id) + } + }, + ) + .await; + } + sim.wait_converged(&[&b, &c, &x], SOON).await; + + write(&x, "y", json!(1)).await; + { + let c = &c; + sim.wait_until( + "c has x's write within a second", + Duration::from_secs(1), + move || async move { read(c, "y").await == Some(json!(1)) }, + ) + .await; + } +} diff --git a/src/swarm/relay.rs b/src/swarm/relay.rs index 5ab93f2..1393d50 100644 --- a/src/swarm/relay.rs +++ b/src/swarm/relay.rs @@ -16,10 +16,11 @@ //! message would announce to every neighbour, and announcements would outnumber the //! data they announce (about 15 to 1 at 250 nodes). //! -//! In a full mesh the writer reaches everyone itself, so nothing is announced, but to -//! the neighbours the writer can't reach: those on none of its transports (a member on -//! Tor alone, reached through one on Tor and libp2p, from a writer on libp2p alone). -//! Whom it reaches is what the member list makes its neighbours (`topology::neighbours_of`). +//! In a full mesh too: the member list makes every member the writer's neighbour, but +//! that says nothing about whether the writer reaches them. One behind NAT may reach a +//! single one of them, one on another transport (on Tor alone) none, and no node can +//! tell which from where it is. A neighbour that has the changes already does nothing +//! with an announcement but answer it. use std::collections::{HashMap, HashSet}; use std::sync::atomic::Ordering; @@ -44,15 +45,7 @@ const ANNOUNCE_DELAY: Duration = Duration::from_millis(200); /// After changes from `from` were applied and our heads changed: tell our other /// neighbours, in the next announcement round. pub(crate) async fn announce(state: &AppState, from: &str) { - // While the view is unknown everyone counts as a neighbour, but that isn't a full - // mesh: the others may not hear from the writer. - if !state.initial_sync_done.load(Ordering::Acquire) - || (topology::view_known(state) - && topology::is_full_mesh(state) - && unreached(state, &HashSet::from([from.to_owned()])) - .await - .is_empty()) - { + if !state.initial_sync_done.load(Ordering::Acquire) { return; } let schedule = { @@ -101,38 +94,19 @@ async fn announce_now(state: &AppState, senders: &HashSet<String>) { /// The neighbours to tell of the changes `senders` sent us. The peer whose changes we /// announce has them already, so with a single sender it is skipped; with several, each -/// lacks the others' changes. In a full mesh, also those every sender is a neighbour of: -/// the writers reached them themselves. +/// lacks the others' changes. async fn unreached(state: &AppState, senders: &HashSet<String>) -> Vec<String> { let skip = match senders.len() { 1 => senders.iter().next().cloned(), _ => None, }; - let full_mesh = topology::view_known(state) && topology::is_full_mesh(state); - // Unknown (a sender not in the member list): nobody counts as reached by it. - let reached: Vec<Option<std::collections::BTreeSet<String>>> = if full_mesh { - senders - .iter() - .map(|sender| topology::neighbours_of(state, sender)) - .collect() - } else { - Vec::new() - }; - let reached_by_all = |peer: &str| { - full_mesh - && reached.iter().zip(senders).all(|(neighbours, sender)| { - sender == peer || neighbours.as_ref().is_some_and(|n| n.contains(peer)) - }) - }; state .peer_states .read() .await .keys() .filter(|peer| { - skip.as_deref() != Some(peer.as_str()) - && topology::is_neighbour(state, peer) - && !reached_by_all(peer) + skip.as_deref() != Some(peer.as_str()) && topology::is_neighbour(state, peer) }) .cloned() .collect() diff --git a/src/swarm/topology/mod.rs b/src/swarm/topology/mod.rs index df75f46..e3479e3 100644 --- a/src/swarm/topology/mod.rs +++ b/src/swarm/topology/mod.rs @@ -67,6 +67,7 @@ pub(crate) fn view_known(state: &AppState) -> bool { } /// Whether every reachable member is our neighbour (also while the view is unknown). +#[cfg(test)] pub(crate) fn is_full_mesh(state: &AppState) -> bool { state .neighbours @@ -141,17 +142,6 @@ pub(crate) fn intended_graph(state: &AppState) -> Option<BTreeMap<String, BTreeS Some(graph) } -/// `member`'s neighbours as the member list makes them, as this node sees it (as in -/// `intended_graph`); None until the view is known, or for one the list doesn't have. -pub(crate) fn neighbours_of(state: &AppState, member: &str) -> Option<BTreeSet<String>> { - let view = state.neighbours.read_or_recover(); - let view = view.as_ref()?; - view.members - .iter() - .any(|listed| listed == member) - .then(|| view.neighbours_of(state.local_id(), member)) -} - /// The transports `member` is on, by the member list (the schemes of its addresses); /// ours as we are on them. None known: empty. pub(crate) fn transports_of(state: &AppState, member: &str) -> BTreeSet<String> { |
