aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/state/peer.rs85
-rw-r--r--src/state/timing.rs6
-rw-r--r--src/swarm/peer/worker.rs25
-rw-r--r--src/swarm/reach.rs32
-rw-r--r--src/transport/mod.rs2
5 files changed, 132 insertions, 18 deletions
diff --git a/src/state/peer.rs b/src/state/peer.rs
index ca721b5..a1d40ab 100644
--- a/src/state/peer.rs
+++ b/src/state/peer.rs
@@ -108,6 +108,13 @@ pub(crate) struct PeerState {
pub(crate) keepalive_received_at: Option<Instant>,
/// The last STATUS between us, either way (`status_refresh`).
pub(crate) last_status_at: Option<Instant>,
+ /// Since when we wait for an answer from it (to a keepalive, or to a sync message
+ /// whose answer is overdue) with nothing heard from it since, and whether we asked
+ /// again: halfway to `Timing::unreached_after` a PING (`take_ask_again_due`; a sync
+ /// message whose answer is overdue goes again anyway), so one lost message or answer
+ /// doesn't do it; after it, we don't count it as reached (`swarm::reach`). Cleared by
+ /// any message from it (`mark_seen`).
+ awaiting_answer: Option<(Instant, bool)>,
/// Last time a sync message from this peer was applied (survives resets: the
/// keepalive heal looks at it).
@@ -230,6 +237,7 @@ impl PeerState {
last_keepalive_at: None,
keepalive_received_at: None,
last_status_at: None,
+ awaiting_answer: None,
last_sync_rx: None,
last_resync_at: None,
diverged_since: None,
@@ -270,9 +278,14 @@ impl PeerState {
return None;
}
let largest = self.sync.reply.as_ref().map(|wait| wait.largest);
+ let started = self.sync.reply.as_ref().map(|wait| wait.started.0);
let overdue = self.sync.take_overdue_reply(now);
if overdue.is_some() {
self.incidents.reply_timeouts += 1;
+ if let Some(started) = started {
+ // It goes again now: that is asking again.
+ self.awaiting_answer.get_or_insert((started, true));
+ }
if let Some(largest) = largest {
self.whole_failed(largest, now);
}
@@ -303,6 +316,7 @@ impl PeerState {
/// said farewell (`left`), only that it was heard from.
pub(crate) fn mark_seen(&mut self, now: Instant) -> bool {
self.last_seen = Some(now);
+ self.awaiting_answer = None;
if self.left {
return false;
}
@@ -342,6 +356,32 @@ impl PeerState {
&& self.epoch == Some(epoch)
}
+ /// Whether what we sent it went unanswered, with nothing else heard from it, for
+ /// `Timing::unreached_after` (`awaiting_answer`): then we don't count it as reached
+ /// (`swarm::reach`), long before it would be dormant.
+ pub(crate) fn answer_overdue(&self, timing: &Timing, now: Instant) -> bool {
+ self.awaiting_answer.is_some_and(|(since, _)| {
+ now.saturating_duration_since(since) >= timing.unreached_after
+ })
+ }
+
+ /// Whether to ask an established, active peer again, with a PING, as nothing answered
+ /// us for half of `Timing::unreached_after` (`awaiting_answer`); records it.
+ pub(crate) fn take_ask_again_due(&mut self, timing: &Timing, now: Instant) -> bool {
+ if self.liveness.is_dormant() || self.hello_state != HelloState::Established {
+ return false;
+ }
+ match &mut self.awaiting_answer {
+ Some((since, asked_again))
+ if !*asked_again && now >= *since + timing.unreached_after / 2 =>
+ {
+ *asked_again = true;
+ true
+ }
+ _ => false,
+ }
+ }
+
/// Whether to leave the peer out when choosing neighbours (`swarm::topology`).
pub(crate) fn is_unreachable(&self) -> bool {
self.unreachable_since.is_some()
@@ -452,6 +492,7 @@ impl PeerState {
return None;
}
self.last_keepalive_at = Some(now);
+ self.awaiting_answer.get_or_insert((now, false));
let status = self
.last_status_at
.is_none_or(|t| now.saturating_duration_since(t) >= timing.status_refresh);
@@ -493,8 +534,9 @@ impl PeerState {
/// When the peer worker next has something to do for this peer
/// (`swarm::peer::run_peer_worker`): a keepalive (to a `neighbour`), dormancy (of a
- /// neighbour, once `ready`), a HELLO retry, an overdue sync answer. None if nothing
- /// is due until something changes.
+ /// neighbour, once `ready`), a HELLO retry, an overdue sync answer, asking again for
+ /// an answer and its being overdue (`take_ask_again_due`, `answer_overdue`). None if nothing is due until something
+ /// changes.
pub(crate) fn next_due(
&self,
timing: &Timing,
@@ -515,7 +557,18 @@ impl PeerState {
let reply = (!self.liveness.is_dormant() && self.hello_state == HelloState::Established)
.then(|| self.sync.reply.as_ref().map(|wait| wait.deadline))
.flatten();
- [dormancy, keepalive, hello, reply]
+ let unreached = self
+ .awaiting_answer
+ .map(|(since, asked_again)| {
+ if asked_again {
+ since + timing.unreached_after
+ } else {
+ since + timing.unreached_after / 2
+ }
+ })
+ // Once past, nothing more is due for it.
+ .filter(|at| !self.liveness.is_dormant() && *at > Instant::now());
+ [dormancy, keepalive, hello, reply, unreached]
.into_iter()
.flatten()
.min()
@@ -674,6 +727,32 @@ mod tests {
assert!(b.take_keepalive_due(&timing, next).is_none());
}
+ /// A keepalive nothing answers: asked again halfway, after `unreached_after` not
+ /// reached; any message from the peer, reached again. One lost answer doesn't do it.
+ #[test]
+ fn unanswered_keepalives_stop_counting_as_reached() {
+ let timing = Timing::from_keepalive(Duration::from_secs(60), Duration::from_secs(300));
+ let mut peer = PeerState::new(true, &timing);
+ peer.hello_state = HelloState::Established;
+ let sent = peer.added_at + timing.keepalive;
+ assert!(peer.take_keepalive_due(&timing, sent).is_some());
+ let half = sent + timing.unreached_after / 2;
+ assert!(!peer.take_ask_again_due(&timing, half - Duration::from_millis(1)));
+ assert!(peer.take_ask_again_due(&timing, half));
+ assert!(!peer.take_ask_again_due(&timing, half), "asked again once");
+ assert!(!peer.answer_overdue(&timing, half));
+ // The second ask was answered: reached all along.
+ peer.mark_seen(half + Duration::from_millis(50));
+ assert!(!peer.answer_overdue(&timing, sent + timing.unreached_after));
+ // The next keepalive, and its second ask, go unanswered.
+ let sent = half + timing.keepalive * 2;
+ assert!(peer.take_keepalive_due(&timing, sent).is_some());
+ assert!(peer.take_ask_again_due(&timing, sent + timing.unreached_after / 2));
+ assert!(peer.answer_overdue(&timing, sent + timing.unreached_after));
+ peer.mark_seen(sent + timing.unreached_after * 2);
+ assert!(!peer.answer_overdue(&timing, sent + timing.unreached_after * 2));
+ }
+
/// Keepalives are PINGs, every `status_refresh` a STATUS; only when quiet.
#[test]
fn keepalives_are_pings_and_now_and_then_a_status() {
diff --git a/src/state/timing.rs b/src/state/timing.rs
index af5c0c0..fed48e9 100644
--- a/src/state/timing.rs
+++ b/src/state/timing.rs
@@ -35,6 +35,11 @@ pub(crate) struct Timing {
/// First wait for an answer to a sync message before sending again; doubles while
/// answers stay out (`SyncSession::reply_backoff`).
pub(crate) sync_reply_timeout: Duration,
+ /// How long what we sent a peer may go unanswered, with nothing else heard from it,
+ /// before we don't count it as reached (`swarm::reach`, `PeerState::answer_overdue`):
+ /// twice the first sync reply timeout, so one lost message (or its answer) doesn't
+ /// do it.
+ pub(crate) unreached_after: Duration,
/// How long the transport tries to deliver a message before dropping it
/// (`Transport::delivery_limit`): a peer silent for longer has nothing on the way to
/// us any more.
@@ -93,6 +98,7 @@ impl Timing {
// Only the source gets these, and only while catching up.
catch_up_probe: (keepalive / 30).clamp(Duration::from_secs(1), Duration::from_secs(5)),
sync_reply_timeout,
+ unreached_after: sync_reply_timeout * 2,
delivery_limit,
}
}
diff --git a/src/swarm/peer/worker.rs b/src/swarm/peer/worker.rs
index 45e8d8f..dad0870 100644
--- a/src/swarm/peer/worker.rs
+++ b/src/swarm/peer/worker.rs
@@ -1,8 +1,9 @@
-//! The peer worker: keepalives due, dormancy, HELLO retries and overdue sync replies,
-//! when the earliest of them is due (or something may have made one due sooner,
-//! `Due`); every `FORGET_CHECK_INTERVAL` (and at once when the node becomes ready),
-//! forget records, the member registry, stability, measurements and the state file;
-//! forget records also at once when they changed.
+//! The peer worker: keepalives due, dormancy, HELLO retries, overdue sync replies and
+//! answers (`PeerState::answer_overdue`, `swarm::reach`), when the earliest of them is
+//! due (or something may have made one due sooner, `Due`); every
+//! `FORGET_CHECK_INTERVAL` (and at once when the node becomes ready), forget records,
+//! the member registry, stability, measurements and the state file; forget records
+//! also at once when they changed.
use std::time::Duration;
@@ -80,13 +81,15 @@ pub(crate) async fn run_peer_worker(state: AppState) {
was_ready = ready;
let peers = state.peers_snapshot().await;
+ // A member we said we reach stopped answering: tell our neighbours now.
+ let mut reach_changed = false;
for (peer, peer_state_mutex) in &peers {
let now = Instant::now();
// Only neighbours are kept alive and judged; others we only answer.
let neighbour = topology::is_neighbour(&state, peer);
- let (became_dormant, keepalive, unanswered) = {
+ let (became_dormant, keepalive, unanswered, ask_again) = {
let mut peer_state = peer_state_mutex.lock().await;
let became_dormant = ready
&& neighbour
@@ -102,7 +105,10 @@ pub(crate) async fn run_peer_worker(state: AppState) {
peer_state.stop_hello_retries(topology::is_listed(&state, peer));
}
let unanswered = peer_state.take_overdue_sync_reply(now);
- (became_dormant, keepalive, unanswered)
+ let ask_again = peer_state.take_ask_again_due(&state.timing, now);
+ reach_changed |= peer_state.answer_overdue(&state.timing, now)
+ && crate::swarm::reach::reached_stops_answering(&state, peer);
+ (became_dormant, keepalive, unanswered, ask_again)
};
if let Some(changes) = unanswered {
@@ -128,6 +134,8 @@ pub(crate) async fn run_peer_worker(state: AppState) {
}
keepalive => keepalive,
};
+ // Nothing answered us for a while: once more before it counts as not reached.
+ let keepalive = keepalive.or(ask_again.then_some(Keepalive::Ping));
let sent = match keepalive {
Some(Keepalive::Ping) => send_ping(&state, peer).await,
Some(Keepalive::Status) => send_status(&state, peer).await,
@@ -160,6 +168,9 @@ pub(crate) async fn run_peer_worker(state: AppState) {
if records_changed || periodic {
apply_forget_records(&state, &mut forgets_applied).await;
}
+ if reach_changed && !periodic {
+ crate::swarm::reach::check(&state).await;
+ }
if periodic {
forget_strangers(&state).await;
crate::swarm::members::ensure_registered(&state).await;
diff --git a/src/swarm/reach.rs b/src/swarm/reach.rs
index 901b47c..7bd0f73 100644
--- a/src/swarm/reach.rs
+++ b/src/swarm/reach.rs
@@ -2,13 +2,16 @@
//! member list says.
//!
//! A node knows its own links, and from them whom it reaches: the members it has an
-//! active session with (handshake done, neither dormant nor unreachable). It tells its
+//! active session with (handshake done, neither dormant nor unreachable) that answer
+//! (`PeerState::answer_overdue`: none left a keepalive or sync message unanswered for
+//! `Timing::unreached_after`, asked again halfway, with nothing else heard from it; a
+//! member gone shows within seconds, not after `dormant_after`). It tells its
//! neighbours so in its STATUS (`Status::reaches`, each member by its pair key,
//! `topology::pair_key`, 8 bytes instead of a node ID): in every keepalive STATUS and
-//! STATUS_REPLY, and soon (the peer worker looks every 5 s) to every neighbour when it
-//! changed,
-//! but not in announcements of heads, which go often. So every node knows whom its
-//! neighbours reach, as of their latest STATUS, for as long as the session lasts (a
+//! STATUS_REPLY, and soon to every neighbour when it changed (the peer worker looks
+//! every 5 s, and at once when a member we reach stops answering), but not in
+//! announcements of heads, which go often. So every node knows whom its neighbours
+//! reach, as of their latest STATUS, for as long as the session lasts (a
//! neighbour that goes dormant, or starts anew, says it again). Not for a set time:
//! while a pair exchanges changes it sends no keepalives, and so no STATUS, for as
//! long as the writes go on. Nothing goes into the document, and nobody knows more than
@@ -37,14 +40,17 @@ pub(crate) struct Reach {
published: Option<Vec<u64>>,
}
-/// The pair keys of the members we reach now, sorted: those with an active session.
+/// The pair keys of the members we reach now, sorted: those with an active session
+/// that answer.
async fn measure(state: &AppState) -> Vec<u64> {
+ let now = Instant::now();
let mut reached = Vec::new();
for (peer, peer_state) in state.peers_snapshot().await {
let peer_state = peer_state.lock().await;
if peer_state.hello_state == HelloState::Established
&& !peer_state.liveness.is_dormant()
&& !peer_state.is_unreachable()
+ && !peer_state.answer_overdue(&state.timing, now)
&& matches!(peer_state.standing, Standing::Member { .. })
{
reached.push(topology::pair_key(&peer));
@@ -64,7 +70,8 @@ pub(crate) fn published(state: &AppState) -> Vec<u64> {
.unwrap_or_default()
}
-/// With the peer worker's periodic pass (every 5 s): whom we reach now; if it changed,
+/// With the peer worker's periodic pass (every 5 s), and at once when a member we said
+/// we reach stops answering (`reached_stops_answering`): whom we reach now; if it changed,
/// our neighbours are told at once (a STATUS each, unanswered if the heads are the
/// same). A change that is undone within a pass isn't told.
pub(crate) async fn check(state: &AppState) {
@@ -91,6 +98,17 @@ pub(crate) async fn check(state: &AppState) {
}
}
+/// Whether `peer`, whose answer is overdue (`PeerState::answer_overdue`), is one we
+/// said we reach: then the peer worker checks at once (`check`).
+pub(crate) fn reached_stops_answering(state: &AppState, peer: &str) -> bool {
+ state
+ .reach
+ .lock_or_recover()
+ .published
+ .as_ref()
+ .is_some_and(|keys| keys.contains(&topology::pair_key(peer)))
+}
+
/// A STATUS to `peer` with whom we reach: as an announcement (unanswered if the heads
/// are the same), but with `reaches`.
async fn tell(state: &AppState, peer: &str) -> anyhow::Result<()> {
diff --git a/src/transport/mod.rs b/src/transport/mod.rs
index fb198ec..e27ce2d 100644
--- a/src/transport/mod.rs
+++ b/src/transport/mod.rs
@@ -207,7 +207,7 @@ pub(crate) struct Status {
/// neighbour that has the changes already, which has nothing to tell.
pub(crate) announcing: bool,
/// Whom the sender reaches (`swarm::reach`): the pair keys (`topology::pair_key`)
- /// of the members it has an active session with, sorted. In keepalive STATUSes and
+ /// of the members it has an active session with that answer, sorted. In keepalive STATUSes and
/// their replies, and when it changed; not in announcements of heads (None).
pub(crate) reaches: Option<Vec<u64>>,
}