Skip to main content

blockworx/log/
fold.rs

1//! Replay: the one and only writer of document state.
2//!
3//! Deliberately dumb. It reads nothing but the command in front of it and the
4//! register that command names — no I/O, no clock, no randomness, and no
5//! re-running of the logic that produced the command. Anything else would make
6//! two replicas' replays able to disagree, and every guarantee in this module
7//! tree rests on them agreeing.
8
9use crate::log::change::Change;
10use crate::log::command::{Command, PropTag, PropWrite};
11use crate::log::id::{ElementId, WriteOrder};
12use crate::log::state::{DocState, Liveness};
13
14/// A log that cannot be replayed. Both variants mean the caller broke a
15/// precondition rather than that the document is odd: changes must arrive
16/// after their parents, and identities are never reused.
17#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
18pub enum FoldError {
19    /// A command named an element whose create has not been applied. Causal
20    /// delivery makes this unreachable — an author cannot reference what they
21    /// had not seen — so it means changes were fed in out of order. Reported
22    /// rather than skipped: a skipped command forks the state silently.
23    #[error("command {index} names element {id}, which has not been created")]
24    UnknownElement { id: ElementId, index: usize },
25}
26
27/// Apply one change. Every command of the change shares its stamp and is
28/// separated only by its batch index.
29pub fn apply(state: &mut DocState, change: &Change) -> Result<(), FoldError> {
30    for (index, command) in change.commands().iter().enumerate() {
31        apply_command(state, command, change.order_of(index), index)?;
32    }
33    Ok(())
34}
35
36/// Apply a whole sequence, which must already be in causal order.
37pub fn fold<'a>(
38    state: &mut DocState,
39    changes: impl IntoIterator<Item = &'a Change>,
40) -> Result<(), FoldError> {
41    for change in changes {
42        apply(state, change)?;
43    }
44    Ok(())
45}
46
47fn apply_command(
48    state: &mut DocState,
49    command: &Command,
50    order: WriteOrder,
51    index: usize,
52) -> Result<(), FoldError> {
53    let missing = |id| FoldError::UnknownElement { id, index };
54    match command {
55        Command::Create {
56            id,
57            kind,
58            parent,
59            init,
60        } => {
61            state.ensure(*id, *kind, order);
62            for write in init.iter() {
63                // The `parent` field is the authority, so anything the init
64                // set for that register is overwritten below rather than
65                // racing it at equal order.
66                if write.tag() != PropTag::Parent {
67                    state.write(*id, write.tag(), Some(write.clone()), order);
68                }
69            }
70            state.write(
71                *id,
72                PropTag::Parent,
73                Some(PropWrite::Parent(*parent)),
74                order,
75            );
76            state.set_liveness(*id, Liveness::Live, order);
77        }
78        Command::SetProp { id, change } => {
79            state
80                .write(*id, change.tag(), change.new_value(), order)
81                .ok_or_else(|| missing(*id))?;
82        }
83        Command::Delete { id } => {
84            state
85                .set_liveness(*id, Liveness::Deleted, order)
86                .ok_or_else(|| missing(*id))?;
87        }
88        Command::Restore { id } => {
89            state
90                .set_liveness(*id, Liveness::Live, order)
91                .ok_or_else(|| missing(*id))?;
92        }
93    }
94    Ok(())
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::log::change::SemanticLabel;
101    use crate::log::command::{ElementKind, PropChange, PropSet};
102    use crate::log::encode;
103    use crate::log::id::{ActorId, Lamport, Stamp};
104
105    fn actor(byte: u8) -> ActorId {
106        ActorId::from_uuid(uuid::Uuid::from_bytes([byte; 16]))
107    }
108
109    fn element(byte: u8) -> ElementId {
110        ElementId::from_uuid(uuid::Uuid::from_bytes([byte; 16]))
111    }
112
113    fn change(lamport: u64, actor_byte: u8, commands: Vec<Command>) -> Change {
114        Change::new(
115            Stamp {
116                lamport: Lamport::new(lamport),
117                actor: actor(actor_byte),
118            },
119            [],
120            SemanticLabel::new("test"),
121            commands,
122        )
123    }
124
125    fn create(id: ElementId) -> Command {
126        Command::Create {
127            id,
128            kind: ElementKind::Block,
129            parent: ElementId::DOCUMENT,
130            init: PropSet::new(),
131        }
132    }
133
134    fn set_accent(id: ElementId, from: Option<u8>, to: u8) -> Command {
135        Command::SetProp {
136            id,
137            change: PropChange::Accent {
138                old: from,
139                new: Some(to),
140            },
141        }
142    }
143
144    fn folded(changes: &[Change]) -> DocState {
145        let mut state = DocState::new();
146        fold(&mut state, changes).expect("the fixture is in causal order");
147        state
148    }
149
150    #[test]
151    fn a_create_seeds_the_parent_register_so_a_move_is_an_ordinary_write() {
152        let id = element(1);
153        let state = folded(&[change(1, 1, vec![create(id)])]);
154        assert_eq!(
155            state.get(id, PropTag::Parent),
156            Some(&PropWrite::Parent(ElementId::DOCUMENT))
157        );
158    }
159
160    /// The create's own `parent` field is what the element's parent is; an
161    /// init write for the same register cannot quietly win the tie.
162    #[test]
163    fn the_create_field_beats_a_parent_in_the_init_set() {
164        let id = element(1);
165        let state = folded(&[change(
166            1,
167            1,
168            vec![Command::Create {
169                id,
170                kind: ElementKind::Block,
171                parent: ElementId::DOCUMENT,
172                init: PropSet::new().with(PropWrite::Parent(element(9))),
173            }],
174        )]);
175        assert_eq!(
176            state.get(id, PropTag::Parent),
177            Some(&PropWrite::Parent(ElementId::DOCUMENT))
178        );
179    }
180
181    /// Concurrent writes to one register, delivered both ways round. This is
182    /// the property the whole design exists to have.
183    #[test]
184    fn concurrent_writes_converge_whichever_order_they_arrive() {
185        let id = element(1);
186        let seed = change(1, 0, vec![create(id)]);
187        let alice = change(5, 1, vec![set_accent(id, None, 1)]);
188        let bob = change(5, 2, vec![set_accent(id, None, 2)]);
189
190        let one = folded(&[seed.clone(), alice.clone(), bob.clone()]);
191        let other = folded(&[seed, bob, alice]);
192
193        assert_eq!(one, other);
194        assert_eq!(
195            one.get(id, PropTag::Accent),
196            Some(&PropWrite::Accent(2)),
197            "equal lamports break toward the higher actor id"
198        );
199    }
200
201    #[test]
202    fn delivering_the_same_change_twice_changes_nothing() {
203        let id = element(1);
204        let seed = change(1, 1, vec![create(id)]);
205        let edit = change(2, 1, vec![set_accent(id, None, 4)]);
206
207        let once = folded(&[seed.clone(), edit.clone()]);
208        let twice = folded(&[seed.clone(), edit.clone(), edit, seed]);
209        assert_eq!(once, twice);
210    }
211
212    /// A change that writes the same register twice is ordered only by batch
213    /// index, so the second write must land.
214    #[test]
215    fn a_batch_writing_one_register_twice_ends_on_its_last_write() {
216        let id = element(1);
217        let state = folded(&[change(
218            1,
219            1,
220            vec![
221                create(id),
222                set_accent(id, None, 1),
223                set_accent(id, Some(1), 2),
224            ],
225        )]);
226        assert_eq!(state.get(id, PropTag::Accent), Some(&PropWrite::Accent(2)));
227    }
228
229    #[test]
230    fn an_edit_of_an_uncreated_element_is_refused_rather_than_dropped() {
231        let mut state = DocState::new();
232        let id = element(3);
233        let result = apply(&mut state, &change(1, 1, vec![set_accent(id, None, 1)]));
234        assert_eq!(result, Err(FoldError::UnknownElement { id, index: 0 }));
235    }
236
237    #[test]
238    fn deleting_hides_an_element_but_keeps_it_restorable() {
239        let id = element(1);
240        let mut state = folded(&[change(1, 1, vec![create(id), Command::Delete { id }])]);
241
242        assert!(state.live(id).is_none());
243        assert!(state.contains(id), "the tombstone keeps the identity");
244
245        apply(&mut state, &change(2, 1, vec![Command::Restore { id }]))
246            .expect("restoring a tombstone");
247        assert!(state.live(id).is_some());
248    }
249
250    /// Edit-versus-delete resolves by order, not by which arrived first.
251    #[test]
252    fn an_edit_concurrent_with_a_delete_converges() {
253        let id = element(1);
254        let seed = change(1, 0, vec![create(id)]);
255        let edit = change(4, 1, vec![set_accent(id, None, 5)]);
256        let delete = change(6, 2, vec![Command::Delete { id }]);
257
258        let one = folded(&[seed.clone(), edit.clone(), delete.clone()]);
259        let other = folded(&[seed, delete, edit]);
260
261        assert_eq!(one, other);
262        assert!(
263            one.live(id).is_none(),
264            "the later-ordered delete decides liveness"
265        );
266        assert_eq!(
267            one.element(id).expect("tombstone").get(PropTag::Accent),
268            Some(&PropWrite::Accent(5)),
269            "the losing edit is kept, so a restore brings it back with the edit"
270        );
271    }
272
273    /// Two replicas that saw the same changes in different orders must not
274    /// merely be `==` — they must encode identically, since that is what a
275    /// snapshot and a sync comparison actually use.
276    #[test]
277    fn replicas_agree_byte_for_byte_not_just_structurally() {
278        let (a, b) = (element(1), element(2));
279        let seed = change(1, 0, vec![create(a), create(b)]);
280        let alice = change(3, 1, vec![set_accent(a, None, 1)]);
281        let bob = change(3, 2, vec![set_accent(b, None, 2)]);
282
283        let one = encode::state_to_bytes(&folded(&[seed.clone(), alice.clone(), bob.clone()]));
284        let other = encode::state_to_bytes(&folded(&[seed, bob, alice]));
285        assert_eq!(one, other);
286    }
287
288    #[test]
289    fn replay_is_a_function_of_the_log_alone() {
290        let id = element(1);
291        let log = [
292            change(1, 1, vec![create(id)]),
293            change(2, 1, vec![set_accent(id, None, 1)]),
294        ];
295        assert_eq!(
296            encode::state_to_bytes(&folded(&log)),
297            encode::state_to_bytes(&folded(&log)),
298            "two replays of one log must agree"
299        );
300    }
301}