diff options
| author | tv <tv@krebsco.de> | 2026-11-04 05:53:24 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-04 05:53:24 +0100 |
| commit | 9bda1116b34c7bc3dcf38c570ce21b21670f16e8 (patch) | |
| tree | 287a6da96bd9390fb6cb3ddd2cf648ffc0aa0b70 /src/state | |
| parent | aac0d4bafc2f28bbb07bb10e9c8d85b7bea1370e (diff) | |
no heal while both move on
The heal clock now also restarts when our heads and the peer's have both
changed since the last STATUS that showed them differing. While writes
go on, two neighbours's heads differ because of changes still on their
way, and that alone no longer leads to a heal.
Diffstat (limited to 'src/state')
| -rw-r--r-- | src/state/peer.rs | 52 |
1 files changed, 49 insertions, 3 deletions
diff --git a/src/state/peer.rs b/src/state/peer.rs index 0d666e6..ce7a3c3 100644 --- a/src/state/peer.rs +++ b/src/state/peer.rs @@ -121,8 +121,11 @@ pub(crate) struct PeerState { 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). + /// from it in between, nor both documents moving on (None while they match). pub(crate) diverged_since: Option<Instant>, + /// Our heads and the peer's at the latest STATUS that showed them differ + /// (`needs_heal`). + diverged_heads: Option<(transport::HeadsSummary, transport::HeadsSummary)>, /// Epoch the peer advertised in its most recent message (late ones aside: see /// `epoch_seq`). @@ -271,6 +274,7 @@ impl PeerState { last_sync_rx: None, last_resync_at: None, diverged_since: None, + diverged_heads: None, epoch: None, epoch_seq: None, run_id: None, @@ -608,6 +612,7 @@ impl PeerState { ) -> bool { if local == remote { self.diverged_since = None; + self.diverged_heads = None; return false; } @@ -615,9 +620,18 @@ impl PeerState { // 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. + // the clock, and so do both documents moving on since the last STATUS: while + // writes go on, heads differ by what is still on its way, between neighbours that + // exchange no sync messages as much as between others. A pair whose sync is stuck + // shows the same heads on one side at least (that side gets nothing), or once + // the writes stop, on both. + let moved = self + .diverged_heads + .as_ref() + .is_some_and(|(ours, theirs)| ours != local && theirs != remote); + self.diverged_heads = Some((local.clone(), remote.clone())); let noticed = match self.diverged_since { - Some(since) if self.last_sync_rx.is_none_or(|rx| rx <= since) => since, + Some(since) if !moved && self.last_sync_rx.is_none_or(|rx| rx <= since) => since, _ => *self.diverged_since.insert(now), }; @@ -639,6 +653,7 @@ impl PeerState { return false; } self.diverged_since = None; + self.diverged_heads = None; self.incidents.heals += 1; self.last_resync_at = Some(now); @@ -818,6 +833,37 @@ mod tests { assert!(peer.answer_overdue(overdue + timing.unreached_after / 2)); } + /// Heads that differ while both documents move on (writes on their way) don't heal; + /// heads that differ and stay put on one side do, after `resync_after`. + #[test] + fn heals_only_where_heads_stay_put() { + use crate::document::ChangeHash; + use crate::transport::HeadsSummary; + 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 heads = |n: u8| HeadsSummary::of(vec![ChangeHash([n; 32])]); + let start = peer.added_at; + // Both moving on, STATUS after STATUS, for longer than `resync_after`. + for i in 0..10u8 { + let at = start + timing.resync_after * u32::from(i) / 4; + assert!( + !peer.needs_heal(&heads(2 * i + 1), &heads(2 * i + 2), &timing, at), + "both moving on" + ); + } + // Then theirs stay put while ours move on: it gets nothing. + let from = start + timing.resync_after * 3; + assert!(!peer.needs_heal(&heads(100), &heads(50), &timing, from)); + assert!(!peer.needs_heal( + &heads(101), + &heads(50), + &timing, + from + timing.resync_after / 2 + )); + assert!(peer.needs_heal(&heads(102), &heads(50), &timing, from + timing.resync_after)); + } + /// Keepalives are PINGs, every `status_refresh` a STATUS; only when quiet. #[test] fn keepalives_are_pings_and_now_and_then_a_status() { |
