From da8340c7c1da0115189afe159f013557461b6a91 Mon Sep 17 00:00:00 2001 From: tv Date: Mon, 26 Oct 2026 04:32:36 +0100 Subject: split peer module --- src/swarm/peer/epoch.rs | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/swarm/peer/epoch.rs (limited to 'src/swarm/peer/epoch.rs') diff --git a/src/swarm/peer/epoch.rs b/src/swarm/peer/epoch.rs new file mode 100644 index 0000000..56711b1 --- /dev/null +++ b/src/swarm/peer/epoch.rs @@ -0,0 +1,96 @@ +//! The epochs peers advertise: recorded per peer, late messages aside, and compared +//! with ours. + +use tokio::time::Instant; +use tracing::{debug, info, warn}; + +use crate::document::replica::{Epoch, EpochRelation}; +use crate::state::AppState; +use crate::swarm::sync::SyncEvent; +use crate::transport; + +/// Records the epoch a peer advertised and compares it with ours. Returns true when the +/// message's epoch is ours, the only case in which its documents may be synced. +/// +/// `seq` is the peer's `epoch_seq` with it. A message with a lower one than we have was +/// sent before the peer's last epoch change and delivered late: its epoch isn't +/// recorded, so it can't flip our view of the peer back. The count starts over when the +/// peer restarts, which its HELLO or HELLO_REPLY tells by a new run (`run`). +pub(super) async fn observe_peer_epoch( + state: &AppState, + peer: &str, + remote: Epoch, + seq: u32, + run: Option, +) -> bool { + let local = state.local_epoch(); + let relation = local.relation_to(&remote); + + let Some(peer_state_mutex) = state.get_peer_state(peer).await else { + return relation == EpochRelation::Same; + }; + + let previous = { + let mut peer_state = peer_state_mutex.lock().await; + if let Some(run) = run.filter(|run| peer_state.run_id != Some(*run)) { + peer_state.run_id = Some(run); + peer_state.epoch_seq = None; + } + if peer_state.epoch_seq.is_some_and(|newest| seq < newest) { + debug!( + peer = %peer, + epoch = %remote, + seq, + newest = ?peer_state.epoch_seq, + "Ignoring a late message's epoch" + ); + return relation == EpochRelation::Same; + } + peer_state.epoch_seq = Some(seq); + let previous = peer_state.epoch.replace(remote); + if previous.is_some_and(|previous| previous != remote) { + // The peer switched documents; our sync state describes its old one. Count + // the restart as sync activity, so the keepalive heal doesn't re-handshake + // (resetting both sides again) while this fresh sync is under way. + peer_state.reset_sync(); + peer_state.last_sync_rx = Some(Instant::now()); + // Whether it would sync with us was said about its old epoch. + peer_state.syncs_with_us = None; + } + previous + }; + + // Log transitions only, not every keepalive. + if previous != Some(remote) { + match relation { + EpochRelation::Same => { + if previous.is_some() { + info!(peer = %peer, epoch = %remote, "Peer is on our epoch again; resuming sync"); + let _ = state + .sync_events + .send(SyncEvent::PeerNeedsSync(peer.to_owned())); + } + } + // Expected after a remake: either side switches or catches up. + EpochRelation::PeerAhead | EpochRelation::PeerBehind => { + info!( + peer = %peer, + local_epoch = %local, + peer_epoch = %remote, + relation = ?relation, + "Peer is on another epoch; not syncing with it until both are on the same" + ); + } + EpochRelation::Diverged => { + warn!( + peer = %peer, + local_epoch = %local, + peer_epoch = %remote, + "Peer is on a different epoch of the same generation; not syncing with it" + ); + } + } + } + + relation == EpochRelation::Same +} -- cgit v1.3.1