summaryrefslogtreecommitdiffstats
path: root/src/state.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/state.rs')
-rw-r--r--src/state.rs147
1 files changed, 26 insertions, 121 deletions
diff --git a/src/state.rs b/src/state.rs
index 64fa8ea..3abfd85 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -89,8 +89,8 @@ pub(crate) struct PendingOutbound {
#[derive(Clone)]
pub(crate) struct PeerState {
- /// Whether we lead the pair (`swarm::pair`): we open sync after a reset and send the
- /// keepalive PINGs. Fixed for the pair, so computed once.
+ /// 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,
@@ -138,79 +138,10 @@ pub(crate) struct SyncSession {
/// How long to wait for it; doubles while answers stay out.
pub(crate) reply_timeout: Duration,
- /// Who sends the first message of this session.
- pub(crate) opening: Opening,
-
- /// What started this session.
- pub(crate) cause: SessionCause,
-
/// Consecutive exchanges that changed nothing (livelock detection).
pub(crate) stalled_exchanges: u32,
}
-/// Who opens a sync session. After a reset, usually both sides do it and both would
-/// open at once. The openings cross, and each side answers the other's: harmless between
-/// peers that share history, but an empty peer answers with empty heads, which Automerge
-/// takes for "lost everything", so the whole document goes out a second time. So only
-/// the side that leads the pair opens.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub(crate) enum Opening {
- /// We open: we lead the pair.
- Ours,
- /// We open, leader or not: we just switched epochs, and the peer, already on our new
- /// epoch, still has our old one on record. It won't sync with us until a message
- /// tells it otherwise, and our opening does (no epoch announcement PING needed).
- /// Survives resets until the peer has answered, since only then do we know it
- /// learned our epoch.
- Announce,
- /// The peer opens. If nothing came by `until`, we open anyway: the peer may not have
- /// reset, or its opening got lost.
- Theirs { until: Instant },
-}
-
-/// What started a sync session. Kept for the log: when the peer doesn't open within the
-/// grace period, the cause usually tells why.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub(crate) enum SessionCause {
- /// First session with a peer we just learned about.
- NewPeer,
- /// The peer sent HELLO: it (re)started its handshake with us.
- PeerHello,
- /// Its HELLO_REPLY completed our handshake.
- HandshakeComplete,
- /// The peer advertised another epoch than before.
- PeerEpochChanged,
- /// We switched epochs.
- OwnEpochSwitch,
- /// The peer went dormant.
- PeerDormant,
- /// The peer failed the swarm key check.
- KeyRejected,
-}
-
-impl SessionCause {
- /// The likely reason the peer didn't open a session started this way.
- pub(crate) fn missing_opening_hint(self) -> &'static str {
- match self {
- Self::NewPeer | Self::PeerHello | Self::HandshakeComplete => {
- "the peer may not count us as a member yet (our HELLO_REPLY hasn't reached it), \
- or its opening was lost"
- }
- Self::PeerEpochChanged => {
- "the peer switched epochs but doesn't know we're on its new one yet, or its \
- opening was lost"
- }
- Self::OwnEpochSwitch => {
- "we switched epochs; the peer opens only once it learns that, and it may not \
- have yet"
- }
- Self::PeerDormant | Self::KeyRejected => {
- "only our side reset, so the peer had no reason to open"
- }
- }
- }
-}
-
/// Sync messages we sent and expect an answer to. Automerge assumes reliable delivery:
/// a lost message leaves both sides waiting (`in_flight`), and changes it carried are
/// never sent again (`sent_hashes`). We resend after a timeout instead.
@@ -227,47 +158,27 @@ const SYNC_REPLY_TIMEOUT: Duration = Duration::from_secs(10);
/// Upper bound (Mycelium gives up on a message after five minutes).
const SYNC_REPLY_TIMEOUT_MAX: Duration = Duration::from_secs(300);
-/// How long the side that doesn't open a fresh sync waits for the other's opening
-/// before opening itself (the other one may not have reset, or its message got lost).
-const SYNC_OPENING_GRACE: Duration = Duration::from_secs(3);
-
impl SyncSession {
- pub(crate) fn new(leads: bool, cause: SessionCause) -> Self {
+ pub(crate) fn new() -> Self {
Self {
- cause,
state: sync::State::new(),
outbound: None,
reply: None,
reply_timeout: SYNC_REPLY_TIMEOUT,
- opening: if leads {
- Opening::Ours
- } else {
- Opening::Theirs {
- until: Instant::now() + SYNC_OPENING_GRACE,
- }
- },
stalled_exchanges: 0,
}
}
- /// How much longer to hold back our opening, if at all. Only the first message
- /// counts: once the peer has said anything, we answer as usual.
- pub(crate) fn opening_wait(&self, now: Instant) -> Option<Duration> {
- if self.state.their_heads.is_some() {
- return None;
- }
- match self.opening {
- Opening::Ours | Opening::Announce => None,
- Opening::Theirs { until } => until
- .checked_duration_since(now)
- .filter(|wait| !wait.is_zero()),
- }
- }
-
- /// The peer should have opened by now but hasn't said anything: we open ourselves.
- pub(crate) fn opening_overdue(&self, now: Instant) -> bool {
- self.state.their_heads.is_none()
- && matches!(self.opening, Opening::Theirs { until } if until <= now)
+ /// Whether we leave the first message of this session to the peer.
+ ///
+ /// After a reset, both sides usually open at once, and the openings cross. Between
+ /// peers that share history that costs nothing. But a peer with an empty document
+ /// answers our opening with empty heads, which Automerge takes for "lost
+ /// everything", so the whole document would go out a second time. So a ready node
+ /// never opens towards a peer that isn't ready (one that is still catching up); that
+ /// peer opens itself.
+ pub(crate) fn leave_opening(&self, we_ready: bool, peer_ready: Option<bool>) -> bool {
+ self.state.their_heads.is_none() && we_ready && peer_ready == Some(false)
}
/// A sync message went out; `sent` are the changes it carried for the first time.
@@ -314,7 +225,7 @@ impl PeerState {
leads,
standing: Standing::default(),
hello_state: HelloState::default(),
- sync: SyncSession::new(leads, SessionCause::NewPeer),
+ sync: SyncSession::new(),
liveness: Liveness::Active,
added_at: Instant::now(),
last_seen: None,
@@ -330,13 +241,8 @@ impl PeerState {
/// Start Automerge sync with this peer from scratch. Any envelope generated against
/// the old state is discarded; a stale RetryDelivery event is then a no-op.
- pub(crate) fn reset_sync(&mut self, cause: SessionCause) {
- let announce =
- self.sync.opening == Opening::Announce && self.sync.state.their_heads.is_none();
- self.sync = SyncSession::new(self.leads, cause);
- if announce {
- self.sync.opening = Opening::Announce;
- }
+ pub(crate) fn reset_sync(&mut self) {
+ self.sync = SyncSession::new();
}
/// An overdue sync answer from an established, active peer (see
@@ -374,7 +280,7 @@ impl PeerState {
}
self.liveness = Liveness::Dormant { since: now };
- self.reset_sync(SessionCause::PeerDormant);
+ self.reset_sync();
if !matches!(self.hello_state, HelloState::Sending { .. }) {
self.hello_state = HelloState::AwaitingReply {
@@ -782,22 +688,21 @@ impl AppState {
self.publish_change();
// Tell peers about the new epoch now instead of with the next keepalive (up to a
- // keepalive interval later). Peers already on it learn it from our sync opening,
- // which goes out right away; everyone else gets a PING, so peers that never saw
- // the commit start catching up (we don't sync with them, so nothing else would
- // tell them).
+ // 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
+ // never saw the commit start catching up (we don't sync with them, so nothing
+ // else would tell them).
let local = self.local_epoch();
let mut announce = Vec::new();
for (id, peer_state_mutex) in &peers {
let mut peer_state = peer_state_mutex.lock().await;
// What it said about syncing with us was about our old epoch.
peer_state.syncs_with_us = None;
- let syncs_with_us = peer_state.liveness == Liveness::Active
+ let gets_our_opening = peer_state.liveness == Liveness::Active
&& matches!(peer_state.standing, Standing::Member { .. })
- && peer_state.epoch == Some(local);
- if syncs_with_us {
- peer_state.sync.opening = Opening::Announce;
- } else {
+ && peer_state.epoch == Some(local)
+ && peer_state.reports_ready == Some(true);
+ if !gets_our_opening {
announce.push(id);
}
}
@@ -1067,7 +972,7 @@ impl Host for AppState {
let now = Instant::now();
for peer in state.peer_states.blocking_read().values() {
let mut peer = peer.blocking_lock();
- peer.reset_sync(SessionCause::OwnEpochSwitch);
+ peer.reset_sync();
// A fresh start, not an idle sync: no heal re-handshake for it.
peer.last_sync_rx = Some(now);
}