diff options
Diffstat (limited to 'src/state/peer.rs')
| -rw-r--r-- | src/state/peer.rs | 352 |
1 files changed, 352 insertions, 0 deletions
diff --git a/src/state/peer.rs b/src/state/peer.rs new file mode 100644 index 0000000..e96cd9c --- /dev/null +++ b/src/state/peer.rs @@ -0,0 +1,352 @@ +//! What a node knows about each peer: standing, liveness, handshake, incidents. + +use std::time::Duration; + +use tokio::time::Instant; + +use super::{SyncSession, Timing}; +use crate::backoff::Backoff; +use crate::document::replica::Epoch; +use crate::transport; + +/// How far a peer got into the swarm, as far as we're concerned. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) enum Standing { + /// Nothing proven yet. With a swarm key, everything but a handshake from it is + /// ignored (`swarm::peer::admit`). + #[default] + Stranger, + /// Proved the swarm key in a HELLO; no HELLO_REPLY yet. Only reached with a key. + Admitted, + /// Its HELLO_REPLY named our swarm (and, with a key, proved it): we sync with it. + Member { swarm_id: String }, +} + +impl Standing { + /// The swarm a member belongs to; None before its HELLO_REPLY. + pub(crate) fn swarm_id(&self) -> Option<&str> { + match self { + Self::Member { swarm_id } => Some(swarm_id), + Self::Stranger | Self::Admitted => None, + } + } + + /// Proved the swarm key (meaningful only with one). + pub(crate) fn is_admitted(&self) -> bool { + !matches!(self, Self::Stranger) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Liveness { + Active, + /// No inbound message for Timing::dormant_after. Kept indefinitely, probed slowly. + Dormant { + since: Instant, + }, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub(crate) enum HelloState { + #[default] + NotSent, + /// A send is in flight. + Sending, + /// HELLO was sent (or the send failed); no HELLO_REPLY yet. Retried at + /// `next_attempt` (`PeerState::hello_backoff`, or `dormant_probe` while dormant). + AwaitingReply { next_attempt: Instant }, + /// HELLO_REPLY received. No more retries. + Established, +} + +/// First wait for a HELLO_REPLY before sending the HELLO again; doubles while none +/// comes. +const HELLO_RETRY: Duration = Duration::from_secs(1); +const HELLO_RETRY_MAX: Duration = Duration::from_secs(30); + +#[derive(Clone)] +pub(crate) struct PeerState { + /// Whether we lead the pair (`swarm::pair`): we send the keepalive PINGs. Fixed for + /// the pair, so computed once. + pub(crate) leads: bool, + + pub(crate) standing: Standing, + pub(crate) hello_state: HelloState, + /// Wait between unanswered HELLOs; reset whenever a new handshake starts. + pub(crate) hello_backoff: Backoff, + + /// Everything about the current Automerge sync with this peer; a reset replaces it. + pub(crate) sync: SyncSession, + + pub(crate) liveness: Liveness, + pub(crate) added_at: Instant, + pub(crate) last_seen: Option<Instant>, + pub(crate) last_ping_at: Option<Instant>, + + /// Last time a sync message from this peer was applied (survives resets: the + /// keepalive heal looks at it). + pub(crate) last_sync_rx: Option<Instant>, + + /// Last self-healing re-handshake (cooldown). + pub(crate) last_resync_at: Option<Instant>, + + /// Since when keepalives show the peer's heads differ from ours with no sync message + /// from it in between (None while they match). + pub(crate) diverged_since: Option<Instant>, + + /// Epoch the peer advertised in its most recent message (late ones aside: see + /// `epoch_seq`). + pub(crate) epoch: Option<Epoch>, + /// The peer's `epoch_seq` with `epoch`: how many times it changed epoch in its run. + /// An epoch with a lower one is older than what we have, delivered late. + pub(crate) epoch_seq: Option<u32>, + /// The peer's run, from its HELLO or HELLO_REPLY: a new one starts `epoch_seq` over. + pub(crate) run_id: Option<crate::transport::RunId>, + + /// Whether the peer was ready according to its most recent sync message or keepalive. + pub(crate) reports_ready: Option<bool>, + + /// Whether the peer said, in its most recent keepalive, that it would sync with us. + pub(crate) syncs_with_us: Option<bool>, + + /// How many heads its most recent keepalive listed (None if it sent a digest). + pub(crate) reported_heads: Option<usize>, + + /// 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 + /// dormant). Cleared by any message from it; ignored after `dormant_probe`, so the + /// peer becomes a neighbour candidate again and gets tried once more. + pub(crate) unreachable_since: Option<Instant>, + + /// What went wrong with this peer so far (counted only, for /peers and /admin/stats). + pub(crate) incidents: Incidents, +} + +/// 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. +#[derive(Debug, Default, Clone, Copy, serde::Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub(crate) struct Incidents { + /// Our sync messages left unanswered until the reply timeout (then sent again). + pub(crate) reply_timeouts: u64, + /// Re-handshakes because our documents differed while sync was idle. + pub(crate) heals: u64, + /// Times the peer went dormant. + pub(crate) dormancies: u64, + /// Time spent dormant, in milliseconds, up to its last return (the current + /// dormancy, if any, isn't included). + pub(crate) dormant_ms: u64, +} + +impl Incidents { + pub(crate) fn add(&mut self, other: &Incidents) { + self.reply_timeouts += other.reply_timeouts; + self.heals += other.heals; + self.dormancies += other.dormancies; + self.dormant_ms += other.dormant_ms; + } +} + +/// Unanswered HELLOs after which a peer counts as unreachable (1 + 2 + 4 s apart). +const UNREACHABLE_AFTER_HELLOS: u32 = 3; + +impl PeerState { + pub(crate) fn new(leads: bool, timing: &Timing) -> Self { + Self { + leads, + standing: Standing::default(), + hello_state: HelloState::default(), + hello_backoff: Backoff::new(HELLO_RETRY, HELLO_RETRY_MAX), + sync: SyncSession::new(timing.sync_reply_backoff()), + liveness: Liveness::Active, + added_at: Instant::now(), + last_seen: None, + last_ping_at: None, + last_sync_rx: None, + last_resync_at: None, + diverged_since: None, + epoch: None, + epoch_seq: None, + run_id: None, + reports_ready: None, + syncs_with_us: None, + reported_heads: None, + hello_attempts: 0, + unreachable_since: None, + incidents: Incidents::default(), + } + } + + /// Start Automerge sync with this peer from scratch. A message still being handed to + /// Mycelium was generated against the old state; its outcome is then ignored. + pub(crate) fn reset_sync(&mut self) { + // The same waits for answers, from the start. + let mut reply_backoff = self.sync.reply_backoff; + reply_backoff.reset(); + self.sync = SyncSession::new(reply_backoff); + } + + /// An overdue sync answer from an established, active peer (see + /// `SyncSession::take_overdue_reply`). + pub(crate) fn take_overdue_sync_reply(&mut self, now: Instant) -> Option<usize> { + if self.liveness != Liveness::Active || self.hello_state != HelloState::Established { + return None; + } + let overdue = self.sync.take_overdue_reply(now); + if overdue.is_some() { + self.incidents.reply_timeouts += 1; + } + overdue + } + + 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. + pub(crate) fn mark_seen(&mut self, now: Instant) -> bool { + self.last_seen = Some(now); + self.hello_attempts = 0; + self.unreachable_since = None; + + if let Liveness::Dormant { since } = self.liveness { + self.incidents.dormant_ms += now.saturating_duration_since(since).as_millis() as u64; + self.liveness = Liveness::Active; + // Force a fresh handshake; its reply resets sync on both sides. + if self.hello_state != HelloState::Sending { + self.hello_state = HelloState::NotSent; + } + return true; + } + false + } + + /// A HELLO went out (or failed to). After the third unanswered one, the peer counts + /// as unreachable. + pub(crate) fn hello_sent(&mut self, now: Instant) { + self.hello_attempts += 1; + if self.hello_attempts >= UNREACHABLE_AFTER_HELLOS { + self.unreachable_since.get_or_insert(now); + } + } + + /// Whether to leave the peer out when choosing neighbours (`swarm::topology`). + pub(crate) fn is_unreachable(&self, timing: &Timing, now: Instant) -> bool { + self.unreachable_since + .is_some_and(|since| now.saturating_duration_since(since) < timing.dormant_probe) + } + + /// No more HELLO retries (the peer isn't our neighbour any more). A HELLO in flight + /// finishes; a later handshake starts over. + pub(crate) fn stop_hello_retries(&mut self) { + if matches!(self.hello_state, HelloState::AwaitingReply { .. }) { + self.hello_state = HelloState::NotSent; + } + } + + /// Moves a silent peer to Dormant. Returns true on the transition. + pub(crate) fn check_dormancy(&mut self, timing: &Timing, now: Instant) -> bool { + if self.liveness != Liveness::Active || self.quiet_for(now) < timing.dormant_after { + return false; + } + + self.liveness = Liveness::Dormant { since: now }; + self.incidents.dormancies += 1; + self.unreachable_since.get_or_insert(now); + self.reset_sync(); + + if self.hello_state != HelloState::Sending { + self.hello_state = HelloState::AwaitingReply { + next_attempt: now + timing.dormant_probe, + }; + } + true + } + + /// True (and records the ping) if an established, quiet peer needs a keepalive. + /// + /// One PING and its PONG tell both sides that the other is alive, so only one side of + /// a pair needs to ping: the one leading the pair (`leads`). Each of its PINGs + /// makes the other side's peer look recently heard from, so the other side never + /// pings while it does. If the pings stop, the other side pings once it has heard + /// nothing for half an interval more. Without that, both sides would get due at about + /// the same time (each one's quiet time starts with the same exchange) and their + /// PINGs would often cross. + pub(crate) fn take_keepalive_due(&mut self, timing: &Timing, now: Instant) -> bool { + let quiet_enough = if self.leads { + timing.keepalive + } else { + timing.keepalive + timing.keepalive / 2 + }; + let due = self.liveness == Liveness::Active + && self.hello_state == HelloState::Established + && now.saturating_duration_since(self.added_at) >= timing.keepalive + && self.quiet_for(now) >= quiet_enough + && self + .last_ping_at + .is_none_or(|t| now.saturating_duration_since(t) >= timing.keepalive); + + if due { + self.last_ping_at = Some(now); + } + due + } + + /// Self-healing check on PING/PONG. Returns true if a re-handshake should start. + pub(crate) fn needs_heal( + &mut self, + local: &transport::HeadsSummary, + remote: &transport::HeadsSummary, + timing: &Timing, + now: Instant, + ) -> bool { + if local == remote { + self.diverged_since = None; + return false; + } + + // How long the heads have differed without sync making progress. Measured from + // when we noticed, not from the last sync message: in a quiet swarm that one is + // long past, and the first keepalive after a lost message would heal at once, + // before the sender's reply timeout could resend it. Any sync message restarts + // the clock. + let noticed = match self.diverged_since { + Some(since) if self.last_sync_rx.is_none_or(|rx| rx <= since) => since, + _ => *self.diverged_since.insert(now), + }; + + // While an answer to our last sync message is due, a lost message is the reply + // timeout's to resend; healing is for when neither side has anything outstanding. + if self.liveness != Liveness::Active + || self.hello_state != HelloState::Established + || self.sync.reply.is_some() + { + return false; + } + + let persisted = now.saturating_duration_since(noticed) >= timing.resync_after; + let cooled_down = self + .last_resync_at + .is_none_or(|t| now.saturating_duration_since(t) >= timing.resync_after); + + if !(persisted && cooled_down) { + return false; + } + self.diverged_since = None; + self.incidents.heals += 1; + + self.last_resync_at = Some(now); + self.hello_state = HelloState::NotSent; // establish_peer resets sync on the reply + true + } + + /// Heard from recently. A returning node counts as alive from its first message, + /// before the handshake finishes, so a swarm-wide forget can't make it flap. + pub(crate) fn is_alive(&self, timing: &Timing, now: Instant) -> bool { + self.liveness == Liveness::Active + && self + .last_seen + .is_some_and(|t| now.saturating_duration_since(t) < timing.dormant_after) + } +} |
