aboutsummaryrefslogtreecommitdiffstats
path: root/src/state/peer.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/peer.rs')
-rw-r--r--src/state/peer.rs182
1 files changed, 160 insertions, 22 deletions
diff --git a/src/state/peer.rs b/src/state/peer.rs
index 923c6d2..063b72b 100644
--- a/src/state/peer.rs
+++ b/src/state/peer.rs
@@ -4,9 +4,10 @@ use std::time::Duration;
use tokio::time::Instant;
-use super::{SyncSession, Timing};
+use super::Timing;
use crate::backoff::Backoff;
use crate::document::replica::Epoch;
+use crate::document::sync::SyncSession;
use crate::transport;
/// How far a peer got into the swarm, as far as we're concerned.
@@ -137,6 +138,10 @@ pub(crate) struct PeerState {
/// How many heads its most recent keepalive listed (None if it sent a digest).
pub(crate) reported_heads: Option<usize>,
+ /// It said farewell (`say_farewell`): what else that run of it still sends while it
+ /// stops doesn't make it active again; a message from another run does (`returned`).
+ pub(crate) left: bool,
+
/// HELLOs sent in the current handshake attempt without hearing anything back.
pub(crate) hello_attempts: u32,
/// Since when we consider the peer unreachable (three unanswered HELLOs, or
@@ -146,8 +151,17 @@ pub(crate) struct PeerState {
/// What went wrong with this peer so far (counted only, for /peers and /admin/stats).
pub(crate) incidents: Incidents,
+
+ /// The largest sync message to it that didn't make it (its send failed, or it went
+ /// unanswered), in bytes, and when: messages about that large go as deltas for a
+ /// while (`swarm::blob::Blobs::delta_threshold_for`), which a slow link takes chunk
+ /// by chunk.
+ whole_failed: Option<(usize, Instant)>,
}
+/// How long a sync message that didn't make it keeps others that large from going whole.
+const WHOLE_FAILED_KEPT: Duration = Duration::from_secs(3600);
+
/// Trouble with a peer, counted since it was first listed. Lost keepalives and refused
/// sends are in the traffic counters (`transport::traffic`): every PONG answers a PING,
/// every STATUS_REPLY a STATUS.
@@ -212,9 +226,11 @@ impl PeerState {
reports_ready: None,
syncs_with_us: None,
reported_heads: None,
+ left: false,
hello_attempts: 0,
unreachable_since: None,
incidents: Incidents::default(),
+ whole_failed: None,
}
}
@@ -233,20 +249,43 @@ impl PeerState {
if self.liveness.is_dormant() || self.hello_state != HelloState::Established {
return None;
}
+ let largest = self.sync.reply.as_ref().map(|wait| wait.largest);
let overdue = self.sync.take_overdue_reply(now);
if overdue.is_some() {
self.incidents.reply_timeouts += 1;
+ if let Some(largest) = largest {
+ self.whole_failed(largest, now);
+ }
}
overdue
}
+ /// A sync message of `bytes` to it didn't make it (`whole_failed`).
+ pub(crate) fn whole_failed(&mut self, bytes: usize, now: Instant) {
+ let kept = self
+ .whole_failed
+ .filter(|(failed, at)| *failed >= bytes && now < *at + WHOLE_FAILED_KEPT);
+ self.whole_failed = kept.or(Some((bytes, now)));
+ }
+
+ /// The largest sync message to it that didn't make it lately, in bytes.
+ pub(crate) fn failed_whole(&self, now: Instant) -> Option<usize> {
+ self.whole_failed
+ .filter(|(_, at)| now < *at + WHOLE_FAILED_KEPT)
+ .map(|(bytes, _)| bytes)
+ }
+
fn quiet_for(&self, now: Instant) -> Duration {
now.saturating_duration_since(self.last_seen.unwrap_or(self.added_at))
}
- /// Records inbound activity. Returns true if the peer was dormant.
+ /// Records inbound activity. Returns true if the peer was dormant. From a peer that
+ /// said farewell (`left`), only that it was heard from.
pub(crate) fn mark_seen(&mut self, now: Instant) -> bool {
self.last_seen = Some(now);
+ if self.left {
+ return false;
+ }
self.hello_attempts = 0;
self.unreachable_since = None;
self.probe_backoff.reset();
@@ -275,11 +314,32 @@ impl PeerState {
}
}
+ /// Whether we sync with it on `epoch`: an active member on that epoch (what our
+ /// STATUS tells it as `syncs_with_you`).
+ pub(crate) fn syncs_on(&self, epoch: Epoch) -> bool {
+ !self.liveness.is_dormant()
+ && matches!(self.standing, Standing::Member { .. })
+ && self.epoch == Some(epoch)
+ }
+
/// Whether to leave the peer out when choosing neighbours (`swarm::topology`).
pub(crate) fn is_unreachable(&self) -> bool {
self.unreachable_since.is_some()
}
+ /// What choosing neighbours reads of this peer (`swarm::topology::refresh`): whether
+ /// it is unreachable, whether it was ever heard from, whether it is a member, and
+ /// whether it syncs with us. A change to any may change the neighbours
+ /// (`swarm::topology::change_peer`).
+ pub(crate) fn neighbour_inputs(&self) -> impl PartialEq + use<> {
+ (
+ self.is_unreachable(),
+ self.last_seen.is_some(),
+ matches!(self.standing, Standing::Member { .. }),
+ self.syncs_with_us,
+ )
+ }
+
/// The wait before the next HELLO: a probe's if unreachable.
pub(crate) fn next_hello_wait(&mut self) -> Duration {
if self.is_unreachable() {
@@ -304,9 +364,32 @@ impl PeerState {
if self.liveness.is_dormant() || self.quiet_for(now) < timing.dormant_after {
return false;
}
+ self.incidents.dormancies += 1;
+ self.go_dormant(now);
+ true
+ }
+
+ /// The peer said farewell (it is shutting down): Dormant now, without waiting for
+ /// its silence, and not counted as an incident. Returns true on the transition.
+ pub(crate) fn say_farewell(&mut self, now: Instant) -> bool {
+ self.left = true;
+ if self.liveness.is_dormant() {
+ return false;
+ }
+ self.go_dormant(now);
+ true
+ }
+
+ /// A message from `run` of the peer (a HELLO's or HELLO_REPLY's): a run other than
+ /// the one that said farewell is back (`left`).
+ pub(crate) fn returned(&mut self, run: crate::transport::RunId) {
+ if self.run_id != Some(run) {
+ self.left = false;
+ }
+ }
+ fn go_dormant(&mut self, now: Instant) {
self.liveness = Liveness::Dormant { since: now };
- self.incidents.dormancies += 1;
self.unreachable_since.get_or_insert(now);
self.reset_sync();
@@ -315,7 +398,6 @@ impl PeerState {
next_attempt: now + self.probe_backoff.next(),
};
}
- true
}
/// The keepalive an established, quiet peer needs, if any (and records it): a PING,
@@ -339,6 +421,26 @@ impl PeerState {
timing: &Timing,
now: Instant,
) -> Option<Keepalive> {
+ if self.keepalive_due_at(timing).is_none_or(|at| now < at) {
+ return None;
+ }
+ self.last_keepalive_at = Some(now);
+ let status = self
+ .last_status_at
+ .is_none_or(|t| now.saturating_duration_since(t) >= timing.status_refresh);
+ Some(if status {
+ Keepalive::Status
+ } else {
+ Keepalive::Ping
+ })
+ }
+
+ /// When the next keepalive is due (`take_keepalive_due`), if any is: none to a
+ /// dormant peer, nor before the handshake.
+ pub(crate) fn keepalive_due_at(&self, timing: &Timing) -> Option<Instant> {
+ if self.liveness.is_dormant() || self.hello_state != HelloState::Established {
+ return None;
+ }
let our_turn = match (self.last_keepalive_at, self.keepalive_received_at) {
(None, None) => self.leads,
(None, Some(_)) => true,
@@ -354,27 +456,44 @@ impl PeerState {
} else {
timing.keepalive + timing.keepalive / 2
};
- let due = !self.liveness.is_dormant()
- && self.hello_state == HelloState::Established
- && now.saturating_duration_since(self.added_at) >= timing.keepalive
- && self.quiet_for(now) >= quiet_enough
- && self
- .last_keepalive_at
- .is_none_or(|t| now.saturating_duration_since(t) >= timing.keepalive);
- if !due {
- return None;
- }
- self.last_keepalive_at = Some(now);
- let status = self
- .last_status_at
- .is_none_or(|t| now.saturating_duration_since(t) >= timing.status_refresh);
- Some(if status {
- Keepalive::Status
- } else {
- Keepalive::Ping
+ let quiet_since = self.last_seen.unwrap_or(self.added_at);
+ let at = (self.added_at + timing.keepalive).max(quiet_since + quiet_enough);
+ Some(match self.last_keepalive_at {
+ Some(sent) => at.max(sent + timing.keepalive),
+ None => at,
})
}
+ /// 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.
+ pub(crate) fn next_due(
+ &self,
+ timing: &Timing,
+ neighbour: bool,
+ ready: bool,
+ ) -> Option<Instant> {
+ let dormancy = (ready && neighbour && !self.liveness.is_dormant())
+ .then(|| self.last_seen.unwrap_or(self.added_at) + timing.dormant_after);
+ let keepalive = if neighbour {
+ self.keepalive_due_at(timing)
+ } else {
+ None
+ };
+ let hello = match self.hello_state {
+ HelloState::AwaitingReply { next_attempt } => Some(next_attempt),
+ _ => None,
+ };
+ 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]
+ .into_iter()
+ .flatten()
+ .min()
+ }
+
/// Self-healing check on a STATUS or STATUS_REPLY. Returns true if a re-handshake
/// should start.
pub(crate) fn needs_heal(
@@ -454,6 +573,25 @@ mod tests {
assert!(silent.liveness.is_dormant());
}
+ /// After its farewell, a peer stays dormant whatever its stopping run still sends;
+ /// its next run makes it active again.
+ #[test]
+ fn dormant_after_farewell_until_back() {
+ let timing = Timing::from_keepalive(Duration::from_secs(60), Duration::from_secs(300));
+ let mut peer = PeerState::new(true, &timing);
+ let now = peer.added_at;
+ peer.run_id = Some([1; 16]);
+ peer.mark_seen(now);
+ assert!(peer.say_farewell(now));
+ assert!(!peer.mark_seen(now), "a straggler from the same run");
+ assert!(peer.liveness.is_dormant() && peer.is_unreachable());
+ peer.returned([1; 16]);
+ assert!(!peer.mark_seen(now), "the same run again");
+ peer.returned([2; 16]);
+ assert!(peer.mark_seen(now), "its next run");
+ assert!(!peer.liveness.is_dormant() && !peer.is_unreachable());
+ }
+
/// The two sides of a pair take turns: each sends every other interval.
#[test]
fn keepalives_take_turns() {