summaryrefslogtreecommitdiffstats
path: root/src/peer.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/peer.rs')
-rw-r--r--src/peer.rs675
1 files changed, 0 insertions, 675 deletions
diff --git a/src/peer.rs b/src/peer.rs
deleted file mode 100644
index d27cda8..0000000
--- a/src/peer.rs
+++ /dev/null
@@ -1,675 +0,0 @@
-//! Peers: handshake (HELLO / HELLO_REPLY), liveness and keepalives, self-healing,
-//! cluster-wide forget records, and dispatch of inbound messages.
-
-use std::sync::Arc;
-use std::time::Duration;
-
-use anyhow::{anyhow, bail, Context, Result};
-use automerge::AutoCommit;
-use sha2::{Digest, Sha256};
-use tokio::sync::Mutex;
-use tokio::time::{Instant, MissedTickBehavior};
-use tracing::{debug, info, instrument, trace, warn};
-
-use crate::replica::{Epoch, EpochRelation};
-use crate::state::{AppState, HelloState, Liveness, PeerMembership, PeerState};
-use crate::store::{self, StoreError};
-use crate::sync::{handle_sync, SyncEvent};
-use crate::transport::{self, ParsedMessage};
-
-const HELLO_RETRY_INITIAL: Duration = Duration::from_secs(1);
-const HELLO_RETRY_MAX: Duration = Duration::from_secs(30);
-
-const PEER_TICK: Duration = Duration::from_millis(500);
-
-const META_ROOT: &str = "_continuum";
-const FORGET_CHECK_INTERVAL: Duration = Duration::from_secs(5);
-
-// --- forget records ----------------------------------------------------------
-
-fn forgotten_dir() -> Vec<String> {
- vec![META_ROOT.to_owned(), "forgotten".to_owned()]
-}
-
-pub(crate) fn forgotten_path(pk: &str) -> Vec<String> {
- let mut path = forgotten_dir();
- path.push(pk.to_owned());
- path
-}
-
-fn forgotten_peers(doc: &AutoCommit) -> Vec<String> {
- store::keys_at(doc, &forgotten_dir()).unwrap_or_default()
-}
-
-async fn apply_forget_records(state: &AppState) {
- // Before the initial sync we can't judge the records, and we have nothing to delete.
- if !state.is_ready().await {
- return;
- }
-
- let forgotten = forgotten_peers(&state.doc.read().await.doc);
- if forgotten.is_empty() {
- return;
- }
-
- let now = Instant::now();
- let mut cancel = Vec::new();
-
- for pk in forgotten {
- if pk == state.local_id() {
- cancel.push(pk); // we are evidently alive
- continue;
- }
-
- let Some(peer_state_mutex) = state.get_peer_state(&pk).await else {
- continue; // already forgotten here
- };
-
- let alive = peer_state_mutex.lock().await.is_alive(&state.timing, now);
-
- if alive {
- cancel.push(pk);
- } else if state.peer_states.write().await.remove(&pk).is_some() {
- info!(peer = %pk, "Forgetting peer per cluster-wide record");
- }
- }
-
- if cancel.is_empty() {
- return;
- }
-
- // Individual failures are logged inside; NotFound means another node already
- // cancelled the record.
- let _ = state
- .mutate_doc(move |doc| {
- for pk in &cancel {
- match store::delete_path(&mut *doc, &forgotten_path(pk), false) {
- Ok(()) => info!(peer = %pk, "Peer is alive; cancelling cluster-wide forget"),
- Err(StoreError::NotFound) => {}
- Err(error) => {
- warn!(peer = %pk, error = %error, "Failed to cancel forget record")
- }
- }
- }
- Ok::<(), StoreError>(())
- })
- .await;
-}
-
-// --- keepalive and self-healing ----------------------------------------------
-
-fn heads_digest(doc: &mut AutoCommit) -> transport::HeadsDigest {
- let mut heads = doc.get_heads();
- heads.sort_unstable();
- let mut hasher = Sha256::new();
- for head in &heads {
- hasher.update(head.0);
- }
- hasher.finalize().into()
-}
-
-pub(crate) async fn local_keepalive(state: &AppState) -> transport::Keepalive {
- // AutoCommit::get_heads takes &mut self (it closes the pending transaction).
- // Digest and epoch come from the same locked replica, so they always match.
- let mut replica = state.doc.write().await;
- transport::Keepalive {
- heads_digest: heads_digest(&mut replica.doc),
- epoch: replica.epoch,
- }
-}
-
-async fn heal_if_diverged(
- state: &AppState,
- peer: &str,
- local: &transport::Keepalive,
- remote: &transport::Keepalive,
-) {
- // Digests of documents from different epochs are never equal and never converge.
- if local.epoch != remote.epoch {
- return;
- }
-
- let Some(peer_state_mutex) = state.get_peer_state(peer).await else {
- return;
- };
-
- let heal = peer_state_mutex.lock().await.needs_heal(
- &local.heads_digest,
- &remote.heads_digest,
- &state.timing,
- Instant::now(),
- );
-
- if heal {
- warn!(peer = %peer, "Documents differ and sync is idle; re-handshaking to restart sync");
- ensure_handshake(state, peer).await;
- }
-}
-
-// --- peer discovery and HELLO ------------------------------------------------
-
-pub(crate) async fn remember_peer(state: &AppState, peer: &str) {
- if peer.is_empty() {
- warn!("Ignoring empty peer public key");
- return;
- }
-
- if peer == state.local_id() {
- debug!("Ignoring own public key");
- return;
- }
-
- let mut peer_states = state.peer_states.write().await;
-
- if !peer_states.contains_key(peer) {
- peer_states.entry(peer.to_owned()).or_default();
- info!(
- peer = %peer,
- peer_count = peer_states.len(),
- "Discovered peer"
- );
- } else {
- trace!(peer = %peer, "Peer already registered");
- }
-}
-
-pub(crate) async fn send_hello_once(state: &AppState, peer: &str) -> Result<()> {
- let Some(peer_state_mutex) = state.get_peer_state(peer).await else {
- trace!(peer = %peer, "Peer was forgotten; not sending HELLO");
- return Ok(());
- };
-
- let claimed = {
- let mut peer_state = peer_state_mutex.lock().await;
- if peer_state.hello_state == HelloState::NotSent {
- peer_state.hello_state = HelloState::Sending {
- next_delay: HELLO_RETRY_INITIAL,
- };
- true
- } else {
- trace!(peer = %peer, hello_state = ?peer_state.hello_state, "not sending");
- false
- }
- };
-
- if !claimed {
- return Ok(());
- }
-
- debug!(to = %peer, "Sending HELLO");
- transmit_hello(state, peer, HELLO_RETRY_INITIAL).await
-}
-
-async fn retry_hello_if_due(state: &AppState, peer: &str) -> Result<()> {
- let Some(peer_state_mutex) = state.get_peer_state(peer).await else {
- return Ok(());
- };
-
- let delay = {
- let mut peer_state = peer_state_mutex.lock().await;
- match peer_state.hello_state {
- HelloState::AwaitingReply {
- next_attempt,
- next_delay,
- } if Instant::now() >= next_attempt => {
- peer_state.hello_state = HelloState::Sending { next_delay };
- next_delay
- }
- _ => return Ok(()),
- }
- };
-
- debug!(
- peer = %peer,
- next_retry_ms = delay.as_millis(),
- "No HELLO_REPLY yet; re-sending HELLO"
- );
- transmit_hello(state, peer, delay).await
-}
-
-/// Sends a HELLO without holding any lock, then schedules the next retry.
-/// Success and failure both end in AwaitingReply: Mycelium accepting the message
-/// says nothing about whether the peer received it.
-async fn transmit_hello(state: &AppState, peer: &str, delay: Duration) -> Result<()> {
- let result = state
- .transport
- .send_message(
- peer,
- &ParsedMessage::Hello(transport::HelloRequest {
- epoch: state.local_epoch(),
- }),
- )
- .await;
-
- if let Some(peer_state_mutex) = state.get_peer_state(peer).await {
- let mut peer_state = peer_state_mutex.lock().await;
-
- if matches!(peer_state.hello_state, HelloState::Sending { .. }) {
- let (wait, next_delay) = match peer_state.liveness {
- Liveness::Dormant { .. } => {
- (state.timing.dormant_probe, state.timing.dormant_probe)
- }
- Liveness::Active => (delay, delay.saturating_mul(2).min(HELLO_RETRY_MAX)),
- };
- peer_state.hello_state = HelloState::AwaitingReply {
- next_attempt: Instant::now() + wait,
- next_delay,
- };
- }
- }
-
- result.context("Sending HELLO")
-}
-
-async fn ensure_handshake(state: &AppState, peer: &str) {
- if let Err(error) = send_hello_once(state, peer).await {
- warn!(peer = %peer, error = %format!("{error:#}"), "HELLO failed; will retry");
- }
-}
-
-/// Sends a HELLO_REPLY if one is owed and our cluster ID is known.
-/// Otherwise it stays owed and the peer worker tries again.
-async fn send_owed_hello_reply(state: &AppState, peer: &str) {
- if state.cluster_id().await.is_none() {
- return;
- }
-
- let Some(peer_state_mutex) = state.get_peer_state(peer).await else {
- return;
- };
-
- let owed = std::mem::take(&mut peer_state_mutex.lock().await.reply_owed);
- if !owed {
- return;
- }
-
- if let Err(error) = send_hello_reply(state, peer).await {
- warn!(peer = %peer, error = %format!("{error:#}"), "HELLO_REPLY failed; will retry");
- peer_state_mutex.lock().await.reply_owed = true;
- }
-}
-
-async fn flush_owed_hello_replies(state: &AppState) {
- let peers: Vec<String> = state.peer_states.read().await.keys().cloned().collect();
- for peer in &peers {
- send_owed_hello_reply(state, peer).await;
- }
-}
-
-async fn send_hello_reply(state: &AppState, peer: &str) -> Result<()> {
- let cluster_id = state
- .cluster_id()
- .await
- .ok_or_else(|| anyhow!("Cannot send HELLO_REPLY without an established cluster ID"))?;
-
- let candidates: Vec<(String, Arc<Mutex<PeerState>>)> = state
- .peer_states
- .read()
- .await
- .iter()
- .filter(|(known, _)| known.as_str() != peer)
- .map(|(known, m)| (known.clone(), Arc::clone(m)))
- .collect();
-
- let mut peers = Vec::new();
- for (known, peer_state_mutex) in candidates {
- let peer_state = peer_state_mutex.lock().await;
- if peer_state.liveness == Liveness::Active
- && peer_state.hello_state == HelloState::Established
- {
- peers.push(known);
- }
- }
-
- let reply = transport::HelloReply {
- cluster_id,
- peers,
- epoch: state.local_epoch(),
- };
-
- let ready = state.is_ready().await;
-
- debug!(to = %peer, ready, "Sending HELLO_REPLY");
-
- state
- .transport
- .send_message(peer, &ParsedMessage::HelloReply(reply))
- .await
- .context("Sending HELLO_REPLY")?;
-
- trace!(to = %peer, "HELLO_REPLY sent");
- Ok(())
-}
-
-async fn discover_peers(state: &AppState, from_peer: &str, advertised_peers: &[String]) {
- // TODO here we could add a trace t oshwo advertised_peers
- for discovered_peer in advertised_peers {
- if discovered_peer == from_peer {
- continue;
- }
-
- remember_peer(state, discovered_peer).await;
-
- if let Err(error) = send_hello_once(state, discovered_peer).await {
- warn!(
- peer = %discovered_peer,
- error = %format!("{error:#}"),
- "Failed to initiate HELLO with discovered peer"
- );
- }
- }
-}
-
-// --- peer worker -------------------------------------------------------------
-
-pub(crate) async fn run_peer_worker(state: AppState) {
- let mut tick = tokio::time::interval(PEER_TICK);
- tick.set_missed_tick_behavior(MissedTickBehavior::Delay);
-
- info!("Starting peer maintenance worker");
-
- let mut last_forget_check: Option<Instant> = None;
-
- loop {
- tick.tick().await;
-
- // While joining, keep handshakes fast; dormancy would slow probes to minutes.
- let ready = state.is_ready().await;
-
- let peers: Vec<(String, Arc<Mutex<PeerState>>)> = state
- .peer_states
- .read()
- .await
- .iter()
- .map(|(pk, m)| (pk.clone(), Arc::clone(m)))
- .collect();
-
- for (peer, peer_state_mutex) in &peers {
- let now = Instant::now();
-
- let (became_dormant, ping_due) = {
- let mut peer_state = peer_state_mutex.lock().await;
- let became_dormant = ready && peer_state.check_dormancy(&state.timing, now);
- let ping_due = peer_state.take_keepalive_due(&state.timing, now);
- (became_dormant, ping_due)
- };
-
- if became_dormant {
- warn!(
- peer = %peer,
- silent_secs = state.timing.dormant_after.as_secs(),
- probe_secs = state.timing.dormant_probe.as_secs(),
- "Peer went dormant"
- );
- }
-
- if ping_due {
- let local = local_keepalive(&state).await;
- if let Err(error) = state
- .transport
- .send_message(peer, &ParsedMessage::Ping(local))
- .await
- {
- debug!(peer = %peer, error = %format!("{error:#}"), "Keepalive PING failed");
- }
- }
-
- if let Err(error) = retry_hello_if_due(&state, peer).await {
- warn!(peer = %peer, error = %format!("{error:#}"), "HELLO retry failed");
- }
-
- send_owed_hello_reply(&state, peer).await;
- }
-
- if last_forget_check.is_none_or(|t| t.elapsed() >= FORGET_CHECK_INTERVAL) {
- apply_forget_records(&state).await;
- last_forget_check = Some(Instant::now());
- }
- }
-}
-
-// --- peer membership ---------------------------------------------------------
-
-async fn establish_peer(
- state: &AppState,
- peer: &str,
- cluster_id: String,
-) -> Result<Vec<transport::SyncEnvelope>> {
- let expected_cluster = state.cluster_id().await;
-
- if let Some(expected) = &expected_cluster {
- if expected != &cluster_id {
- bail!(
- "Incompatible cluster at {peer}: expected {}, received {}",
- expected,
- cluster_id
- );
- }
- }
-
- let peer_state_mutex = state.get_or_create_peer_state(peer).await;
- let mut peer_state = peer_state_mutex.lock().await;
-
- if let PeerMembership::Member {
- cluster_id: existing,
- } = &peer_state.membership
- {
- if existing != &cluster_id {
- bail!(
- "Peer {peer} changed cluster identity from {} to {}",
- existing,
- cluster_id
- );
- }
- }
-
- // Lock order: peer mutex -> lifecycle.
- if expected_cluster.is_none() {
- state.set_expected_cluster_id(cluster_id.clone()).await?;
- }
-
- if matches!(peer_state.membership, PeerMembership::Unknown) {
- peer_state.membership = PeerMembership::Member {
- cluster_id: cluster_id.clone(),
- };
- }
-
- // First reply completes the handshake: stop retries and sync from a clean slate.
- // Duplicate replies (caused by our own retries) don't reset again.
- if peer_state.hello_state != HelloState::Established {
- peer_state.hello_state = HelloState::Established;
- peer_state.reset_sync();
- info!(peer = %peer, "Handshake complete; sync state reset");
- }
-
- let pending = std::mem::take(&mut peer_state.pending_inbound_sync);
-
- info!(peer = %peer, cluster_id = %cluster_id, pending_messages = pending.len(), "Peer membership established");
- Ok(pending)
-}
-
-#[instrument(skip(state), fields(peer = %peer))]
-pub(crate) async fn peer_cluster(state: &AppState, peer: &str) -> Option<String> {
- let peer_state_mutex = state.get_peer_state(peer).await?;
- let peer_state = peer_state_mutex.lock().await;
- match &peer_state.membership {
- PeerMembership::Unknown => None,
- PeerMembership::Member { cluster_id } => Some(cluster_id.clone()),
- }
-}
-
-// --- epochs ------------------------------------------------------------------
-
-/// Records the epoch a peer advertised and compares it with ours. Returns true when both
-/// are on the same epoch, the only case in which the documents may be synced.
-async fn observe_peer_epoch(state: &AppState, peer: &str, remote: Epoch) -> 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;
- 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());
- }
- 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 compaction: 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
-}
-
-// --- inbound dispatch --------------------------------------------------------
-
-pub(crate) async fn handle_message(
- state: &AppState,
- peer: String,
- message: transport::ParsedMessage,
-) -> Result<()> {
- if peer == state.local_id() {
- debug!("Ignoring message from self");
- return Ok(());
- }
-
- remember_peer(state, &peer).await;
-
- // Any inbound message proves liveness.
- let reactivated = state
- .get_or_create_peer_state(&peer)
- .await
- .lock()
- .await
- .mark_seen(Instant::now());
-
- if reactivated {
- info!(peer = %peer, "Dormant peer is active again; re-handshaking");
- }
-
- let same_epoch = observe_peer_epoch(state, &peer, message.epoch()).await;
-
- //debug!(from = %peer, message = ?message, "Received message");
-
- match message {
- transport::ParsedMessage::Hello(_) => {
- // A HELLO means the peer is (re)starting its handshake with us, e.g. after a
- // restart. Whatever our sync state believed about it is stale.
- {
- let peer_state_mutex = state.get_or_create_peer_state(&peer).await;
- let mut peer_state = peer_state_mutex.lock().await;
- peer_state.reset_sync();
- peer_state.reply_owed = true;
- }
-
- // Sends now if we know our cluster ID; otherwise stays owed.
- send_owed_hello_reply(state, &peer).await;
-
- // Our own HELLO goes out regardless of whether the reply could be sent.
- if let Err(error) = send_hello_once(state, &peer).await {
- warn!(peer = %peer, error = %format!("{error:#}"), "HELLO failed; will retry");
- }
-
- // Restart sync with the fresh state (no-op until the peer is a validated member).
- let _ = state
- .sync_events
- .send(SyncEvent::PeerNeedsSync(peer.clone()));
- }
- transport::ParsedMessage::HelloReply(reply) => {
- let pending = establish_peer(state, &peer, reply.cluster_id.clone()).await?;
-
- discover_peers(state, &peer, &reply.peers).await;
-
- // Our cluster ID may have just become known: pay any replies we owe.
- flush_owed_hello_replies(state).await;
-
- if !pending.is_empty() {
- debug!(
- to = %peer,
- pending_messages = pending.len(),
- "Replaying pending sync messages"
- );
- for envelope in pending {
- if let Err(error) = handle_sync(state, &peer, envelope).await {
- warn!(peer = %peer, error = %format!("{error:#}"), "Discarding invalid pending sync envelope");
- }
- }
- }
-
- state
- .sync_events
- .send(SyncEvent::PeerValidated(peer.clone()))
- .map_err(|error| anyhow!("Failed to notify sync worker: {error}"))?;
- }
- transport::ParsedMessage::Sync(envelope) => {
- if reactivated {
- ensure_handshake(state, &peer).await;
- }
- if !same_epoch {
- debug!(peer = %peer, epoch = %envelope.epoch, "Dropping sync message from another epoch");
- return Ok(());
- }
- handle_sync(state, &peer, envelope).await?;
- }
- transport::ParsedMessage::Ping(remote) => {
- if reactivated || peer_cluster(state, &peer).await.is_none() {
- ensure_handshake(state, &peer).await;
- }
- let local = local_keepalive(state).await;
- heal_if_diverged(state, &peer, &local, &remote).await;
- if let Err(error) = state
- .transport
- .send_message(&peer, &ParsedMessage::Pong(local))
- .await
- {
- debug!(peer = %peer, error = %format!("{error:#}"), "PONG failed");
- }
- }
- transport::ParsedMessage::Pong(remote) => {
- if reactivated || peer_cluster(state, &peer).await.is_none() {
- ensure_handshake(state, &peer).await;
- }
- let local = local_keepalive(state).await;
- heal_if_diverged(state, &peer, &local, &remote).await;
- }
- }
-
- Ok(())
-}