aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/document/sync.rs108
-rw-r--r--src/swarm/sync.rs31
-rw-r--r--src/swarm/tree.rs5
-rw-r--r--src/transport/mod.rs5
-rw-r--r--src/transport/wire.rs3
5 files changed, 112 insertions, 40 deletions
diff --git a/src/document/sync.rs b/src/document/sync.rs
index c432173..14adca7 100644
--- a/src/document/sync.rs
+++ b/src/document/sync.rs
@@ -234,16 +234,19 @@ impl SyncSession {
})
}
- /// The message just generated passes a change on along a writer's tree, to a peer
- /// that had nothing we lack (`have_all_theirs`), and no answer was awaited before it:
- /// once it has the message, it holds exactly our `heads`, and answers nothing
- /// (`answered_along_tree`). So it is taken as delivered: the peer's heads are ours,
- /// and no answer is awaited (about half the sync messages, otherwise, are those
- /// answers). Should it be lost, the announcements make the peer pull: the slow round
- /// of its other neighbours, or ours where it reaches no other member
- /// (`swarm::tree::Plan::alone`); should the peer have changes we lack after all, it
- /// answers with them, as its heads differ from ours. Undone if our transport refuses
- /// the message (`send_failed`).
+ /// The message just generated goes to a peer that had nothing we lack
+ /// (`have_all_theirs`), and no answer was awaited before it: once it has the message,
+ /// it holds exactly our `heads`, and answers nothing (`answered_as_delivered`; the
+ /// envelope says so, `SyncEnvelope::delivered`). So it is taken as delivered: the
+ /// peer's heads are ours, and no answer is awaited (otherwise about half the sync
+ /// messages are those answers: along trees, to pulls, to what is sent again). Should
+ /// it be lost, the peer still lacks it: where it asked (a pull), it asks again after
+ /// its own reply timeout; otherwise the announcements make it pull (the slow round of
+ /// its other neighbours, or ours where it reaches no other member:
+ /// `swarm::tree::Plan::alone`), or our next message, which it can't apply, makes it
+ /// ask. Should the peer have changes we lack after all, it answers with them, as its
+ /// heads differ from ours. Undone if our transport refuses the message
+ /// (`send_failed`).
pub(crate) fn assume_delivered(&mut self, heads: &[crate::document::ChangeHash]) {
let Some(sending) = &mut self.sending else {
return;
@@ -258,11 +261,11 @@ impl SyncSession {
self.reply = None;
}
- /// We applied a message passing a change on along a writer's tree: if we hold
- /// exactly its sender's heads now, its sender took it as delivered
- /// (`assume_delivered`), and Automerge's answer, our heads, would tell it nothing.
- /// So none goes: we count as having said our `heads`. Returns whether so.
- pub(crate) fn answered_along_tree(&mut self, heads: &[crate::document::ChangeHash]) -> bool {
+ /// We applied a message its sender took as delivered (`assume_delivered`,
+ /// `SyncEnvelope::delivered`): if we hold exactly its sender's heads now, Automerge's
+ /// answer, our heads, would tell it nothing. So none goes: we count as having said
+ /// our `heads`. Returns whether so.
+ pub(crate) fn answered_as_delivered(&mut self, heads: &[crate::document::ChangeHash]) -> bool {
let mut ours = heads.to_vec();
ours.sort_unstable();
let theirs = self.state.their_heads.as_ref().map(|theirs| {
@@ -619,10 +622,10 @@ mod tests {
(a, b)
}
- /// A message along a tree to a peer that had everything else: taken as delivered,
- /// and not answered; neither side has more to say.
+ /// A message to a peer that had everything else: taken as delivered, and not
+ /// answered; neither side has more to say.
#[test]
- fn tree_messages_need_no_answer() {
+ fn delivered_messages_need_no_answer() {
use crate::document::testing::change;
use automerge::{ROOT, transaction::Transactable};
let (mut ours, mut theirs) = (Document::new(), Document::new());
@@ -638,7 +641,7 @@ mod tests {
assert!(a.reply.is_none(), "no answer awaited");
b.receive(&mut theirs, decode(&message).unwrap()).unwrap();
- assert!(b.answered_along_tree(&theirs.get_heads()));
+ assert!(b.answered_as_delivered(&theirs.get_heads()));
assert!(
b.generate(&theirs, now, bytes).is_none(),
"nothing to answer"
@@ -653,7 +656,7 @@ mod tests {
/// The peer wrote meanwhile: its heads differ from ours after our message, so it
/// answers, with its change.
#[test]
- fn tree_messages_are_answered_with_changes_we_lack() {
+ fn delivered_messages_are_answered_with_changes_we_lack() {
use crate::document::testing::change;
use automerge::{ROOT, transaction::Transactable};
let (mut ours, mut theirs) = (Document::new(), Document::new());
@@ -668,17 +671,17 @@ mod tests {
a.send_succeeded(a.id());
b.receive(&mut theirs, decode(&message).unwrap()).unwrap();
- assert!(!b.answered_along_tree(&theirs.get_heads()));
+ assert!(!b.answered_as_delivered(&theirs.get_heads()));
let answer = b.generate(&theirs, now, bytes).expect("b answers");
b.send_succeeded(b.id());
a.receive(&mut ours, decode(&answer).unwrap()).unwrap();
assert_eq!(ours.get_heads(), theirs.get_heads());
}
- /// A message along a tree is lost, and the next one arrives: the peer can't apply
- /// it, answers what it needs, and gets the lost change then.
+ /// A message taken as delivered is lost, and the next one arrives: the peer can't
+ /// apply it, answers what it needs, and gets the lost change then.
#[test]
- fn a_lost_tree_message_is_asked_for() {
+ fn a_lost_delivered_message_is_asked_for() {
use crate::document::testing::change;
use automerge::{ROOT, transaction::Transactable};
let (mut ours, mut theirs) = (Document::new(), Document::new());
@@ -700,7 +703,7 @@ mod tests {
a.send_succeeded(a.id());
b.receive(&mut theirs, decode(&next).unwrap()).unwrap();
- assert!(!b.answered_along_tree(&theirs.get_heads()));
+ assert!(!b.answered_as_delivered(&theirs.get_heads()));
let ask = b
.generate(&theirs, now, bytes)
.expect("b asks for what it lacks");
@@ -713,10 +716,63 @@ mod tests {
assert_eq!(ours.get_heads(), theirs.get_heads());
}
+ /// Two messages taken as delivered arrive in reverse order: the peer asks for the
+ /// first, which arrives meanwhile, and what is sent again is taken as delivered too.
+ /// Nobody waits for an answer that won't come.
+ #[test]
+ fn reordered_delivered_messages_leave_nothing_awaited() {
+ use crate::document::testing::change;
+ use automerge::{ROOT, transaction::Transactable};
+ let (mut ours, mut theirs) = (Document::new(), Document::new());
+ change(&mut ours, |doc| doc.put(ROOT, "early", 1).unwrap());
+ let (mut a, mut b) = in_step(&mut ours, &mut theirs);
+ let (now, bytes) = (Instant::now(), SyncBytes::default());
+ let send = |a: &mut SyncSession, ours: &Document| {
+ assert!(a.reply.is_none() && a.have_all_theirs(ours));
+ let message = a.generate(ours, now, bytes).unwrap();
+ a.assume_delivered(&ours.get_heads());
+ a.send_succeeded(a.id());
+ message
+ };
+
+ change(&mut ours, |doc| doc.put(ROOT, "first", 2).unwrap());
+ let first = send(&mut a, &ours);
+ change(&mut ours, |doc| doc.put(ROOT, "second", 3).unwrap());
+ let second = send(&mut a, &ours);
+
+ b.receive(&mut theirs, decode(&second).unwrap()).unwrap();
+ assert!(!b.answered_as_delivered(&theirs.get_heads()));
+ let ask = b
+ .generate(&theirs, now, bytes)
+ .expect("b asks for the first");
+ b.send_succeeded(b.id());
+ b.receive(&mut theirs, decode(&first).unwrap()).unwrap();
+ assert_eq!(theirs.get_heads(), ours.get_heads(), "b has both now");
+ // The first says older heads than b's now: b tells its own, once.
+ assert!(!b.answered_as_delivered(&theirs.get_heads()));
+ let level = b.generate(&theirs, now, bytes);
+ b.send_succeeded(b.id());
+
+ a.receive(&mut ours, decode(&ask).unwrap()).unwrap();
+ a.resend_if_needed("b");
+ let again = send(&mut a, &ours);
+ if let Some(level) = level {
+ a.receive(&mut ours, decode(&level).unwrap()).unwrap();
+ }
+ b.receive(&mut theirs, decode(&again).unwrap()).unwrap();
+ assert!(b.answered_as_delivered(&theirs.get_heads()));
+ assert!(
+ b.generate(&theirs, now, bytes).is_none(),
+ "nothing to answer"
+ );
+ assert!(a.reply.is_none(), "nothing awaited");
+ assert_eq!(ours.get_heads(), theirs.get_heads());
+ }
+
/// Our transport refused a message taken as delivered: the peer's heads are as
/// before, so the next message carries the change again.
#[test]
- fn a_refused_tree_message_goes_again() {
+ fn a_refused_delivered_message_goes_again() {
use crate::document::testing::change;
use automerge::{ROOT, transaction::Transactable};
let (mut ours, mut theirs) = (Document::new(), Document::new());
diff --git a/src/swarm/sync.rs b/src/swarm/sync.rs
index 02c6379..5be0754 100644
--- a/src/swarm/sync.rs
+++ b/src/swarm/sync.rs
@@ -144,16 +144,15 @@ async fn deliver_sync_to_peer(state: &AppState, peer: &str) -> Result<()> {
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 (session, epoch, epoch_seq, body, writer) = {
+ let (session, epoch, epoch_seq, body, writer, delivered) = {
let replica = doc.blocking_read();
let mut peer_state = psm.blocking_lock();
let (epoch, epoch_seq) = (replica.epoch, replica.epoch_seq);
// Whose change it passes on, if any: this message carries it.
let writer = peer_state.tree_writer.take();
- // Along a tree, to a peer that had nothing we lack, with nothing
- // awaited: taken as delivered (`SyncSession::assume_delivered`).
- let level = writer.is_some()
- && peer_state.sync.reply.is_none()
+ // To a peer that had nothing we lack, with nothing awaited: taken
+ // as delivered (`SyncSession::assume_delivered`).
+ let level = peer_state.sync.reply.is_none()
&& peer_state.sync.have_all_theirs(&replica.doc);
// Under the same locks as the epoch label: the message belongs to
// this document.
@@ -178,10 +177,18 @@ async fn deliver_sync_to_peer(state: &AppState, peer: &str) -> Result<()> {
);
transport::SyncBody::Delta(spec)
});
- if level && matches!(body, Some(transport::SyncBody::Message(_))) {
+ let delivered = level && matches!(body, Some(transport::SyncBody::Message(_)));
+ if delivered {
peer_state.sync.assume_delivered(replica.heads());
}
- (peer_state.sync.id(), epoch, epoch_seq, body, writer)
+ (
+ peer_state.sync.id(),
+ epoch,
+ epoch_seq,
+ body,
+ writer,
+ delivered,
+ )
};
let Some(body) = body else {
return (session, Ok(None));
@@ -193,6 +200,7 @@ async fn deliver_sync_to_peer(state: &AppState, peer: &str) -> Result<()> {
body,
ready,
writer,
+ delivered,
};
let encoded = transport
.encode(&peer_owned, &ParsedMessage::Sync(envelope))
@@ -353,6 +361,7 @@ pub(crate) async fn handle_sync(
let epoch_label = envelope.epoch;
let from_ready = envelope.ready;
let writer = envelope.writer;
+ let delivered = envelope.delivered;
let payload = match envelope.body {
transport::SyncBody::Message(payload) => payload,
transport::SyncBody::Delta(spec) => {
@@ -419,10 +428,10 @@ pub(crate) async fn handle_sync(
peer_state.sync.resend_if_needed(&peer);
replica.refresh_heads();
let heads_after = replica.heads().to_vec();
- if writer.is_some() {
- // Level with the sender now: it takes the message as delivered, so
- // there is nothing to answer (`SyncSession::answered_along_tree`).
- peer_state.sync.answered_along_tree(&heads_after);
+ if delivered {
+ // Level with the sender now, which takes the message as delivered:
+ // nothing to answer (`SyncSession::answered_as_delivered`).
+ peer_state.sync.answered_as_delivered(&heads_after);
}
stats.heads_seen(heads_after.len());
let changed = heads_after != heads_before;
diff --git a/src/swarm/tree.rs b/src/swarm/tree.rs
index c474754..c6baff9 100644
--- a/src/swarm/tree.rs
+++ b/src/swarm/tree.rs
@@ -16,8 +16,9 @@
//! pair key here): whoever applies changes passes them on to its own children in each of
//! their writers' trees (`forward`), however they came; so does the writer, to all its
//! neighbours, with its own writes. A sync message along a tree also says whose
-//! (`SyncEnvelope::writer`), for one that had them already (`swarm::sync`), and needs no
-//! answer where the child had nothing the parent lacks (`SyncSession::assume_delivered`).
+//! (`SyncEnvelope::writer`), for one that had them already (`swarm::sync`); like any
+//! sync message, it needs no answer where the child had nothing the parent lacks
+//! (`SyncSession::assume_delivered`).
//! A member no member it reaches comes before (or whose parent didn't get the change)
//! gets it from the announcements (`swarm::relay`), as before.
//!
diff --git a/src/transport/mod.rs b/src/transport/mod.rs
index 17d6d4c..426ef72 100644
--- a/src/transport/mod.rs
+++ b/src/transport/mod.rs
@@ -666,6 +666,11 @@ pub(crate) struct SyncEnvelope {
/// the writer's, on its own push and on every member's after; None otherwise
/// (answers, pulls).
pub(crate) writer: Option<u64>,
+ /// The sender takes the message as delivered (`SyncSession::assume_delivered`): it
+ /// had everything the receiver had, so the receiver will hold exactly the sender's
+ /// heads, and the sender awaits no answer. A receiver it leaves level with the
+ /// sender answers nothing; one that has changes the sender lacks answers with them.
+ pub(crate) delivered: bool,
}
/// What a sync envelope carries.
diff --git a/src/transport/wire.rs b/src/transport/wire.rs
index 899454c..4199927 100644
--- a/src/transport/wire.rs
+++ b/src/transport/wire.rs
@@ -14,7 +14,7 @@ use super::{EncodedMessage, MessageKind, ParsedMessage};
/// The wire format's version, the first byte of every payload. Bump it with every change
/// to a message type or a codec (`golden_bytes` fails then).
-pub(crate) const VERSION: u8 = 24;
+pub(crate) const VERSION: u8 = 25;
/// What the second byte of a payload says.
fn tag(kind: MessageKind) -> u8 {
@@ -410,6 +410,7 @@ pub(crate) fn samples() -> Vec<ParsedMessage> {
}),
ready: true,
writer: Some(7),
+ delivered: true,
}),
ParsedMessage::Status(status.clone()),
ParsedMessage::StatusReply(status),