p2panda_auth/
graph.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Graph functions for identifying related sets of concurrent operations.
4
5use std::collections::HashSet;
6
7use petgraph::algo::has_path_connecting;
8use petgraph::graphmap::DiGraphMap;
9use petgraph::visit::{Dfs, Reversed};
10
11use crate::traits::OperationId;
12
13/// Recursively identify all operations concurrent with the given target operation.
14fn concurrent_bubble<OP>(
15    graph: &DiGraphMap<OP, ()>,
16    target: OP,
17    processed: &mut HashSet<OP>,
18) -> HashSet<OP>
19where
20    OP: OperationId + Ord,
21{
22    let mut bubble = HashSet::new();
23    bubble.insert(target);
24
25    concurrent_operations(graph, target)
26        .into_iter()
27        .for_each(|op| {
28            if processed.insert(op) {
29                bubble.extend(concurrent_bubble(graph, op, processed).iter())
30            }
31        });
32
33    bubble
34}
35
36/// Walk the graph and identify all sets of concurrent operations.
37pub fn concurrent_bubbles<OP>(graph: &DiGraphMap<OP, ()>) -> Vec<HashSet<OP>>
38where
39    OP: OperationId + Ord,
40{
41    let mut processed: HashSet<OP> = HashSet::new();
42    let mut bubbles = Vec::new();
43
44    graph.nodes().for_each(|target| {
45        if processed.insert(target) {
46            let bubble = concurrent_bubble(graph, target, &mut processed);
47            if bubble.len() > 1 {
48                bubbles.push(bubble)
49            }
50        }
51    });
52
53    bubbles
54}
55
56/// Return any operations concurrent with the given target operation.
57///
58/// Operations are considered concurrent if they are neither predecessors nor successors of the
59/// target operation.
60fn concurrent_operations<OP>(graph: &DiGraphMap<OP, ()>, target: OP) -> HashSet<OP>
61where
62    OP: OperationId + Ord,
63{
64    // Get all successors.
65    let mut successors = HashSet::new();
66    let mut dfs = Dfs::new(&graph, target);
67    while let Some(nx) = dfs.next(&graph) {
68        successors.insert(nx);
69    }
70
71    // Get all predecessors.
72    let mut predecessors = HashSet::new();
73    let reversed = Reversed(graph);
74    let mut dfs_rev = Dfs::new(&reversed, target);
75    while let Some(nx) = dfs_rev.next(&reversed) {
76        predecessors.insert(nx);
77    }
78
79    let relatives: HashSet<_> = successors.union(&predecessors).cloned().collect();
80
81    // Collect all operations which are not successors or predecessors.
82    graph.nodes().filter(|n| !relatives.contains(n)).collect()
83}
84
85/// Split a set of operations into concurrent, predecessors and successors.
86pub fn split_bubble<OP>(
87    graph: &DiGraphMap<OP, ()>,
88    bubble: &HashSet<OP>,
89    target: OP,
90) -> (HashSet<OP>, HashSet<OP>, Vec<OP>)
91where
92    OP: OperationId + Ord,
93{
94    // Get all successors.
95    let mut concurrent = bubble.clone();
96    let mut successors = Vec::new();
97    let mut dfs = Dfs::new(&graph, target);
98    while let Some(id) = dfs.next(&graph) {
99        concurrent.remove(&id);
100        successors.push(id);
101    }
102
103    // Get all predecessors.
104    let mut predecessors = HashSet::new();
105    let reversed = Reversed(graph);
106    let mut dfs_rev = Dfs::new(&reversed, target);
107    while let Some(id) = dfs_rev.next(&reversed) {
108        concurrent.remove(&id);
109        predecessors.insert(id);
110    }
111
112    (concurrent, predecessors, successors)
113}
114
115/// Return `true` if a linear path exists in the graph between `from` and `to`.
116///
117/// This indicates whether `to` is a successor of `from`.
118pub fn has_path<OP>(graph: &DiGraphMap<OP, ()>, from: OP, to: OP) -> bool
119where
120    OP: OperationId + Ord,
121{
122    from != to && has_path_connecting(graph, from, to, None)
123}
124
125/// Return `true` if no linear path exists between `a` and `b` in either direction.
126///
127/// This indicates whether or not the given operations occurred concurrently.
128pub fn is_concurrent<OP>(graph: &DiGraphMap<OP, ()>, a: OP, b: OP) -> bool
129where
130    OP: OperationId + Ord,
131{
132    a != b && !has_path(graph, a, b) && !has_path(graph, b, a)
133}
134
135#[cfg(test)]
136mod tests {
137    use std::collections::HashSet;
138
139    use petgraph::{graph::DiGraph, prelude::DiGraphMap};
140
141    use crate::graph::concurrent_bubbles;
142    use crate::traits::OperationId;
143
144    impl OperationId for &str {}
145
146    #[test]
147    fn test_linear_chain_no_concurrency() {
148        let mut graph = DiGraphMap::new();
149        graph.add_edge(1, 2, ());
150        graph.add_edge(2, 3, ());
151        graph.add_edge(3, 4, ());
152
153        let bubbles = concurrent_bubbles(&graph);
154        assert!(bubbles.is_empty());
155    }
156
157    #[test]
158    fn test_bubble() {
159        let mut graph = DiGraphMap::new();
160        graph.add_edge(1, 2, ());
161        graph.add_edge(1, 3, ());
162        graph.add_edge(2, 4, ());
163        graph.add_edge(3, 4, ());
164
165        let bubbles = concurrent_bubbles(&graph);
166
167        // 2 and 3 are concurrent.
168        assert_eq!(bubbles.len(), 1);
169        let expected: HashSet<_> = [2, 3].into_iter().collect();
170        assert_eq!(bubbles[0], expected);
171    }
172
173    #[test]
174    fn test_two_bubbles() {
175        let mut graph = DiGraphMap::new();
176        // Bubble 1: 1 → 2, 1 → 3, 2 → 4, 3 → 4
177        graph.add_edge(1, 2, ());
178        graph.add_edge(1, 3, ());
179        graph.add_edge(2, 4, ());
180        graph.add_edge(3, 4, ());
181        // Bubble 2: 4 → 5, 4 → 6, 5 → 7, 6 → 7
182        graph.add_edge(4, 5, ());
183        graph.add_edge(4, 6, ());
184        graph.add_edge(5, 7, ());
185        graph.add_edge(6, 7, ());
186
187        let bubbles = concurrent_bubbles(&graph);
188        assert_eq!(bubbles.len(), 2);
189
190        let b1: HashSet<_> = [2, 3].into_iter().collect();
191        let b2: HashSet<_> = [5, 6].into_iter().collect();
192
193        assert!(bubbles.contains(&b1));
194        assert!(bubbles.contains(&b2));
195    }
196
197    #[test]
198    fn complex_bubble() {
199        //       A
200        //     /   \
201        //    B     C
202        //   / \     \
203        //  D   E     F
204        //   \ /     /
205        //    G     H
206        //     \   /
207        //       I
208        //       |
209        //       J
210
211        let mut graph = DiGraph::new();
212
213        // Add nodes A–M.
214        let a = graph.add_node("A"); // 0
215        let b = graph.add_node("B"); // 1
216        let c = graph.add_node("C"); // 2
217        let d = graph.add_node("D"); // 3
218        let e = graph.add_node("E"); // 4
219        let f = graph.add_node("F"); // 5
220        let g = graph.add_node("G"); // 6
221        let h = graph.add_node("H"); // 7
222        let i = graph.add_node("I"); // 8
223        let j = graph.add_node("J"); // 9
224
225        // Add edges.
226        graph.extend_with_edges([
227            (a, b),
228            (a, c),
229            (b, d),
230            (b, e),
231            (d, g),
232            (e, g),
233            (c, f),
234            (f, h),
235            (h, i),
236            (g, i),
237            (i, j),
238        ]);
239
240        let graph_map = DiGraphMap::from_graph(graph);
241        let concurrent_bubbles = concurrent_bubbles(&graph_map);
242
243        assert_eq!(concurrent_bubbles.len(), 1);
244        let bubble = concurrent_bubbles.first().unwrap();
245        for id in &["B", "C", "D", "E", "F", "G", "H"] {
246            assert!(bubble.contains(id));
247        }
248    }
249}