diff options
| author | tv <tv@krebsco.de> | 2026-10-26 06:02:14 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-10-26 06:02:14 +0100 |
| commit | 3be4ec3aa0e076e5386cfd1b93b07d12c40a88cb (patch) | |
| tree | 0c5e03337ad47694299caaa997b5bcdb3ac9e945 /src | |
| parent | 0b13e180c4dcd38e3ea8ffeecc9e5ebefa023212 (diff) | |
add session IDs
This prevents the regenerate flag from being lost across reset_sync
during an in-flight send.
Diffstat (limited to 'src')
| -rw-r--r-- | src/state/session.rs | 68 | ||||
| -rw-r--r-- | src/swarm/sync.rs | 38 |
2 files changed, 87 insertions, 19 deletions
diff --git a/src/state/session.rs b/src/state/session.rs index 0dcc76d..618d65b 100644 --- a/src/state/session.rs +++ b/src/state/session.rs @@ -6,6 +6,7 @@ //! forgetting what was sent when the peer still needs it) live here with it, so an //! Automerge upgrade that changes the state touches this file only. +use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Duration; use automerge::sync::{self, SyncDoc}; @@ -26,6 +27,9 @@ pub(crate) struct Sending { pub(crate) regenerate: bool, } +/// Session IDs, unique within the process. +static NEXT_SESSION: AtomicU64 = AtomicU64::new(1); + /// First wait before generating again after our Mycelium refused a message; doubles /// while it keeps refusing. const SEND_RETRY: Duration = Duration::from_millis(500); @@ -35,6 +39,8 @@ const SEND_RETRY_MAX: Duration = Duration::from_secs(30); /// forget lives here, so a reset replaces it whole and can't miss a field. #[derive(Clone)] pub(crate) struct SyncSession { + /// Tells this session from the one a reset puts in its place (`NEXT_SESSION`). + id: u64, state: sync::State, /// A message being handed to our Mycelium. @@ -69,6 +75,7 @@ impl SyncSession { /// `reply_backoff`: waits for sync answers (`Timing::sync_reply_backoff`). pub(crate) fn new(reply_backoff: Backoff) -> Self { Self { + id: NEXT_SESSION.fetch_add(1, Ordering::Relaxed), state: sync::State::new(), sending: None, retry_at: None, @@ -193,8 +200,20 @@ impl SyncSession { self.state.their_heads.is_none() && we_ready && peer_ready == Some(false) } - /// Our Mycelium took the message. - pub(crate) fn send_succeeded(&mut self) -> bool { + /// Which session this is: a message generated from it reports its send's outcome + /// with this (`send_succeeded`, `send_failed`). + pub(crate) fn id(&self) -> u64 { + self.id + } + + /// Our Mycelium took the message generated from session `session`. Returns whether + /// to generate again: a pass was requested while it was handed over, or a reset + /// replaced the session meanwhile (any pass requested then was lost with the old + /// session, and the new one hasn't necessarily opened yet). + pub(crate) fn send_succeeded(&mut self, session: u64) -> bool { + if session != self.id { + return true; + } self.retry_at = None; self.send_backoff.reset(); self.sending @@ -205,8 +224,11 @@ impl SyncSession { /// Our Mycelium refused the message: as far as the peer is concerned, it was never /// sent. Takes back its changes (they go out with the next message), clears /// `in_flight`, and returns how long to wait before generating again. None if a reset - /// already discarded what was being sent. - pub(crate) fn send_failed(&mut self, now: Instant) -> Option<Duration> { + /// already discarded what was being sent (the message came from another `session`). + pub(crate) fn send_failed(&mut self, session: u64, now: Instant) -> Option<Duration> { + if session != self.id { + return None; + } let sending = self.sending.take()?; for hash in &sending.sent { self.state.sent_hashes.remove(hash); @@ -264,3 +286,41 @@ impl SyncSession { Some(wait.sent.len()) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn session() -> SyncSession { + SyncSession::new(Backoff::new( + Duration::from_secs(1), + Duration::from_secs(30), + )) + } + + fn sending() -> Option<Sending> { + Some(Sending { + sent: Vec::new(), + regenerate: false, + }) + } + + #[test] + fn a_send_from_a_replaced_session_leaves_the_new_one_alone() { + let old = session(); + let mut new = session(); + new.sending = sending(); + + // A reset discarded the old session while its message was handed over: the + // new one's own message stays, and the peer is synced again. + assert!(new.send_succeeded(old.id())); + assert!(new.sending.is_some()); + assert_eq!(new.send_failed(old.id(), Instant::now()), None); + assert!(new.sending.is_some()); + assert_eq!(new.retry_at, None); + + // Its own outcome still applies. + assert!(!new.send_succeeded(new.id())); + assert!(new.sending.is_none()); + } +} diff --git a/src/swarm/sync.rs b/src/swarm/sync.rs index 805d700..5a4cba4 100644 --- a/src/swarm/sync.rs +++ b/src/swarm/sync.rs @@ -126,7 +126,7 @@ async fn deliver_sync_to_peer(state: &AppState, peer: &str) -> Result<()> { } } - let encoded = { + let (session, encoded) = { let doc = Arc::clone(&state.doc); let psm = Arc::clone(&peer_state_mutex); let transport = Arc::clone(&state.transport); @@ -135,11 +135,12 @@ async fn deliver_sync_to_peer(state: &AppState, peer: &str) -> Result<()> { // Before this message is counted. let bytes_before = SyncBytes::of(&state.traffic, peer); - let generated = - tokio::task::spawn_blocking(move || -> Result<Option<transport::EncodedMessage>> { + // The session the message comes from, for its send's outcome. + let (session, generated) = tokio::task::spawn_blocking( + move || -> (u64, Result<Option<transport::EncodedMessage>>) { // Label the message with the epoch of the document it was generated // from, read under the same lock. - let (epoch, epoch_seq, message) = { + let (session, epoch, epoch_seq, message) = { let mut replica = doc.blocking_write(); let mut peer_state = psm.blocking_lock(); let (epoch, epoch_seq) = (replica.epoch, replica.epoch_seq); @@ -149,10 +150,10 @@ async fn deliver_sync_to_peer(state: &AppState, peer: &str) -> Result<()> { peer_state .sync .generate(&mut replica.doc, Instant::now(), bytes_before); - (epoch, epoch_seq, message) + (peer_state.sync.id(), epoch, epoch_seq, message) }; let Some(message) = message else { - return Ok(None); + return (session, Ok(None)); }; let envelope = transport::SyncEnvelope { swarm_id, @@ -161,12 +162,14 @@ async fn deliver_sync_to_peer(state: &AppState, peer: &str) -> Result<()> { message, ready, }; - transport + let encoded = transport .encode(&peer_owned, &ParsedMessage::Sync(envelope)) - .map(Some) - }) - .await - .context("Sync generation task panicked")?; + .map(Some); + (session, encoded) + }, + ) + .await + .context("Sync generation task panicked")?; let generated = match generated { Ok(generated) => generated, Err(error) => { @@ -175,7 +178,7 @@ async fn deliver_sync_to_peer(state: &AppState, peer: &str) -> Result<()> { .lock() .await .sync - .send_failed(Instant::now()); + .send_failed(session, Instant::now()); return Err(error); } }; @@ -184,13 +187,13 @@ async fn deliver_sync_to_peer(state: &AppState, peer: &str) -> Result<()> { return Ok(()); }; - encoded + (session, encoded) }; // No state locks are held across the send. match state.transport.send_encoded(&encoded).await { Ok(()) => { - let regenerate = peer_state_mutex.lock().await.sync.send_succeeded(); + let regenerate = peer_state_mutex.lock().await.sync.send_succeeded(session); trace!(regenerate, "Outbound sync message accepted by Mycelium"); @@ -208,9 +211,14 @@ async fn deliver_sync_to_peer(state: &AppState, peer: &str) -> Result<()> { .lock() .await .sync - .send_failed(Instant::now()) + .send_failed(session, Instant::now()) else { + // The new session may not have opened yet. debug!("Sync message refused, but a reset already discarded it"); + state + .sync_events + .send(SyncEvent::PeerNeedsSync(peer.to_owned())) + .map_err(|error| anyhow!("Failed to notify sync worker: {error}"))?; return Ok(()); }; |
