summaryrefslogtreecommitdiffstats
path: root/src/replica.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/replica.rs')
-rw-r--r--src/replica.rs130
1 files changed, 130 insertions, 0 deletions
diff --git a/src/replica.rs b/src/replica.rs
new file mode 100644
index 0000000..4579029
--- /dev/null
+++ b/src/replica.rs
@@ -0,0 +1,130 @@
+//! The local replica: the Automerge document together with the epoch it belongs to.
+//!
+//! Compaction (later steps) replaces the document with a snapshot whose history is
+//! unrelated to the old one. Syncing two unrelated histories would make Automerge merge
+//! them, turning every key into a conflict with its own copy, so nodes on different
+//! epochs must never sync. Keeping the epoch and the document under one lock makes it
+//! impossible to label a sync message from one document with the other's epoch.
+
+use std::fmt;
+use std::ops::{Deref, DerefMut};
+
+use automerge::AutoCommit;
+
+/// One generation of the cluster document.
+///
+/// * `generation` orders epochs: every compaction increments it, so a node can tell
+/// whether a peer is behind or ahead.
+/// * `id` is the hash of the epoch's snapshot change. It tells two epochs of the same
+/// generation apart, which only happens if two compactions committed concurrently.
+#[derive(Clone, Copy, PartialEq, Eq, Hash)]
+pub struct Epoch {
+ pub generation: u64,
+ pub id: [u8; 32],
+}
+
+/// How a peer's epoch relates to ours.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum EpochRelation {
+ Same,
+ /// The peer is on an older generation and has to migrate.
+ PeerBehind,
+ /// The peer is on a newer generation; we have to migrate.
+ PeerAhead,
+ /// Same generation, different snapshot: competing compactions. Never sync.
+ Diverged,
+}
+
+impl Epoch {
+ /// Clusters that have never been compacted.
+ pub const GENESIS: Epoch = Epoch {
+ generation: 0,
+ id: [0; 32],
+ };
+
+ pub fn relation_to(&self, peer: &Epoch) -> EpochRelation {
+ use std::cmp::Ordering;
+ match peer.generation.cmp(&self.generation) {
+ Ordering::Less => EpochRelation::PeerBehind,
+ Ordering::Greater => EpochRelation::PeerAhead,
+ Ordering::Equal if peer.id == self.id => EpochRelation::Same,
+ Ordering::Equal => EpochRelation::Diverged,
+ }
+ }
+
+ pub fn id_hex(&self) -> String {
+ self.id.iter().map(|byte| format!("{byte:02x}")).collect()
+ }
+}
+
+impl fmt::Display for Epoch {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if *self == Epoch::GENESIS {
+ write!(f, "0:genesis")
+ } else {
+ write!(f, "{}:{}", self.generation, &self.id_hex()[..12])
+ }
+ }
+}
+
+impl fmt::Debug for Epoch {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Display::fmt(self, f)
+ }
+}
+
+/// The document and its epoch, always locked and swapped together.
+///
+/// Derefs to the document, so code that only needs the `AutoCommit` keeps working
+/// with a guard of `Replica`.
+pub struct Replica {
+ pub epoch: Epoch,
+ pub doc: AutoCommit,
+}
+
+impl Replica {
+ pub fn new(epoch: Epoch, doc: AutoCommit) -> Self {
+ Self { epoch, doc }
+ }
+}
+
+impl Deref for Replica {
+ type Target = AutoCommit;
+
+ fn deref(&self) -> &AutoCommit {
+ &self.doc
+ }
+}
+
+impl DerefMut for Replica {
+ fn deref_mut(&mut self) -> &mut AutoCommit {
+ &mut self.doc
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn epoch(generation: u64, byte: u8) -> Epoch {
+ Epoch {
+ generation,
+ id: [byte; 32],
+ }
+ }
+
+ #[test]
+ fn relation() {
+ let local = epoch(3, 1);
+ assert_eq!(local.relation_to(&epoch(3, 1)), EpochRelation::Same);
+ assert_eq!(local.relation_to(&epoch(2, 9)), EpochRelation::PeerBehind);
+ assert_eq!(local.relation_to(&epoch(4, 9)), EpochRelation::PeerAhead);
+ assert_eq!(local.relation_to(&epoch(3, 2)), EpochRelation::Diverged);
+ }
+
+ #[test]
+ fn display() {
+ assert_eq!(Epoch::GENESIS.to_string(), "0:genesis");
+ assert_eq!(epoch(2, 0xab).to_string(), "2:abababababab");
+ }
+}