Skip to main content

aranya_runtime/client/
transaction.rs

1use alloc::collections::{BTreeMap, VecDeque};
2use core::{marker::PhantomData, mem};
3
4use buggy::{BugExt as _, bug};
5
6use super::braiding;
7use crate::{
8    Address, BraidBuffer, ClientError, CmdId, Command, GraphId, Location, MAX_COMMAND_LENGTH,
9    MergeIds, Perspective as _, Policy as _, PolicyError, PolicyId, PolicyStore, Prior,
10    Revertable as _, RuntimeBuffers, Segment as _, Sink, Storage, StorageError, StorageProvider,
11    TraversalBuffer, policy::CommandPlacement, storage::Spill,
12};
13
14/// Transaction used to receive many commands at once.
15///
16/// The transaction allows us to have many temporary heads at once, so we don't
17/// need as many merges when adding commands. When the transaction is committed,
18/// we will merge all temporary heads and the graph head, and then commit the
19/// result as the new graph head.
20pub struct Transaction<SP: StorageProvider, PS> {
21    /// The ID of the associated graph
22    graph_id: GraphId,
23    /// The head of the graph when this transaction is first used.
24    original_head: Option<Location>,
25    /// Current working perspective
26    perspective: Option<SP::Perspective>,
27    /// Head of the current perspective
28    phead: Option<CmdId>,
29    /// Written but not committed heads
30    heads: BTreeMap<CmdId, Location>,
31    /// Tag for associated policy store
32    policy_store: PhantomData<PS>,
33}
34
35impl<SP: StorageProvider, PS> Transaction<SP, PS> {
36    pub(super) const fn new(graph_id: GraphId) -> Self {
37        Self {
38            graph_id,
39            original_head: None,
40            perspective: None,
41            phead: None,
42            heads: BTreeMap::new(),
43            policy_store: PhantomData,
44        }
45    }
46}
47
48impl<SP: StorageProvider, PS: PolicyStore> Transaction<SP, PS> {
49    /// Returns the transaction's graph id.
50    pub fn graph_id(&self) -> GraphId {
51        self.graph_id
52    }
53
54    /// Find a given id if reachable within this transaction.
55    ///
56    /// Does not search `self.perspective`, which should be written out beforehand.
57    fn locate(
58        &self,
59        storage: &mut SP::Storage,
60        address: Address,
61        buffer: &mut TraversalBuffer,
62    ) -> Result<Option<Location>, ClientError> {
63        // Search from committed head.
64        if let Some(found) = storage.get_location(address, buffer)? {
65            return Ok(Some(found));
66        }
67        // Search from our temporary heads.
68        for &head in self.heads.values() {
69            if let Some(found) = storage.get_location_from(head, address, buffer)? {
70                return Ok(Some(found));
71            }
72        }
73        Ok(None)
74    }
75
76    /// Write current perspective, merge transaction heads, and commit to graph.
77    pub(super) fn commit<F, MS>(
78        mut self,
79        provider: &mut SP,
80        policy_store: &mut PS,
81        sink: &mut impl Sink<PS::Effect>,
82        buffers: &mut RuntimeBuffers<SP::Segment>,
83        make_spill: &MS,
84    ) -> Result<bool, ClientError>
85    where
86        F: Spill,
87        MS: Fn() -> Result<F, StorageError>,
88    {
89        let storage = provider.get_storage(self.graph_id)?;
90
91        let Some(original_head) = self.original_head else {
92            return Ok(false);
93        };
94        if original_head != storage.get_head()? {
95            return Err(ClientError::ConcurrentTransaction);
96        }
97
98        // Write out current perspective.
99        if let Some(p) = Option::take(&mut self.perspective) {
100            self.phead = None;
101            let segment = storage.write(p)?;
102            self.heads
103                .insert(segment.head_id(), segment.head_location()?);
104        }
105
106        if self.heads.is_empty() {
107            return Ok(false);
108        }
109
110        // Merge heads pairwise until single head left, then commit.
111        // TODO(#370): Merge deterministically
112        let mut heads: VecDeque<_> = mem::take(&mut self.heads).into_iter().collect();
113        let mut merging_head = false;
114        while let Some((left_id, mut left_loc)) = heads.pop_front() {
115            if let Some((right_id, mut right_loc)) = heads.pop_front() {
116                let (policy, policy_id) =
117                    choose_policy(storage, policy_store, left_loc, right_loc)?;
118
119                let mut buf = [0u8; MAX_COMMAND_LENGTH];
120                let merge_ids = MergeIds::new(
121                    Address {
122                        id: left_id,
123                        max_cut: left_loc.max_cut,
124                    },
125                    Address {
126                        id: right_id,
127                        max_cut: right_loc.max_cut,
128                    },
129                )
130                .assume("merging different ids")?;
131                if left_id > right_id {
132                    mem::swap(&mut left_loc, &mut right_loc);
133                }
134                let command = policy.merge(&mut buf, merge_ids)?;
135
136                let (braid, last_common_ancestor) = make_braid_segment::<_, PS, F, MS>(
137                    storage,
138                    left_loc,
139                    right_loc,
140                    sink,
141                    policy,
142                    &mut buffers.traversal.primary,
143                    &mut buffers.braid,
144                    make_spill,
145                )?;
146
147                let mut perspective = storage.new_merge_perspective(
148                    left_loc,
149                    right_loc,
150                    last_common_ancestor,
151                    policy_id,
152                    braid,
153                )?;
154                perspective.add_command(&command)?;
155
156                let segment = storage.write(perspective)?;
157                heads.push_back((segment.head_id(), segment.head_location()?));
158            } else if storage.is_ancestor(
159                storage.get_head()?,
160                left_loc,
161                &mut buffers.traversal.primary,
162            )? {
163                let segment = storage.get_segment(left_loc)?;
164                storage.commit(segment)?;
165                debug_assert!(heads.is_empty());
166            } else {
167                if merging_head {
168                    bug!("merging with graph head again, would loop");
169                }
170                merging_head = true;
171
172                heads.push_back((left_id, left_loc));
173
174                let head_loc = storage.get_head()?;
175                let segment = storage.get_segment(head_loc)?;
176                heads.push_back((segment.head_id(), segment.head_location()?));
177            }
178        }
179
180        Ok(true)
181    }
182
183    /// Attempt to store the `command` in the graph with `graph_id`. Effects will be
184    /// emitted to the `sink`. This interface is used when syncing with another device
185    /// and integrating the new commands.
186    pub(super) fn add_commands<F, MS>(
187        &mut self,
188        commands: &[impl Command],
189        provider: &mut SP,
190        policy_store: &mut PS,
191        sink: &mut impl Sink<PS::Effect>,
192        buffers: &mut RuntimeBuffers<SP::Segment>,
193        make_spill: &MS,
194    ) -> Result<usize, ClientError>
195    where
196        F: Spill,
197        MS: Fn() -> Result<F, StorageError>,
198    {
199        let mut commands = commands.iter();
200        let mut count: usize = 0;
201
202        // Get storage or try to initialize with first command.
203        let storage = match provider.get_storage(self.graph_id) {
204            Ok(s) => s,
205            Err(StorageError::NoSuchStorage) => {
206                let command = commands.next().ok_or(ClientError::InitError)?;
207                count = count.checked_add(1).assume("must not overflow")?;
208                self.init(command, policy_store, provider, sink)?
209            }
210            Err(e) => return Err(e.into()),
211        };
212
213        if self.original_head.is_none() {
214            self.original_head = Some(storage.get_head()?);
215        }
216
217        // Handle remaining commands.
218        for command in commands {
219            if self
220                .perspective
221                .as_ref()
222                .is_some_and(|p| p.includes(command.id()))
223            {
224                // Command in current perspective.
225                continue;
226            }
227
228            if self
229                .locate(storage, command.address()?, &mut buffers.traversal.primary)?
230                .is_some()
231            {
232                // Command already added.
233                continue;
234            }
235            match command.parent() {
236                Prior::None => {
237                    if command.id().as_base() == self.graph_id.as_base() {
238                        // Graph already initialized, extra init just spurious
239                    } else {
240                        bug!("init command does not belong in graph");
241                    }
242                }
243                Prior::Single(parent) => {
244                    self.add_single(
245                        storage,
246                        policy_store,
247                        sink,
248                        command,
249                        parent,
250                        &mut buffers.traversal.primary,
251                    )?;
252                    count = count.checked_add(1).assume("must not overflow")?;
253                }
254                Prior::Merge(left, right) => {
255                    self.add_merge::<F, MS>(
256                        storage,
257                        policy_store,
258                        sink,
259                        command,
260                        (left, right),
261                        buffers,
262                        make_spill,
263                    )?;
264                    count = count.checked_add(1).assume("must not overflow")?;
265                }
266            }
267        }
268
269        Ok(count)
270    }
271
272    fn add_single(
273        &mut self,
274        storage: &mut <SP as StorageProvider>::Storage,
275        policy_store: &mut PS,
276        sink: &mut impl Sink<PS::Effect>,
277        command: &impl Command,
278        parent: Address,
279        buffer: &mut TraversalBuffer,
280    ) -> Result<(), ClientError> {
281        let perspective = self.get_perspective(parent, storage, buffer)?;
282
283        let policy_id = perspective.policy();
284        let policy = policy_store.get_policy(policy_id)?;
285
286        // Try to run command, or revert if failed.
287        sink.begin();
288        let checkpoint = perspective.checkpoint();
289        if let Err(e) = policy.call_rule(
290            command,
291            perspective,
292            sink,
293            CommandPlacement::OnGraphAtOrigin,
294        ) {
295            perspective.revert(checkpoint)?;
296            sink.rollback();
297            return Err(e.into());
298        }
299        perspective.add_command(command)?;
300        sink.commit();
301
302        self.phead = Some(command.id());
303
304        Ok(())
305    }
306
307    #[allow(clippy::too_many_arguments)]
308    fn add_merge<F, MS>(
309        &mut self,
310        storage: &mut <SP as StorageProvider>::Storage,
311        policy_store: &mut PS,
312        sink: &mut impl Sink<PS::Effect>,
313        command: &impl Command,
314        (left, right): (Address, Address),
315        buffers: &mut RuntimeBuffers<SP::Segment>,
316        make_spill: &MS,
317    ) -> Result<bool, ClientError>
318    where
319        F: Spill,
320        MS: Fn() -> Result<F, StorageError>,
321    {
322        // Must always start a new perspective for merges.
323        if let Some(p) = Option::take(&mut self.perspective) {
324            let seg = storage.write(p)?;
325            self.heads.insert(seg.head_id(), seg.head_location()?);
326        }
327
328        let left_loc = self
329            .locate(storage, left, &mut buffers.traversal.primary)?
330            .ok_or(ClientError::NoSuchParent(left.id))?;
331        let right_loc = self
332            .locate(storage, right, &mut buffers.traversal.primary)?
333            .ok_or(ClientError::NoSuchParent(right.id))?;
334
335        let (policy, policy_id) = choose_policy(storage, policy_store, left_loc, right_loc)?;
336
337        // Braid commands from left and right into an ordered sequence.
338        let (braid, last_common_ancestor) = make_braid_segment::<_, PS, F, MS>(
339            storage,
340            left_loc,
341            right_loc,
342            sink,
343            policy,
344            &mut buffers.traversal.primary,
345            &mut buffers.braid,
346            make_spill,
347        )?;
348
349        let mut perspective = storage.new_merge_perspective(
350            left_loc,
351            right_loc,
352            last_common_ancestor,
353            policy_id,
354            braid,
355        )?;
356        perspective.add_command(command)?;
357
358        // These are no longer heads of the transaction, since they are both covered by the merge
359        self.heads.remove(&left.id);
360        self.heads.remove(&right.id);
361
362        self.perspective = Some(perspective);
363        self.phead = Some(command.id());
364
365        Ok(true)
366    }
367
368    /// Get a perspective to which we can add a command with the given parant.
369    ///
370    /// If parent is the head of the current perspective, we can just use it.
371    /// Otherwise, we must write out the perspective and get a new one.
372    fn get_perspective(
373        &mut self,
374        parent: Address,
375        storage: &mut <SP as StorageProvider>::Storage,
376        buffer: &mut TraversalBuffer,
377    ) -> Result<&mut <SP as StorageProvider>::Perspective, ClientError> {
378        if self.phead == Some(parent.id) {
379            // Command will append to current perspective.
380            return Ok(self
381                .perspective
382                .as_mut()
383                .assume("trx has perspective when has phead")?);
384        }
385
386        // Write out the current perspective.
387        if let Some(p) = Option::take(&mut self.perspective) {
388            self.phead = None;
389            let seg = storage.write(p)?;
390            self.heads.insert(seg.head_id(), seg.head_location()?);
391        }
392
393        let loc = self
394            .locate(storage, parent, buffer)?
395            .ok_or(ClientError::NoSuchParent(parent.id))?;
396
397        // Get a new perspective and store it in the transaction.
398        let p = self
399            .perspective
400            .insert(storage.get_linear_perspective(loc)?);
401
402        self.phead = Some(parent.id);
403        self.heads.remove(&parent.id);
404
405        Ok(p)
406    }
407
408    fn init<'sp>(
409        &mut self,
410        command: &impl Command,
411        policy_store: &mut PS,
412        provider: &'sp mut SP,
413        sink: &mut impl Sink<PS::Effect>,
414    ) -> Result<&'sp mut <SP as StorageProvider>::Storage, ClientError> {
415        // Graph ID is the id of the init command by definition.
416        if self.graph_id.as_base() != command.id().as_base() {
417            return Err(ClientError::InitError);
418        }
419
420        // The init command must not have a parent.
421        if !matches!(command.parent(), Prior::None) {
422            return Err(ClientError::InitError);
423        }
424
425        // The graph must have policy to start with.
426        let Some(policy_data) = command.policy() else {
427            return Err(ClientError::InitError);
428        };
429
430        let policy_id = policy_store.add_policy(policy_data)?;
431        let policy = policy_store.get_policy(policy_id)?;
432
433        // Get an empty perspective and run the init command.
434        let mut perspective = provider.new_perspective(policy_id);
435        sink.begin();
436        if let Err(e) = policy.call_rule(
437            command,
438            &mut perspective,
439            sink,
440            CommandPlacement::OnGraphAtOrigin,
441        ) {
442            sink.rollback();
443            // We don't need to revert perspective since we just drop it.
444            return Err(e.into());
445        }
446        perspective.add_command(command)?;
447
448        let (_, storage) = provider.new_storage(perspective)?;
449
450        // Wait to commit until we are absolutely sure we've initialized.
451        sink.commit();
452
453        Ok(storage)
454    }
455}
456
457/// Run the braid algorithm and evaluate the sequence to create a braided fact index.
458#[allow(clippy::too_many_arguments)]
459fn make_braid_segment<S, PS, F, MS>(
460    storage: &mut S,
461    left: Location,
462    right: Location,
463    sink: &mut impl Sink<PS::Effect>,
464    policy: &PS::Policy,
465    traversal: &mut TraversalBuffer,
466    braid_buf: &mut BraidBuffer<S::Segment>,
467    make_spill: &MS,
468) -> Result<(S::FactIndex, Location), ClientError>
469where
470    S: Storage,
471    PS: PolicyStore,
472    F: Spill,
473    MS: Fn() -> Result<F, StorageError>,
474{
475    let last_common_ancestor = braiding::last_common_ancestor(storage, left, right)?;
476    let mut order = braiding::braid::<_, F, MS>(
477        storage,
478        left,
479        right,
480        last_common_ancestor,
481        traversal,
482        braid_buf,
483        make_spill,
484    )?;
485
486    let mut iter = order.iter()?;
487    let first = iter.next().assume("braid is non-empty")??;
488
489    let mut braid_perspective = storage.get_fact_perspective(first)?;
490
491    sink.begin();
492
493    while let Some(location) = iter.next().transpose()? {
494        let segment = storage.get_segment(location)?;
495        let command = segment
496            .get_command(location)
497            .assume("braid only contains existing commands")?;
498
499        let result = policy.call_rule(
500            &command,
501            &mut braid_perspective,
502            sink,
503            CommandPlacement::OnGraphInBraid,
504        );
505
506        // If the command failed in an uncontrolled way, rollback
507        if let Err(e) = result
508            && !matches!(e, PolicyError::Check)
509        {
510            sink.rollback();
511            return Err(e.into());
512        }
513    }
514
515    let braid = storage.write_facts(braid_perspective)?;
516
517    sink.commit();
518
519    Ok((braid, last_common_ancestor))
520}
521
522/// Select the policy from two locations with the greatest serial value.
523fn choose_policy<'a, PS: PolicyStore>(
524    storage: &impl Storage,
525    policy_store: &'a PS,
526    left: Location,
527    right: Location,
528) -> Result<(&'a PS::Policy, PolicyId), ClientError> {
529    Ok(core::cmp::max_by_key(
530        get_policy(storage, policy_store, left)?,
531        get_policy(storage, policy_store, right)?,
532        |(p, _)| p.serial(),
533    ))
534}
535
536fn get_policy<'a, PS: PolicyStore>(
537    storage: &impl Storage,
538    policy_store: &'a PS,
539    location: Location,
540) -> Result<(&'a PS::Policy, PolicyId), ClientError> {
541    let segment = storage.get_segment(location)?;
542    let policy_id = segment.policy();
543    let policy = policy_store.get_policy(policy_id)?;
544    Ok((policy, policy_id))
545}
546
547#[cfg(test)]
548mod test {
549    use std::collections::HashMap;
550
551    use aranya_crypto::id::{Id, IdTag};
552    use buggy::Bug;
553    use test_log::test;
554
555    use super::*;
556    use crate::{
557        Bytes, ClientState, Keys, MaxCut, MemSpill, MergeIds, Perspective, Policy, Priority,
558        policy::{ActionPlacement, CommandPlacement},
559        storage::linear::testing::MemStorageProvider,
560        testing::{hash_for_testing_only, short_b58},
561    };
562
563    struct SeqPolicyStore;
564
565    /// [`SeqPolicy`] is a very simple policy which appends the id of each
566    /// command to a fact named `b"seq"`. At each point in the graph, the value
567    /// of this fact should be equal to the ids in braid order of all facts up
568    /// to that point.
569    struct SeqPolicy;
570
571    struct SeqCommand {
572        id: CmdId,
573        prior: Prior<Address>,
574        finalize: bool,
575        data: Box<str>,
576        max_cut: MaxCut,
577    }
578
579    impl PolicyStore for SeqPolicyStore {
580        type Policy = SeqPolicy;
581        type Effect = ();
582
583        fn add_policy(&mut self, _policy: &[u8]) -> Result<PolicyId, PolicyError> {
584            Ok(PolicyId::new(0))
585        }
586
587        fn get_policy(&self, _id: PolicyId) -> Result<&Self::Policy, PolicyError> {
588            Ok(&SeqPolicy)
589        }
590    }
591
592    impl Policy for SeqPolicy {
593        type Action<'a> = &'a str;
594        type Effect = ();
595        type Command<'a> = SeqCommand;
596
597        fn serial(&self) -> u32 {
598            0
599        }
600
601        fn call_rule(
602            &self,
603            command: &impl Command,
604            facts: &mut impl crate::FactPerspective,
605            _sink: &mut impl Sink<Self::Effect>,
606            _placement: CommandPlacement,
607        ) -> Result<(), PolicyError> {
608            assert!(
609                !matches!(command.parent(), Prior::Merge { .. }),
610                "merges shouldn't be evaluated"
611            );
612
613            let data = command.bytes();
614            // (q)uiet commmands add no facts so we can test that.
615            if !data.starts_with(b"q") {
616                // For init and basic commands, append the id to the seq fact.
617                if let Some(seq) = facts
618                    .query("seq", &Keys::default())
619                    .assume("can query")?
620                    .as_deref()
621                {
622                    facts
623                        .insert(
624                            "seq".into(),
625                            Keys::default(),
626                            [seq, b":", data].concat().into(),
627                        )
628                        .unwrap();
629                } else {
630                    facts
631                        .insert("seq".into(), Keys::default(), data.into())
632                        .unwrap();
633                }
634            }
635            Ok(())
636        }
637
638        fn call_action(
639            &self,
640            _action: Self::Action<'_>,
641            _facts: &mut impl Perspective,
642            _sink: &mut impl Sink<Self::Effect>,
643            _placement: ActionPlacement,
644        ) -> Result<(), PolicyError> {
645            unimplemented!()
646        }
647
648        fn merge<'a>(
649            &self,
650            _target: &'a mut [u8],
651            ids: MergeIds,
652        ) -> Result<Self::Command<'a>, PolicyError> {
653            let (left, right): (Address, Address) = ids.into();
654            let parents = [*left.id.as_array(), *right.id.as_array()];
655            let id = hash_for_testing_only(parents.as_flattened());
656
657            Ok(SeqCommand::new(
658                id,
659                Prior::Merge(left, right),
660                left.max_cut
661                    .max(right.max_cut)
662                    .checked_add(1)
663                    .assume("must not overflow")?,
664            ))
665        }
666    }
667
668    impl SeqCommand {
669        fn new(id: CmdId, prior: Prior<Address>, max_cut: MaxCut) -> Self {
670            let data = short_b58(id).into_boxed_str();
671            Self {
672                id,
673                prior,
674                finalize: false,
675                data,
676                max_cut,
677            }
678        }
679
680        fn finalize(id: CmdId, prev: Address, max_cut: MaxCut) -> Self {
681            let data = short_b58(id).into_boxed_str();
682            Self {
683                id,
684                prior: Prior::Single(prev),
685                finalize: true,
686                data,
687                max_cut,
688            }
689        }
690    }
691
692    impl Command for SeqCommand {
693        fn priority(&self) -> Priority {
694            if self.finalize {
695                return Priority::Finalize;
696            }
697            match self.prior {
698                Prior::None => Priority::Init,
699                Prior::Single(_) => {
700                    // Use the last byte of the ID as priority, just so we can
701                    // properly see the effects of braiding
702                    let id = self.id.as_bytes();
703                    let priority = u32::from(*id.last().unwrap());
704                    Priority::Basic(priority)
705                }
706                Prior::Merge(_, _) => Priority::Merge,
707            }
708        }
709
710        fn id(&self) -> CmdId {
711            self.id
712        }
713
714        fn parent(&self) -> Prior<Address> {
715            self.prior
716        }
717
718        fn policy(&self) -> Option<&[u8]> {
719            // We don't actually need any policy bytes, but the
720            // transaction/storage requires it on init commands.
721            match self.prior {
722                Prior::None => Some(b""),
723                _ => None,
724            }
725        }
726
727        fn bytes(&self) -> &[u8] {
728            self.data.as_bytes()
729        }
730
731        fn max_cut(&self) -> Result<MaxCut, Bug> {
732            Ok(self.max_cut)
733        }
734    }
735
736    struct NullSink;
737    impl<Eff> Sink<Eff> for NullSink {
738        fn begin(&mut self) {}
739        fn consume(&mut self, _: Eff) {}
740        fn rollback(&mut self) {}
741        fn commit(&mut self) {}
742    }
743
744    /// [`GraphBuilder`] and the associated macro [`graph`] provide an easy way
745    /// to create a graph with a specific structure.
746    struct GraphBuilder<SP: StorageProvider> {
747        client: ClientState<SeqPolicyStore, SP>,
748        trx: Transaction<SP, SeqPolicyStore>,
749        max_cuts: HashMap<CmdId, MaxCut>,
750        buffers: RuntimeBuffers<SP::Segment>,
751    }
752
753    impl<SP: StorageProvider> GraphBuilder<SP> {
754        pub fn init(
755            mut client: ClientState<SeqPolicyStore, SP>,
756            ids: &[CmdId],
757        ) -> Result<Self, ClientError> {
758            let mut trx = Transaction::new(GraphId::transmute(ids[0]));
759            let mut prior: Prior<Address> = Prior::None;
760            let mut max_cuts = HashMap::new();
761            let mut buffers = RuntimeBuffers::new();
762            for (max_cut, &id) in ids.iter().enumerate() {
763                let max_cut = MaxCut::new(max_cut as u64);
764                let cmd = SeqCommand::new(id, prior, max_cut);
765                trx.add_commands(
766                    &[cmd],
767                    &mut client.provider,
768                    &mut client.policy_store,
769                    &mut NullSink,
770                    &mut buffers,
771                    &MemSpill::new,
772                )?;
773                max_cuts.insert(id, max_cut);
774                prior = Prior::Single(Address { id, max_cut });
775            }
776            Ok(Self {
777                client,
778                trx,
779                max_cuts,
780                buffers,
781            })
782        }
783
784        fn get_addr(&self, id: CmdId) -> Address {
785            let max_cut = *self
786                .max_cuts
787                .get(&id)
788                .unwrap_or_else(|| panic!("bad ID {id}"));
789            Address { id, max_cut }
790        }
791
792        pub fn line(&mut self, prev: CmdId, ids: &[CmdId]) -> Result<(), ClientError> {
793            let mut prev = self.get_addr(prev);
794            for &id in ids {
795                let max_cut = prev.max_cut.checked_add(1).unwrap();
796                let cmd = SeqCommand::new(id, Prior::Single(prev), max_cut);
797                self.trx.add_commands(
798                    &[cmd],
799                    &mut self.client.provider,
800                    &mut self.client.policy_store,
801                    &mut NullSink,
802                    &mut self.buffers,
803                    &MemSpill::new,
804                )?;
805                self.max_cuts.insert(id, max_cut);
806                prev = Address { id, max_cut };
807            }
808            Ok(())
809        }
810
811        pub fn finalize(&mut self, prev: CmdId, id: CmdId) -> Result<(), ClientError> {
812            let prev = self.get_addr(prev);
813            let max_cut = prev.max_cut.checked_add(1).unwrap();
814            let cmd = SeqCommand::finalize(id, prev, max_cut);
815            self.trx.add_commands(
816                &[cmd],
817                &mut self.client.provider,
818                &mut self.client.policy_store,
819                &mut NullSink,
820                &mut self.buffers,
821                &MemSpill::new,
822            )?;
823            self.max_cuts.insert(id, max_cut);
824            Ok(())
825        }
826
827        pub fn merge(
828            &mut self,
829            (left, right): (CmdId, CmdId),
830            ids: &[CmdId],
831        ) -> Result<(), ClientError> {
832            let prior = Prior::Merge(self.get_addr(left), self.get_addr(right));
833            let mergecmd = SeqCommand::new(ids[0], prior, prior.next_max_cut().unwrap());
834            let mut prev = Address {
835                id: mergecmd.id,
836                max_cut: mergecmd.max_cut,
837            };
838            self.max_cuts.insert(mergecmd.id, mergecmd.max_cut);
839            self.trx.add_commands(
840                &[mergecmd],
841                &mut self.client.provider,
842                &mut self.client.policy_store,
843                &mut NullSink,
844                &mut self.buffers,
845                &MemSpill::new,
846            )?;
847            for &id in &ids[1..] {
848                let cmd = SeqCommand::new(
849                    id,
850                    Prior::Single(prev),
851                    prev.max_cut.checked_add(1).expect("must not overflow"),
852                );
853                prev = Address {
854                    id: cmd.id,
855                    max_cut: cmd.max_cut,
856                };
857                self.max_cuts.insert(cmd.id, cmd.max_cut);
858                self.trx.add_commands(
859                    &[cmd],
860                    &mut self.client.provider,
861                    &mut self.client.policy_store,
862                    &mut NullSink,
863                    &mut self.buffers,
864                    &MemSpill::new,
865                )?;
866            }
867            Ok(())
868        }
869
870        pub fn flush(&mut self) {
871            if let Some(p) = Option::take(&mut self.trx.perspective) {
872                self.trx.phead = None;
873                let seg = self
874                    .client
875                    .provider
876                    .get_storage(self.trx.graph_id)
877                    .unwrap()
878                    .write(p)
879                    .unwrap();
880                self.trx
881                    .heads
882                    .insert(seg.head_id(), seg.head_location().unwrap());
883            }
884        }
885
886        pub fn commit(&mut self) -> Result<(), ClientError> {
887            let graph_id = self.trx.graph_id;
888            let trx = mem::replace(&mut self.trx, Transaction::new(graph_id));
889            assert!(trx.commit(
890                &mut self.client.provider,
891                &mut self.client.policy_store,
892                &mut NullSink,
893                &mut self.buffers,
894                &MemSpill::new,
895            )?);
896            Ok(())
897        }
898    }
899
900    fn mkid<Tag: IdTag>(x: &str) -> Id<Tag> {
901        x.parse().unwrap()
902    }
903
904    /// See tests for usage.
905    macro_rules! graph {
906        ( $client:expr ; $init:literal $($inits:literal )* ; $($rest:tt)*) => {{
907            let mut gb = GraphBuilder::init($client, &[mkid($init), $(mkid($inits)),*]).unwrap();
908            graph!(@ gb, $($rest)*);
909            gb
910        }};
911        (@ $gb:ident, $prev:literal < $($id:literal)+; $($rest:tt)*) => {
912            $gb.line(mkid($prev), &[$(mkid($id)),+]).unwrap();
913            graph!(@ $gb, $($rest)*);
914        };
915        (@ $gb:ident, $l:literal $r:literal < $($id:literal)+; $($rest:tt)*) => {
916            $gb.merge((mkid($l), mkid($r)), &[$(mkid($id)),+]).unwrap();
917            graph!(@ $gb, $($rest)*);
918        };
919        (@ $gb:ident, $prev:literal < finalize $id:literal; $($rest:tt)*) => {
920            $gb.finalize(mkid($prev), mkid($id)).unwrap();
921            graph!(@ $gb, $($rest)*);
922        };
923        (@ $gb:ident, commit; $($rest:tt)*) => {
924            $gb.commit().unwrap();
925            graph!(@ $gb, $($rest)*);
926        };
927        (@ $gb:ident, ) => {
928            $gb.flush();
929        };
930    }
931
932    fn lookup(storage: &impl Storage, name: &str) -> Option<Bytes> {
933        use crate::Query as _;
934        let head = storage.get_head().unwrap();
935        let p = storage.get_fact_perspective(head).unwrap();
936        p.query(name, &[]).unwrap()
937    }
938
939    #[test]
940    fn test_simple() -> Result<(), StorageError> {
941        let mut gb = graph! {
942            ClientState::new(SeqPolicyStore, MemStorageProvider::default());
943            "a";
944            "a" < "b";
945            "a" < "c";
946            "b" "c" < "ma";
947            "b" < "d";
948            "ma" "d" < "mb";
949            commit;
950        };
951        let g = gb.client.provider.get_storage(mkid("a")).unwrap();
952
953        #[cfg(feature = "graphviz")]
954        graphviz::dot(g, "simple");
955
956        assert_eq!(g.get_head().unwrap().max_cut, MaxCut::new(3));
957
958        let seq = lookup(g, "seq").unwrap();
959        let seq = std::str::from_utf8(&seq).unwrap();
960        assert_eq!(seq, "a:b:d:c");
961
962        Ok(())
963    }
964
965    #[test]
966    fn test_complex() -> Result<(), StorageError> {
967        let mut gb = graph! {
968            ClientState::new(SeqPolicyStore, MemStorageProvider::default());
969            "a";
970            "a" < "1" "2" "3";
971            "3" < "4" "6" "7";
972            "3" < "5" "8";
973            "6" "8" < "9" "aa"; commit;
974            "7" < "a1" "a2";
975            "aa" "a2" < "a3";
976            "a3" < "a6" "a4";
977            "a3" < "a7" "a5";
978            "a4" "a5" < "a8";
979            "9" < "42" "43";
980            "42" < "45" "46";
981            "45" < "47" "48";
982            commit;
983        };
984
985        let g = gb.client.provider.get_storage(mkid("a")).unwrap();
986
987        #[cfg(feature = "graphviz")]
988        graphviz::dot(g, "complex");
989
990        assert_eq!(g.get_head().unwrap().max_cut, MaxCut::new(15));
991
992        let seq = lookup(g, "seq").unwrap();
993        let seq = std::str::from_utf8(&seq).unwrap();
994        assert_eq!(
995            seq,
996            "a:1:2:3:5:8:4:6:42:45:47:48:46:43:aa:7:a1:a2:a7:a6:a5:a4"
997        );
998
999        Ok(())
1000    }
1001
1002    #[test]
1003    fn test_duplicates() {
1004        let mut gb = graph! {
1005            ClientState::new(SeqPolicyStore, MemStorageProvider::default());
1006            "a";
1007            "a" < "b" "c";
1008            "a" < "b";
1009            "b" < "c";
1010            "c" < "d";
1011            commit;
1012            "a" < "b";
1013            "b" < "c";
1014            "d" < "e";
1015            commit;
1016        };
1017
1018        let g = gb.client.provider.get_storage(mkid("a")).unwrap();
1019
1020        #[cfg(feature = "graphviz")]
1021        graphviz::dot(g, "duplicates");
1022
1023        assert_eq!(g.get_head().unwrap().max_cut, MaxCut::new(4));
1024
1025        let seq = lookup(g, "seq").unwrap();
1026        let seq = std::str::from_utf8(&seq).unwrap();
1027        assert_eq!(seq, "a:b:c:d:e");
1028    }
1029
1030    #[test]
1031    fn test_mid_braid_1() {
1032        let mut gb = graph! {
1033            ClientState::new(SeqPolicyStore, MemStorageProvider::default());
1034            "a";
1035            "a" < "b" "c" "d" "e" "f" "g";
1036            "d" < "h" "i" "j";
1037            commit;
1038        };
1039
1040        let g = gb.client.provider.get_storage(mkid("a")).unwrap();
1041
1042        #[cfg(feature = "graphviz")]
1043        graphviz::dot(g, "mid_braid_1");
1044
1045        assert_eq!(g.get_head().unwrap().max_cut, MaxCut::new(7));
1046
1047        let seq = lookup(g, "seq").unwrap();
1048        let seq = std::str::from_utf8(&seq).unwrap();
1049        assert_eq!(seq, "a:b:c:d:h:i:j:e:f:g");
1050    }
1051
1052    #[test]
1053    fn test_mid_braid_2() {
1054        let mut gb = graph! {
1055            ClientState::new(SeqPolicyStore, MemStorageProvider::default());
1056            "a";
1057            "a" < "b" "c" "d" "h" "i" "j";
1058            "d" < "e" "f" "g";
1059            commit;
1060        };
1061
1062        let g = gb.client.provider.get_storage(mkid("a")).unwrap();
1063
1064        #[cfg(feature = "graphviz")]
1065        graphviz::dot(g, "mid_braid_2");
1066
1067        assert_eq!(g.get_head().unwrap().max_cut, MaxCut::new(7));
1068
1069        let seq = lookup(g, "seq").unwrap();
1070        let seq = std::str::from_utf8(&seq).unwrap();
1071        assert_eq!(seq, "a:b:c:d:h:i:j:e:f:g");
1072    }
1073
1074    #[test]
1075    fn test_sequential_finalize() {
1076        let mut gb = graph! {
1077            ClientState::new(SeqPolicyStore, MemStorageProvider::default());
1078            "a";
1079            "a" < "b" "c" "d" "e" "f" "g";
1080            "d" < "h" "i" "j";
1081            "e" < finalize "fff1";
1082            "fff1" < "x" "y";
1083            "y" < finalize "fff2";
1084            commit;
1085        };
1086
1087        let g = gb.client.provider.get_storage(mkid("a")).unwrap();
1088
1089        #[cfg(feature = "graphviz")]
1090        graphviz::dot(g, "finalize_success");
1091
1092        assert_eq!(g.get_head().unwrap().max_cut, MaxCut::new(9));
1093
1094        let seq = lookup(g, "seq").unwrap();
1095        let seq = std::str::from_utf8(&seq).unwrap();
1096        assert_eq!(seq, "a:b:c:d:e:fff1:x:y:fff2:h:i:j:f:g");
1097    }
1098
1099    #[test]
1100    fn test_parallel_finalize() {
1101        let mut gb = graph! {
1102            ClientState::new(SeqPolicyStore, MemStorageProvider::default());
1103            "a";
1104            "a" < "b" "c" "d" "e" "f" "g";
1105            "d" < "h" "i" "j";
1106            "e" < finalize "fff1";
1107            "i" < finalize "fff2";
1108        };
1109        let err = gb.commit().expect_err("merge should fail");
1110        assert!(matches!(err, ClientError::ParallelFinalize), "{err:?}");
1111    }
1112
1113    #[test]
1114    fn test_merge_bug() -> Result<(), StorageError> {
1115        let mut gb = graph! {
1116            ClientState::new(SeqPolicyStore, MemStorageProvider::default());
1117            "i";
1118            "i" < "j";
1119            "j" < "qo1";
1120            "j" < "qa1";
1121            "qo1" "qa1" < "m1";
1122            "m1" < "qo2";
1123            "m1" < "qa2";
1124            "qo2" "qa2" < "m2";
1125            "m2" < "h";
1126            commit;
1127        };
1128        let g = gb.client.provider.get_storage(mkid("i")).unwrap();
1129
1130        #[cfg(feature = "graphviz")]
1131        graphviz::dot(g, "merge-bug");
1132
1133        assert_eq!(g.get_head().unwrap().max_cut, MaxCut::new(6));
1134
1135        let seq = lookup(g, "seq").unwrap();
1136        let seq = std::str::from_utf8(&seq).unwrap();
1137        assert_eq!(seq, "i:j:h");
1138
1139        Ok(())
1140    }
1141
1142    #[test]
1143    fn test_linear_bug() -> Result<(), StorageError> {
1144        let mut gb = graph! {
1145            ClientState::new(SeqPolicyStore, MemStorageProvider::default());
1146            "i";
1147            "i" < "j";
1148            commit;
1149            "j" < "qa" "qb";
1150            commit;
1151            "qa" < "c";
1152            "qb" "c" < "m";
1153            commit;
1154        };
1155        let g = gb.client.provider.get_storage(mkid("i")).unwrap();
1156
1157        #[cfg(feature = "graphviz")]
1158        graphviz::dot(g, "linear-bug");
1159
1160        assert_eq!(g.get_head().unwrap().max_cut, MaxCut::new(4));
1161
1162        let seq = lookup(g, "seq").unwrap();
1163        let seq = std::str::from_utf8(&seq).unwrap();
1164        assert_eq!(seq, "i:j:c");
1165
1166        Ok(())
1167    }
1168
1169    #[test]
1170    fn test_fact_convergence_bug() -> Result<(), StorageError> {
1171        let mut gb = graph! {
1172            ClientState::new(SeqPolicyStore, MemStorageProvider::default());
1173            "i";
1174            "i" < "a" "b";
1175            "i" < "c";
1176            "a" "c" < "m1";
1177            "m1" "b" < "m2";
1178            commit;
1179        };
1180        let g = gb.client.provider.get_storage(mkid("i")).unwrap();
1181
1182        #[cfg(feature = "graphviz")]
1183        graphviz::dot(g, "fact-convergence-bug");
1184
1185        let seq = lookup(g, "seq").unwrap();
1186        let seq = std::str::from_utf8(&seq).unwrap();
1187        assert!(
1188            "iabc".chars().all(|c| seq.contains(c)),
1189            "fact missing from {seq:?}"
1190        );
1191
1192        Ok(())
1193    }
1194
1195    #[cfg(feature = "graphviz")]
1196    mod graphviz {
1197        #![allow(clippy::unwrap_used)]
1198
1199        use std::{
1200            collections::{HashSet, VecDeque},
1201            fs::File,
1202            io::BufWriter,
1203        };
1204
1205        use dot_writer::{Attributes as _, DotWriter, Style};
1206
1207        use crate::{
1208            Command as _, FactIndexExtra, Location, Prior, Query, Segment as _, Storage,
1209            testing::short_b58,
1210        };
1211
1212        fn loc(location: impl Into<Location>) -> String {
1213            let location = location.into();
1214            format!("\"{}:{}\"", location.segment, location.max_cut)
1215        }
1216
1217        fn get_seq(p: &impl Query) -> String {
1218            p.query("seq", &[]).unwrap().map_or(String::new(), |seq| {
1219                String::from_utf8(seq.into_vec()).unwrap()
1220            })
1221        }
1222
1223        fn get_segments(storage: &impl Storage) -> Vec<Location> {
1224            let mut locations = Vec::new();
1225            let mut seen_segments = HashSet::new();
1226            let mut segment_queue = VecDeque::new();
1227            segment_queue.push_back(storage.get_head().unwrap());
1228            while let Some(location) = segment_queue.pop_front() {
1229                if !seen_segments.insert(location.segment) {
1230                    continue;
1231                }
1232                let segment = storage.get_segment(location).unwrap();
1233                segment_queue.extend(segment.prior());
1234                locations.push(location);
1235            }
1236            locations.sort_by_key(|loc| loc.segment);
1237            locations
1238        }
1239
1240        fn dotwrite(storage: &impl Storage<FactIndex: FactIndexExtra>, out: &mut DotWriter<'_>) {
1241            let mut graph = out.digraph();
1242            graph
1243                .graph_attributes()
1244                .set("compound", "true", false)
1245                .set("rankdir", "RL", false)
1246                .set_style(Style::Filled)
1247                .set("color", "grey", false);
1248            graph
1249                .node_attributes()
1250                .set("shape", "square", false)
1251                .set_style(Style::Filled)
1252                .set("color", "lightgrey", false);
1253
1254            let mut seen_facts = HashSet::new();
1255            let mut external_facts = Vec::new();
1256
1257            let segments = get_segments(storage);
1258
1259            for &location in &segments {
1260                let segment = storage.get_segment(location).unwrap();
1261
1262                let mut cluster = graph.cluster();
1263                match segment.prior() {
1264                    Prior::None => {
1265                        cluster.graph_attributes().set("color", "green", false);
1266                    }
1267                    Prior::Single(..) => {}
1268                    Prior::Merge(..) => {
1269                        cluster.graph_attributes().set("color", "crimson", false);
1270                    }
1271                }
1272
1273                // Draw commands and edges between commands within the segment.
1274                for (i, cmd) in segment
1275                    .get_from(segment.first_location())
1276                    .into_iter()
1277                    .enumerate()
1278                {
1279                    {
1280                        let mut node =
1281                            cluster.node_named(loc((segment.index(), cmd.max_cut().unwrap())));
1282                        node.set_label(&short_b58(cmd.id()));
1283                        match cmd.parent() {
1284                            Prior::None => {
1285                                node.set("shape", "house", false);
1286                            }
1287                            Prior::Single(..) => {}
1288                            Prior::Merge(..) => {
1289                                node.set("shape", "hexagon", false);
1290                            }
1291                        }
1292                    }
1293                    if i > 0 {
1294                        let previous = cmd.max_cut().unwrap().decremented().expect("i must be > 0");
1295                        cluster.edge(
1296                            loc((segment.index(), cmd.max_cut().unwrap())),
1297                            loc((segment.index(), previous)),
1298                        );
1299                    }
1300                }
1301
1302                // Draw edges to previous segments.
1303                let first = loc(segment.first_location());
1304                for p in segment.prior() {
1305                    cluster.edge(&first, loc(p));
1306                }
1307
1308                // Draw fact index for this segment.
1309                let facts = segment.facts().unwrap();
1310                let curr = facts.name();
1311                cluster
1312                    .node_named(curr.clone())
1313                    .set_label(&get_seq(&facts))
1314                    .set("shape", "cylinder", false)
1315                    .set("color", "black", false)
1316                    .set("style", "solid", false);
1317                cluster
1318                    .edge(loc(segment.head_location().unwrap()), &curr)
1319                    .attributes()
1320                    .set("color", "red", false);
1321
1322                seen_facts.insert(curr);
1323
1324                // Make sure prior facts of fact index will get processed later.
1325                let mut prior = facts.prior().unwrap();
1326                while let Some(node) = prior {
1327                    let name = node.name();
1328                    if !seen_facts.insert(name) {
1329                        break;
1330                    }
1331                    prior = node.prior().unwrap();
1332                    external_facts.push(node);
1333                }
1334            }
1335
1336            graph
1337                .node_attributes()
1338                .set("shape", "cylinder", false)
1339                .set("color", "black", false)
1340                .set("style", "solid", false);
1341
1342            for fact in external_facts {
1343                // Draw nodes for fact indices not directly associated with a segment.
1344                graph.node_named(fact.name()).set_label(&get_seq(&fact));
1345
1346                // Draw edge to prior facts.
1347                if let Some(prior) = fact.prior().unwrap() {
1348                    graph
1349                        .edge(fact.name(), prior.name())
1350                        .attributes()
1351                        .set("color", "blue", false);
1352                }
1353            }
1354
1355            // Draw edges to prior facts for fact indices in segments.
1356            for &location in &segments {
1357                let segment = storage.get_segment(location).unwrap();
1358                let facts = segment.facts().unwrap();
1359                if let Some(prior) = facts.prior().unwrap() {
1360                    graph
1361                        .edge(facts.name(), prior.name())
1362                        .attributes()
1363                        .set("color", "blue", false);
1364                }
1365            }
1366
1367            // Draw HEAD indicator.
1368            graph.node_named("HEAD").set("shape", "none", false);
1369            graph.edge("HEAD", loc(storage.get_head().unwrap()));
1370        }
1371
1372        pub fn dot(storage: &impl Storage<FactIndex: FactIndexExtra>, name: &str) {
1373            std::fs::create_dir_all(".ignore").unwrap();
1374            dotwrite(
1375                storage,
1376                &mut DotWriter::from(&mut BufWriter::new(
1377                    File::create(format!(".ignore/{name}.dot")).unwrap(),
1378                )),
1379            );
1380        }
1381    }
1382}