Skip to main content

blockworx/log/
dag.rs

1//! The history graph: changes keyed by content address, and the canonical
2//! order they replay in.
3//!
4//! The linearization is *not* what makes the merge correct — registers commute,
5//! so any order folds to the same state. It exists so that two replicas
6//! produce the same snapshot bytes, so undo can find an actor's latest change,
7//! and so collision reporting is reproducible.
8
9use std::cmp::Reverse;
10use std::collections::{BTreeMap, BTreeSet, BinaryHeap};
11
12use crate::log::change::Change;
13use crate::log::id::{ActorId, ChangeHash, Lamport};
14
15/// Changes held by hash. Inserting the same change twice is a no-op, which is
16/// what makes ingest idempotent at the graph level as well as the fold's.
17#[derive(Clone, Debug, Default)]
18pub struct Dag {
19    changes: BTreeMap<ChangeHash, Change>,
20}
21
22impl Dag {
23    pub fn new() -> Self {
24        Self::default()
25    }
26
27    pub fn len(&self) -> usize {
28        self.changes.len()
29    }
30
31    pub fn is_empty(&self) -> bool {
32        self.changes.is_empty()
33    }
34
35    pub fn contains(&self, hash: ChangeHash) -> bool {
36        self.changes.contains_key(&hash)
37    }
38
39    pub fn get(&self, hash: ChangeHash) -> Option<&Change> {
40        self.changes.get(&hash)
41    }
42
43    pub fn insert(&mut self, change: Change) -> ChangeHash {
44        let hash = change.hash();
45        self.changes.entry(hash).or_insert(change);
46        hash
47    }
48
49    pub fn iter(&self) -> impl Iterator<Item = (ChangeHash, &Change)> {
50        self.changes.iter().map(|(hash, change)| (*hash, change))
51    }
52
53    /// Parents referenced by changes held here but not themselves held.
54    ///
55    /// Non-empty means either the history is shallow — compacted down to a
56    /// baseline — or changes arrived before their ancestors and want
57    /// buffering.
58    pub fn missing_parents(&self) -> BTreeSet<ChangeHash> {
59        self.changes
60            .values()
61            .flat_map(Change::parents)
62            .copied()
63            .filter(|parent| !self.changes.contains_key(parent))
64            .collect()
65    }
66
67    /// The changes nothing else builds on — what a peer is told about, and
68    /// what the next local change takes as its parents. Sorted, so two
69    /// replicas holding the same history describe it identically.
70    pub fn heads(&self) -> Vec<ChangeHash> {
71        let claimed: BTreeSet<ChangeHash> = self
72            .changes
73            .values()
74            .flat_map(Change::parents)
75            .copied()
76            .collect();
77        self.changes
78            .keys()
79            .copied()
80            .filter(|hash| !claimed.contains(hash))
81            .collect()
82    }
83
84    /// Every change in replay order: parents before children, concurrent
85    /// changes broken by `(lamport, actor)` and finally by hash.
86    ///
87    /// A parent that is not held is treated as already replayed, so a shallow
88    /// history linearizes rather than deadlocking. Whether such a history is
89    /// acceptable is the caller's call — [`missing_parents`](Self::missing_parents)
90    /// is how they find out.
91    pub fn linearize(&self) -> Vec<ChangeHash> {
92        let mut waiting_on: BTreeMap<ChangeHash, usize> = BTreeMap::new();
93        let mut dependents: BTreeMap<ChangeHash, Vec<ChangeHash>> = BTreeMap::new();
94
95        for (hash, change) in &self.changes {
96            let held_parents: Vec<ChangeHash> = change
97                .parents()
98                .iter()
99                .copied()
100                .filter(|p| self.changes.contains_key(p))
101                .collect();
102            waiting_on.insert(*hash, held_parents.len());
103            for parent in held_parents {
104                dependents.entry(parent).or_default().push(*hash);
105            }
106        }
107
108        let mut ready: BinaryHeap<Reverse<Key>> = waiting_on
109            .iter()
110            .filter(|(_, count)| **count == 0)
111            .map(|(hash, _)| Reverse(self.key(*hash)))
112            .collect();
113
114        let mut order = Vec::with_capacity(self.changes.len());
115        while let Some(Reverse(Key { hash, .. })) = ready.pop() {
116            order.push(hash);
117            for dependent in dependents.get(&hash).into_iter().flatten() {
118                let count = waiting_on.entry(*dependent).or_default();
119                *count = count.saturating_sub(1);
120                if *count == 0 {
121                    ready.push(Reverse(self.key(*dependent)));
122                }
123            }
124        }
125        order
126    }
127
128    fn key(&self, hash: ChangeHash) -> Key {
129        let change = self.changes.get(&hash);
130        Key {
131            lamport: change.map_or(Lamport::ZERO, |c| c.lamport()),
132            actor: change.map_or_else(|| ActorId::from_uuid(uuid::Uuid::nil()), |c| c.actor()),
133            hash,
134        }
135    }
136}
137
138/// Sort key for concurrent changes. The hash is the final tiebreak so the
139/// order is total even if two changes somehow shared a stamp.
140#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
141struct Key {
142    lamport: Lamport,
143    actor: ActorId,
144    hash: ChangeHash,
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::log::change::SemanticLabel;
151    use crate::log::command::{Command, ElementKind, PropSet};
152    use crate::log::id::{ElementId, Stamp};
153
154    fn actor(byte: u8) -> ActorId {
155        ActorId::from_uuid(uuid::Uuid::from_bytes([byte; 16]))
156    }
157
158    fn change(
159        lamport: u64,
160        actor_byte: u8,
161        parents: impl IntoIterator<Item = ChangeHash>,
162    ) -> Change {
163        Change::new(
164            Stamp {
165                lamport: Lamport::new(lamport),
166                actor: actor(actor_byte),
167            },
168            parents,
169            SemanticLabel::new(format!("l{lamport}a{actor_byte}")),
170            vec![Command::Create {
171                id: ElementId::new(),
172                kind: ElementKind::Block,
173                parent: ElementId::DOCUMENT,
174                init: PropSet::new(),
175            }],
176        )
177    }
178
179    fn position(order: &[ChangeHash], hash: ChangeHash) -> usize {
180        order
181            .iter()
182            .position(|h| *h == hash)
183            .expect("every held change is linearized")
184    }
185
186    #[test]
187    fn inserting_a_change_twice_holds_it_once() {
188        let mut dag = Dag::new();
189        let c = change(1, 1, []);
190        let first = dag.insert(c.clone());
191        let second = dag.insert(c);
192        assert_eq!(first, second);
193        assert_eq!(dag.len(), 1);
194    }
195
196    #[test]
197    fn a_chain_has_one_head_and_a_fork_has_two() {
198        let mut dag = Dag::new();
199        let root = dag.insert(change(1, 1, []));
200        let next = dag.insert(change(2, 1, [root]));
201        assert_eq!(dag.heads(), vec![next]);
202
203        let branch = dag.insert(change(2, 2, [root]));
204        let mut heads = dag.heads();
205        heads.sort_unstable();
206        let mut expected = vec![next, branch];
207        expected.sort_unstable();
208        assert_eq!(heads, expected);
209    }
210
211    #[test]
212    fn a_merge_change_collapses_the_heads() {
213        let mut dag = Dag::new();
214        let root = dag.insert(change(1, 1, []));
215        let left = dag.insert(change(2, 1, [root]));
216        let right = dag.insert(change(2, 2, [root]));
217        assert_eq!(dag.heads().len(), 2);
218
219        let merged = change(3, 1, [left, right]);
220        assert!(merged.is_merge());
221        let merge = dag.insert(merged);
222        assert_eq!(dag.heads(), vec![merge]);
223    }
224
225    #[test]
226    fn linearization_puts_parents_first() {
227        let mut dag = Dag::new();
228        let root = dag.insert(change(1, 1, []));
229        let left = dag.insert(change(2, 1, [root]));
230        let right = dag.insert(change(2, 2, [root]));
231        let merge = dag.insert(change(3, 1, [left, right]));
232
233        let order = dag.linearize();
234        assert_eq!(order.len(), 4);
235        assert!(position(&order, root) < position(&order, left));
236        assert!(position(&order, root) < position(&order, right));
237        assert!(position(&order, left) < position(&order, merge));
238        assert!(position(&order, right) < position(&order, merge));
239    }
240
241    /// Two replicas receive the same changes in opposite orders; the
242    /// linearization has to be a function of the graph, not of arrival.
243    #[test]
244    fn linearization_does_not_depend_on_insertion_order() {
245        let root = change(1, 1, []);
246        let root_hash = root.hash();
247        let left = change(2, 1, [root_hash]);
248        let right = change(2, 2, [root_hash]);
249        let merge = change(3, 1, [left.hash(), right.hash()]);
250
251        let mut forwards = Dag::new();
252        for c in [&root, &left, &right, &merge] {
253            forwards.insert(c.clone());
254        }
255        let mut backwards = Dag::new();
256        for c in [&merge, &right, &left, &root] {
257            backwards.insert(c.clone());
258        }
259
260        assert_eq!(forwards.linearize(), backwards.linearize());
261    }
262
263    /// Concurrent siblings are ordered by stamp, so the lower lamport comes
264    /// first regardless of hash.
265    #[test]
266    fn concurrent_changes_are_ordered_by_stamp() {
267        let root = change(1, 1, []);
268        let root_hash = root.hash();
269        let earlier = change(2, 9, [root_hash]);
270        let later = change(5, 1, [root_hash]);
271
272        let mut dag = Dag::new();
273        for c in [&root, &later, &earlier] {
274            dag.insert(c.clone());
275        }
276        let order = dag.linearize();
277        assert!(position(&order, earlier.hash()) < position(&order, later.hash()));
278    }
279
280    #[test]
281    fn a_parent_that_is_not_held_is_reported_and_does_not_stall_replay() {
282        let absent = ChangeHash::from_bytes([0xEE; 32]);
283        let mut dag = Dag::new();
284        let orphan = dag.insert(change(4, 1, [absent]));
285
286        assert_eq!(
287            dag.missing_parents(),
288            [absent].into_iter().collect::<BTreeSet<_>>()
289        );
290        assert_eq!(dag.linearize(), vec![orphan]);
291    }
292
293    #[test]
294    fn a_complete_history_reports_no_missing_parents() {
295        let mut dag = Dag::new();
296        let root = dag.insert(change(1, 1, []));
297        dag.insert(change(2, 1, [root]));
298        assert!(dag.missing_parents().is_empty());
299    }
300}