//! 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"); } }