aboutsummaryrefslogtreecommitdiffstats
path: root/src/state
diff options
context:
space:
mode:
Diffstat (limited to 'src/state')
-rw-r--r--src/state/peer.rs52
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() {