1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
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");
}
}
|