diff options
Diffstat (limited to 'src/state')
| -rw-r--r-- | src/state/host.rs | 30 | ||||
| -rw-r--r-- | src/state/mod.rs | 9 | ||||
| -rw-r--r-- | src/state/peer.rs | 168 | ||||
| -rw-r--r-- | src/state/timing.rs | 18 |
4 files changed, 186 insertions, 39 deletions
diff --git a/src/state/host.rs b/src/state/host.rs index f6c460d..6eab803 100644 --- a/src/state/host.rs +++ b/src/state/host.rs @@ -29,7 +29,7 @@ impl AppState { // Tell peers about the new epoch now instead of with the next keepalive (up to a // keepalive interval later). Ready peers already on it learn it from our sync - // opening, which goes out right away; everyone else gets a PING, so peers that + // opening, which goes out right away; everyone else gets a STATUS, so peers that // never saw the commit start catching up (we don't sync with them, so nothing // else would tell them). let local = self.local_epoch(); @@ -52,7 +52,7 @@ impl AppState { } for id in announce { - if let Err(error) = crate::swarm::peer::send_ping(self, id).await { + if let Err(error) = crate::swarm::peer::send_status(self, id).await { debug!(peer = %id, error = %format!("{error:#}"), "Epoch announcement failed"); } } @@ -161,14 +161,24 @@ impl Host for AppState { fn resume(&self) -> impl Future<Output = ()> + Send { let state = self.clone(); async move { - let mut lifecycle = state.lifecycle.write().await; - if let NodeLifecycle::Joining { - discovered_swarm_id: Some(swarm_id), - } = &*lifecycle - { - let swarm_id = swarm_id.clone(); - debug!(swarm_id = %swarm_id, "Node resumed"); - *lifecycle = NodeLifecycle::Ready { swarm_id }; + let resumed = { + let mut lifecycle = state.lifecycle.write().await; + let swarm_id = match &*lifecycle { + NodeLifecycle::Joining { + discovered_swarm_id: Some(swarm_id), + } => Some(swarm_id.clone()), + _ => None, + }; + if let Some(swarm_id) = &swarm_id { + debug!(swarm_id = %swarm_id, "Node resumed"); + *lifecycle = NodeLifecycle::Ready { + swarm_id: swarm_id.clone(), + }; + } + swarm_id.is_some() + }; + if resumed { + crate::swarm::peer::announce(&state); } } } diff --git a/src/state/mod.rs b/src/state/mod.rs index c8243a2..fec164d 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -24,7 +24,7 @@ mod read; mod session; mod timing; -pub(crate) use peer::{HelloState, Incidents, Liveness, PeerState, Standing}; +pub(crate) use peer::{HelloState, Incidents, Keepalive, Liveness, PeerState, Standing}; pub(crate) use read::{ReadSource, StaleView}; pub(crate) use session::SyncSession; pub(crate) use timing::Timing; @@ -40,7 +40,8 @@ use tokio::sync::{Mutex, RwLock, broadcast, mpsc, watch}; use tokio::time::Instant; use tracing::{debug, error, warn}; -use crate::document::replica::{Epoch, Replica, Stamp}; +use crate::document::lock::DocumentLock; +use crate::document::replica::{Epoch, Stamp}; use crate::locks::MutexExt; use crate::remake::{Host, RemakeState}; use crate::swarm::sync::{ChangeSet, SyncEvent}; @@ -58,7 +59,7 @@ pub(crate) enum NodeLifecycle { #[derive(Clone)] pub(crate) struct AppState { /// The document and its epoch. Derefs to `AutoCommit`. - pub(crate) doc: Arc<RwLock<Replica>>, + pub(crate) doc: Arc<DocumentLock>, /// Copy of `doc.epoch` for cheap, lock-free reads (handshakes, keepalives, /peers). /// Only ever changed while holding `doc`'s write lock, together with `doc.epoch`. @@ -167,7 +168,7 @@ pub(crate) struct CatchUpSource { pub(crate) reply_timeouts: u32, /// When we last warned that catching up from it stalls. pub(crate) last_warned: Option<Instant>, - /// Our last probe PING to it. + /// Our last probe STATUS to it. pub(crate) last_probe: Option<Instant>, /// Its keepalives in a row saying it wouldn't sync with us. pub(crate) refusals: u32, diff --git a/src/state/peer.rs b/src/state/peer.rs index 8086f63..b723ccd 100644 --- a/src/state/peer.rs +++ b/src/state/peer.rs @@ -67,7 +67,7 @@ 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 + /// Whether we lead the pair (`swarm::pair`): we send the first keepalive. Fixed for /// the pair, so computed once. pub(crate) leads: bool, @@ -85,7 +85,12 @@ pub(crate) struct PeerState { pub(crate) liveness: Liveness, pub(crate) added_at: Instant, pub(crate) last_seen: Option<Instant>, - pub(crate) last_ping_at: Option<Instant>, + /// Our last keepalive to it (a PING or a STATUS). + pub(crate) last_keepalive_at: Option<Instant>, + /// Its last keepalive to us (a PING or a STATUS): after it, the next is ours. + pub(crate) keepalive_received_at: Option<Instant>, + /// The last STATUS between us, either way (`status_refresh`). + pub(crate) last_status_at: Option<Instant>, /// Last time a sync message from this peer was applied (survives resets: the /// keepalive heal looks at it). @@ -133,7 +138,8 @@ pub(crate) struct PeerState { } /// 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. +/// sends are in the traffic counters (`transport::traffic`): every PONG answers a PING, +/// every STATUS_REPLY a STATUS. #[derive(Debug, Default, Clone, Copy, serde::Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct Incidents { @@ -157,6 +163,15 @@ impl Incidents { } } +/// What a keepalive due is (`PeerState::take_keepalive_due`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Keepalive { + /// A PING: the peer's transport answers it with a PONG. + Ping, + /// A STATUS: our state, answered with the peer's. + Status, +} + /// Unanswered HELLOs after which a peer counts as unreachable (1 + 2 + 4 s apart). const UNREACHABLE_AFTER_HELLOS: u32 = 3; @@ -172,7 +187,9 @@ impl PeerState { liveness: Liveness::Active, added_at: Instant::now(), last_seen: None, - last_ping_at: None, + last_keepalive_at: None, + keepalive_received_at: None, + last_status_at: None, last_sync_rx: None, last_resync_at: None, diverged_since: None, @@ -287,17 +304,38 @@ impl PeerState { true } - /// True (and records the ping) if an established, quiet peer needs a keepalive. + /// The keepalive an established, quiet peer needs, if any (and records it): a PING, + /// or a STATUS if none went between us for `status_refresh`. + /// + /// A PING and its PONG (or a STATUS and its STATUS_REPLY) tell both sides that the + /// other is alive, so one per interval does for the pair. The two sides take turns: + /// after a keepalive from the peer, the next is ours (the one leading the pair, + /// `leads`, sends the first), so both measure the round trip and each sends every + /// other interval. On our turn we send once the pair was quiet for an interval; on its + /// turn only after half an interval more, in case it doesn't (a keepalive or its + /// answer was lost). Without turns, both sides would get due at about the same time + /// (each one's quiet time starts with the same exchange) and their keepalives would + /// often cross. /// - /// 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 { + /// When they do cross anyway (both sent before seeing the other's: the peer's came + /// within half an interval after ours), each side would take the next turn for its + /// own and they would keep crossing; then the next turn is the leader's. + pub(crate) fn take_keepalive_due( + &mut self, + timing: &Timing, + now: Instant, + ) -> Option<Keepalive> { + let our_turn = match (self.last_keepalive_at, self.keepalive_received_at) { + (None, None) => self.leads, + (None, Some(_)) => true, + (Some(_), None) => false, + (Some(sent), Some(received)) if received > sent => { + let crossed = received.saturating_duration_since(sent) < timing.keepalive / 2; + !crossed || self.leads + } + (Some(_), Some(_)) => false, + }; + let quiet_enough = if our_turn { timing.keepalive } else { timing.keepalive + timing.keepalive / 2 @@ -307,16 +345,24 @@ impl PeerState { && now.saturating_duration_since(self.added_at) >= timing.keepalive && self.quiet_for(now) >= quiet_enough && self - .last_ping_at + .last_keepalive_at .is_none_or(|t| now.saturating_duration_since(t) >= timing.keepalive); - - if due { - self.last_ping_at = Some(now); + if !due { + return None; } - due + 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 + }) } - /// Self-healing check on PING/PONG. Returns true if a re-handshake should start. + /// Self-healing check on a STATUS or STATUS_REPLY. Returns true if a re-handshake + /// should start. pub(crate) fn needs_heal( &mut self, local: &transport::HeadsSummary, @@ -378,6 +424,88 @@ impl PeerState { mod tests { use super::*; + /// The two sides of a pair take turns: each sends every other interval. + #[test] + fn keepalives_take_turns() { + let timing = Timing::from_keepalive(Duration::from_secs(60), Duration::from_secs(300)); + let mut a = PeerState::new(true, &timing); + let mut b = PeerState::new(false, &timing); + a.hello_state = HelloState::Established; + b.hello_state = HelloState::Established; + b.added_at = a.added_at; + let mut senders = Vec::new(); + let mut now = a.added_at; + for _ in 0..40 { + now += Duration::from_secs(5); + // Whoever is due sends; the other one gets it at once, and both hear from + // each other (the keepalive, its answer). + if a.take_keepalive_due(&timing, now).is_some() { + senders.push("a"); + b.keepalive_received_at = Some(now); + } else if b.take_keepalive_due(&timing, now).is_some() { + senders.push("b"); + a.keepalive_received_at = Some(now); + } else { + continue; + } + a.mark_seen(now); + b.mark_seen(now); + } + // 200 s: one keepalive a minute, a first, then taking turns. + assert_eq!(senders, ["a", "b", "a"]); + } + + /// Keepalives that crossed (both sides sent before seeing the other's) leave the next + /// turn to the leader, rather than both taking it and crossing again. + #[test] + fn crossed_keepalives_go_back_to_taking_turns() { + let timing = Timing::from_keepalive(Duration::from_secs(60), Duration::from_secs(300)); + let mut a = PeerState::new(true, &timing); + let mut b = PeerState::new(false, &timing); + let start = a.added_at; + b.added_at = start; + for peer in [&mut a, &mut b] { + peer.hello_state = HelloState::Established; + } + // Both sent at 100 s and got the other's a moment later. + let sent = start + Duration::from_secs(100); + for peer in [&mut a, &mut b] { + peer.last_keepalive_at = Some(sent); + peer.keepalive_received_at = Some(sent + Duration::from_millis(50)); + peer.mark_seen(sent + Duration::from_millis(50)); + } + let next = sent + timing.keepalive + Duration::from_secs(1); + assert!(a.take_keepalive_due(&timing, next).is_some()); // The leader's turn. + assert!(b.take_keepalive_due(&timing, next).is_none()); + } + + /// Keepalives are PINGs, every `status_refresh` a STATUS; only when quiet. + #[test] + fn keepalives_are_pings_and_now_and_then_a_status() { + 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 start = peer.added_at; + assert_eq!(peer.take_keepalive_due(&timing, start), None); // Too new. + let mut kinds = Vec::new(); + for interval in 1..=6 { + let now = start + timing.keepalive * interval; + let due = peer.take_keepalive_due(&timing, now); + if due == Some(Keepalive::Status) { + peer.last_status_at = Some(now); + } + kinds.extend(due); + // Not again within the interval. + assert_eq!(peer.take_keepalive_due(&timing, now), None); + } + use Keepalive::{Ping, Status}; + assert_eq!(kinds, [Status, Ping, Ping, Status, Ping, Ping]); + // Heard from just now: nothing due. + let now = start + timing.keepalive * 7; + peer.mark_seen(now); + assert_eq!(peer.take_keepalive_due(&timing, now), None); + } + /// R41: unreachability lasts until the peer is heard from, not for a set time, and /// probes back off meanwhile. #[test] diff --git a/src/state/timing.rs b/src/state/timing.rs index 0dab3da..9055ed6 100644 --- a/src/state/timing.rs +++ b/src/state/timing.rs @@ -18,10 +18,14 @@ const PROBE_MAX_FACTOR: u32 = 6; #[derive(Clone, Copy, Debug)] pub(crate) struct Timing { pub(crate) keepalive: Duration, + /// How often a quiet pair's keepalive is a STATUS rather than a PING + /// (`PeerState::take_keepalive_due`): its state (heads, readiness, epoch) for the heal + /// and against lost announcements. Changes go out as they happen anyway. + pub(crate) status_refresh: Duration, pub(crate) dormant_after: Duration, pub(crate) resync_after: Duration, pub(crate) dormant_probe: Duration, - /// While catching up: PING the source after this much silence (`swarm::source`). + /// While catching up: send the source a STATUS after this much silence (`swarm::source`). pub(crate) catch_up_probe: Duration, /// First wait for an answer to a sync message before sending again; doubles while /// answers stay out (`SyncSession::reply_backoff`). @@ -42,9 +46,10 @@ impl Timing { /// The timings for `--keepalive`. /// /// Each node keeps about `k` neighbours (`--neighbours`) whatever the swarm's size, - /// and only one side of a pair pings, so idle, a node sends about `k` messages and - /// receives about `k` (PINGs and PONGs) per keepalive interval (more on a stable - /// member others prefer). The interval trades that against how soon a silent peer is + /// and only one side of a pair sends them, so idle, a node sends about `k` messages and + /// receives about `k` per keepalive interval (more on a stable member others prefer): + /// PINGs and PONGs (a few bytes), every third a STATUS and STATUS_REPLY (heads, + /// readiness, epoch) instead. The interval trades that against how soon a silent peer is /// noticed (`dormant_after`, 4 intervals) and a gap closes (within an interval). /// Recommended: /// @@ -68,7 +73,10 @@ impl Timing { ) -> Self { Self { keepalive, - dormant_after: keepalive * 4, // dormant_after >= 4* keepalive to tolerate three lost PINGs in a row + // Every third keepalive; with `resync_after` at two keepalives, a heal needs + // two STATUSes that differ: within about two refreshes. + status_refresh: keepalive * 3, + dormant_after: keepalive * 4, // dormant_after >= 4* keepalive to tolerate three lost keepalives in a row // >= 2 keepalives, so a second exchange confirms the difference; and >= twice // the first sync reply timeout, so a lost message's sender resends it first. resync_after: (keepalive * 2).max(sync_reply_timeout * 2), |
