diff options
Diffstat (limited to 'src/replica.rs')
| -rw-r--r-- | src/replica.rs | 130 |
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"); + } +} |
