Switch to our own DAG representation for COBs

We're using petgraph to encode an operation based CRDT (each graph node
is an operation) as a DAG, and one of the important things to ensure
is that the final state is not influenced by the order of concurrent
operations. In this case a concurrent operation would mean two potential
graph traversal orders, as there's no direct edge between the two
concurrent ops. This is essential a "partial order" of operations.

To be able to test this, we'd want some kind of control over how
neighbors are iterated over I guess, when there's more than one possible
sort order. Say we have a graph like this:

             ┌────────b◄─────┐
             ▼               │
             a               d
             ▲               │
             └────────c◄─────┘

There are two possible topological traverse orders: [a, b, c, d] and [a,
c, b, d]. Having a way to go through these different orders would
be super handy.

One option would be to allow random order traversal. This would allow us
to test all orders by running the test enough times to likely test all
permutations.

Since petgraph doesn't support this, we implement our own simple DAG in
`radicle-dag`, which implements random-order topological orders.

For now, we don't make explicit use of these improvements, we simply
replace the underlying graph with our own.
This commit is contained in:
Alexis Sellier 2022-12-28 11:48:08 +01:00 committed by Alexis Sellier
parent 86a38e208b
commit 2f07b76fcb
No known key found for this signature in database
9 changed files with 392 additions and 121 deletions

25
Cargo.lock generated
View File

@ -906,12 +906,6 @@ dependencies = [
"static_assertions", "static_assertions",
] ]
[[package]]
name = "fixedbitset"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.25" version = "1.0.25"
@ -1861,16 +1855,6 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "petgraph"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7"
dependencies = [
"fixedbitset",
"indexmap",
]
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.0.12" version = "1.0.12"
@ -2130,10 +2114,10 @@ dependencies = [
"git2", "git2",
"log", "log",
"nonempty 0.8.1", "nonempty 0.8.1",
"petgraph",
"qcheck", "qcheck",
"qcheck-macros", "qcheck-macros",
"radicle-crypto", "radicle-crypto",
"radicle-dag",
"radicle-git-ext", "radicle-git-ext",
"serde", "serde",
"serde_json", "serde_json",
@ -2177,6 +2161,13 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "radicle-dag"
version = "0.1.0"
dependencies = [
"fastrand",
]
[[package]] [[package]]
name = "radicle-git-ext" name = "radicle-git-ext"
version = "0.2.0" version = "0.2.0"

View File

@ -5,6 +5,7 @@ members = [
"radicle-cli", "radicle-cli",
"radicle-crdt", "radicle-crdt",
"radicle-crypto", "radicle-crypto",
"radicle-dag",
"radicle-httpd", "radicle-httpd",
"radicle-node", "radicle-node",
"radicle-remote-helper", "radicle-remote-helper",

View File

@ -13,12 +13,12 @@ keywords = ["radicle", "collaborative objects", "cob", "cobs"]
[lib] [lib]
[dependencies] [dependencies]
fastrand = { version = "1.8.0" }
git-commit = { version = "0.2" } git-commit = { version = "0.2" }
git-ref-format = { version = "0.1" } git-ref-format = { version = "0.1" }
git-trailers = { version = "0.1" } git-trailers = { version = "0.1" }
log = { version = "0.4.17" } log = { version = "0.4.17" }
nonempty = { version = "0.8.1", features = ["serialize"] } nonempty = { version = "0.8.1", features = ["serialize"] }
petgraph = { version = "0.5" }
radicle-git-ext = { version = "0" } radicle-git-ext = { version = "0" }
serde_json = { version = "1.0" } serde_json = { version = "1.0" }
thiserror = { version = "1.0" } thiserror = { version = "1.0" }
@ -33,6 +33,10 @@ path = "../radicle-crypto"
version = "0.1" version = "0.1"
features = ["ssh"] features = ["ssh"]
[dependencies.radicle-dag]
path = "../radicle-dag"
version = "0.1"
[dependencies.serde] [dependencies.serde]
version = "1.0" version = "1.0"
features = ["derive"] features = ["derive"]

View File

@ -9,10 +9,7 @@ use std::{
}; };
use git_ext::Oid; use git_ext::Oid;
use petgraph::{ use radicle_dag::{Dag, Node};
visit::{EdgeRef, Topo, Walker},
EdgeDirection,
};
use crate::{ use crate::{
change, object, signatures::Signature, Change, CollaborativeObject, ObjectId, TypeName, change, object, signatures::Signature, Change, CollaborativeObject, ObjectId, TypeName,
@ -24,7 +21,7 @@ use evaluation::evaluate;
/// The graph of changes for a particular collaborative object /// The graph of changes for a particular collaborative object
pub(super) struct ChangeGraph { pub(super) struct ChangeGraph {
object_id: ObjectId, object_id: ObjectId,
graph: petgraph::Graph<Change, ()>, graph: Dag<Oid, Change>,
} }
impl ChangeGraph { impl ChangeGraph {
@ -94,27 +91,23 @@ impl ChangeGraph {
/// or which do not have permission to make a change, or which make a /// or which do not have permission to make a change, or which make a
/// change which invalidates the schema of the object /// change which invalidates the schema of the object
pub(crate) fn evaluate(&self) -> CollaborativeObject { pub(crate) fn evaluate(&self) -> CollaborativeObject {
let mut roots: Vec<petgraph::graph::NodeIndex<u32>> = self let mut roots: Vec<(&Oid, &Node<_, _>)> = self.graph.roots().collect();
.graph roots.sort_by_key(|(k, _)| *k);
.externals(petgraph::Direction::Incoming)
.collect();
roots.sort();
// This is okay because we check that the graph has a root node in // This is okay because we check that the graph has a root node in
// GraphBuilder::build // GraphBuilder::build
let root = roots.first().unwrap(); let (root, root_node) = roots.first().unwrap();
let manifest = { let manifest = root_node.manifest.clone();
let first_node = &self.graph[*root]; let rng = fastrand::Rng::new();
first_node.manifest.clone() let sorted = self.graph.sorted(rng);
}; let items = sorted.iter().map(|oid| {
let topo = Topo::new(&self.graph); let node = &self.graph[oid];
let items = topo.iter(&self.graph).map(|idx| { let child_commits = node
let node = &self.graph[idx]; .dependents
let outgoing_edges = self.graph.edges_directed(idx, EdgeDirection::Outgoing); .iter()
let child_commits = outgoing_edges .map(|e| *self.graph[e].id())
.map(|e| *self.graph[e.target()].id())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
(node, idx, child_commits) (&node.value, *oid, child_commits)
}); });
let history = { let history = {
let root_change = &self.graph[*root]; let root_change = &self.graph[*root];
@ -129,35 +122,24 @@ impl ChangeGraph {
/// Get the tips of the collaborative object /// Get the tips of the collaborative object
pub(crate) fn tips(&self) -> BTreeSet<Oid> { pub(crate) fn tips(&self) -> BTreeSet<Oid> {
self.graph self.graph.tips().map(|(_, change)| *change.id()).collect()
.externals(petgraph::Direction::Outgoing)
.map(|n| {
let change = &self.graph[n];
*change.id()
})
.collect()
} }
pub(crate) fn number_of_nodes(&self) -> u64 { pub(crate) fn number_of_nodes(&self) -> u64 {
self.graph.node_count().try_into().unwrap() self.graph.len().try_into().unwrap()
}
pub(crate) fn graphviz(&self) -> String {
let for_display = self.graph.map(|_ix, n| n.to_string(), |_ix, _e| "");
petgraph::dot::Dot::new(&for_display).to_string()
} }
} }
struct GraphBuilder { struct GraphBuilder {
node_indices: HashMap<Oid, petgraph::graph::NodeIndex<u32>>, node_indices: HashMap<Oid, Oid>,
graph: petgraph::Graph<Change, ()>, graph: Dag<Oid, Change>,
} }
impl Default for GraphBuilder { impl Default for GraphBuilder {
fn default() -> Self { fn default() -> Self {
GraphBuilder { GraphBuilder {
node_indices: HashMap::new(), node_indices: HashMap::new(),
graph: petgraph::graph::Graph::new(), graph: Dag::new(),
} }
} }
} }
@ -173,11 +155,11 @@ impl GraphBuilder {
let resource_commit = *change.resource(); let resource_commit = *change.resource();
let commit_id = commit.id; let commit_id = commit.id;
if let Entry::Vacant(e) = self.node_indices.entry(commit_id) { if let Entry::Vacant(e) = self.node_indices.entry(commit_id) {
let ix = self.graph.add_node(change); self.graph.node(commit_id, change);
e.insert(ix); e.insert(commit_id);
} }
commit.parents.into_iter().filter_map(move |parent| { commit.parents.into_iter().filter_map(move |parent| {
if parent.id != resource_commit && !self.has_edge(parent.id, commit_id) { if parent.id != resource_commit && !self.has_dependency(commit_id, parent.id) {
Some((parent, commit_id)) Some((parent, commit_id))
} else { } else {
None None
@ -185,11 +167,11 @@ impl GraphBuilder {
}) })
} }
fn has_edge(&mut self, parent_id: Oid, child_id: Oid) -> bool { fn has_dependency(&mut self, child_id: Oid, parent_id: Oid) -> bool {
let parent_ix = self.node_indices.get(&parent_id); let parent_ix = self.node_indices.get(&parent_id);
let child_ix = self.node_indices.get(&child_id); let child_ix = self.node_indices.get(&child_id);
match (parent_ix, child_ix) { match (parent_ix, child_ix) {
(Some(parent_ix), Some(child_ix)) => self.graph.contains_edge(*parent_ix, *child_ix), (Some(parent_ix), Some(child_ix)) => self.graph.has_dependency(child_ix, parent_ix),
_ => false, _ => false,
} }
} }
@ -204,16 +186,11 @@ impl GraphBuilder {
.node_indices .node_indices
.get(&parent) .get(&parent)
.expect("BUG: parent id expected to in graph"); .expect("BUG: parent id expected to in graph");
self.graph.update_edge(*parent_id, *child_id, ()); self.graph.dependency(*child_id, *parent_id);
} }
fn build(self, object_id: ObjectId) -> Option<ChangeGraph> { fn build(self, object_id: ObjectId) -> Option<ChangeGraph> {
if self if self.graph.roots().next().is_some() {
.graph
.externals(petgraph::Direction::Incoming)
.next()
.is_some()
{
Some(ChangeGraph { Some(ChangeGraph {
object_id, object_id,
graph: self.graph, graph: self.graph,

View File

@ -6,7 +6,7 @@
use std::{collections::HashMap, ops::ControlFlow}; use std::{collections::HashMap, ops::ControlFlow};
use git_ext::Oid; use git_ext::Oid;
use petgraph::{visit::EdgeRef, EdgeDirection}; use radicle_dag::Dag;
use crate::history::entry::{EntryId, EntryWithClock}; use crate::history::entry::{EntryId, EntryWithClock};
use crate::history::Clock; use crate::history::Clock;
@ -17,8 +17,8 @@ use crate::{change::Change, history, pruning_fold};
/// If the change corresponding to the root OID is not in `items` /// If the change corresponding to the root OID is not in `items`
pub fn evaluate<'b>( pub fn evaluate<'b>(
root: Oid, root: Oid,
graph: &petgraph::Graph<Change, ()>, graph: &Dag<Oid, Change>,
items: impl Iterator<Item = (&'b Change, petgraph::graph::NodeIndex<u32>, Vec<Oid>)>, items: impl Iterator<Item = (&'b Change, Oid, Vec<Oid>)>,
) -> history::History { ) -> history::History {
let entries = pruning_fold::pruning_fold( let entries = pruning_fold::pruning_fold(
HashMap::<EntryId, EntryWithClock>::new(), HashMap::<EntryId, EntryWithClock>::new(),
@ -37,11 +37,11 @@ pub fn evaluate<'b>(
} }
Ok(entry) => { Ok(entry) => {
// Get parent commits and calculate this node's clock based on theirs. // Get parent commits and calculate this node's clock based on theirs.
let incoming = graph.edges_directed(c.idx, EdgeDirection::Incoming); let clock = graph[&c.idx]
let clock = incoming .dependencies
.into_iter() .iter()
.map(|e| { .map(|e| {
let entry = &entries[&graph[e.source()].id.into()]; let entry = &entries[&graph[e].id.into()];
let clock = entry.clock(); let clock = entry.clock();
clock + entry.contents().len() as Clock - 1 clock + entry.contents().len() as Clock - 1
@ -81,7 +81,7 @@ fn evaluate_change(
} }
struct ChangeWithChildren<'a> { struct ChangeWithChildren<'a> {
idx: petgraph::graph::NodeIndex<u32>, idx: Oid,
change: &'a Change, change: &'a Change,
child_commits: Vec<Oid>, child_commits: Vec<Oid>,
} }

View File

@ -9,8 +9,8 @@ use std::{
}; };
use git_ext::Oid; use git_ext::Oid;
use petgraph::visit::Walker as _;
use radicle_crypto::PublicKey; use radicle_crypto::PublicKey;
use radicle_dag::Dag;
use crate::pruning_fold; use crate::pruning_fold;
@ -20,8 +20,8 @@ pub use entry::{Clock, Contents, Entry, EntryId, EntryWithClock, Timestamp};
/// The DAG of changes making up the history of a collaborative object. /// The DAG of changes making up the history of a collaborative object.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct History { pub struct History {
graph: petgraph::Graph<EntryWithClock, (), petgraph::Directed, u32>, graph: Dag<EntryId, EntryWithClock>,
indices: HashMap<EntryId, petgraph::graph::NodeIndex<u32>>, indices: HashMap<EntryId, Oid>,
} }
impl PartialEq for History { impl PartialEq for History {
@ -61,7 +61,7 @@ impl History {
let mut entries = HashMap::new(); let mut entries = HashMap::new();
entries.insert(id, EntryWithClock::from(root_entry)); entries.insert(id, EntryWithClock::from(root_entry));
create_petgraph(&id, &entries) create_dag(&id, &entries)
} }
pub fn new<Id>(root: Id, entries: HashMap<EntryId, EntryWithClock>) -> Result<Self, CreateError> pub fn new<Id>(root: Id, entries: HashMap<EntryId, EntryWithClock>) -> Result<Self, CreateError>
@ -72,7 +72,7 @@ impl History {
if !entries.contains_key(&root) { if !entries.contains_key(&root) {
Err(CreateError::MissingRoot) Err(CreateError::MissingRoot)
} else { } else {
Ok(create_petgraph(&root, &entries)) Ok(create_dag(&root, &entries))
} }
} }
@ -80,11 +80,8 @@ impl History {
/// This is the maximum value of all tips. /// This is the maximum value of all tips.
pub fn clock(&self) -> Clock { pub fn clock(&self) -> Clock {
self.graph self.graph
.externals(petgraph::Direction::Outgoing) .tips()
.map(|n| { .map(|(_, node)| node.clock + node.entry.contents.len() as Clock - 1)
let node = &self.graph[n];
node.clock + node.entry.contents.len() as Clock - 1
})
.max() .max()
.unwrap_or_default() .unwrap_or_default()
} }
@ -93,8 +90,8 @@ impl History {
/// This is the latest timestamp of any tip. /// This is the latest timestamp of any tip.
pub fn timestamp(&self) -> Timestamp { pub fn timestamp(&self) -> Timestamp {
self.graph self.graph
.externals(petgraph::Direction::Outgoing) .tips()
.map(|n| self.graph[n].timestamp()) .map(|(_, n)| n.timestamp())
.max() .max()
.unwrap_or_default() .unwrap_or_default()
} }
@ -109,9 +106,9 @@ impl History {
where where
F: for<'r> FnMut(A, &'r EntryWithClock) -> ControlFlow<A, A>, F: for<'r> FnMut(A, &'r EntryWithClock) -> ControlFlow<A, A>,
{ {
let topo = petgraph::visit::Topo::new(&self.graph); let sorted = self.graph.sorted(fastrand::Rng::new());
#[allow(clippy::let_and_return)] #[allow(clippy::let_and_return)]
let items = topo.iter(&self.graph).map(|idx| { let items = sorted.iter().map(|idx| {
let entry = &self.graph[idx]; let entry = &self.graph[idx];
entry entry
}); });
@ -120,11 +117,8 @@ impl History {
pub(crate) fn tips(&self) -> BTreeSet<Oid> { pub(crate) fn tips(&self) -> BTreeSet<Oid> {
self.graph self.graph
.externals(petgraph::Direction::Outgoing) .tips()
.map(|n| { .map(|(_, entry)| (*entry.id()).into())
let entry = &self.graph[n];
(*entry.id()).into()
})
.collect() .collect()
} }
@ -148,34 +142,36 @@ impl History {
new_contents, new_contents,
new_timestamp, new_timestamp,
); );
let new_ix = self.graph.add_node(EntryWithClock { self.graph.node(
entry: new_entry, new_id,
clock: self.clock() + 1, EntryWithClock {
}); entry: new_entry,
clock: self.clock() + 1,
},
);
for tip in tips { for tip in tips {
let tip_ix = self.indices.get(&tip.into()).unwrap(); let tip_ix = self.indices.get(&tip.into()).unwrap();
self.graph.update_edge(*tip_ix, new_ix, ()); self.graph.dependency(new_id, (*tip_ix).into());
} }
} }
} }
fn create_petgraph<'a>( fn create_dag<'a>(root: &'a EntryId, entries: &'a HashMap<EntryId, EntryWithClock>) -> History {
root: &'a EntryId, let mut graph: Dag<EntryId, EntryWithClock> = Dag::new();
entries: &'a HashMap<EntryId, EntryWithClock>, let mut indices = HashMap::<EntryId, Oid>::new();
) -> History { let root_entry = entries.get(root).unwrap().clone();
let mut graph = petgraph::Graph::new(); graph.node(*root, root_entry.clone());
let mut indices = HashMap::<EntryId, petgraph::graph::NodeIndex<u32>>::new(); indices.insert(root_entry.id, (*root).into());
let root = entries.get(root).unwrap().clone(); let mut to_process = vec![root_entry];
let root_ix = graph.add_node(root.clone());
indices.insert(root.id, root_ix);
let mut to_process = vec![root];
while let Some(entry) = to_process.pop() { while let Some(entry) = to_process.pop() {
let entry_ix = indices[&entry.id]; let entry_ix = indices[&entry.id];
for child_id in entry.children() { for child_id in entry.children() {
let child = entries[child_id].clone(); let child = entries[child_id].clone();
let child_ix = graph.add_node(child.clone()); graph.node(*child_id, child.clone());
indices.insert(child.id, child_ix); indices.insert(child.id, (*child_id).into());
graph.update_edge(entry_ix, child_ix, ()); graph.dependency(*child_id, entry_ix.into());
to_process.push(child.clone()); to_process.push(child.clone());
} }
} }

View File

@ -4,11 +4,8 @@
// Linking Exception. For full terms see the included LICENSE file. // Linking Exception. For full terms see the included LICENSE file.
//! [`ChangeGraphInfo`] provides a useful debugging structure for //! [`ChangeGraphInfo`] provides a useful debugging structure for
//! represnting a single [`crate::CollaborativeObject`]'s underlying //! representing a single [`crate::CollaborativeObject`]'s underlying
//! change graph. This includes a [`ChangeGraphInfo::dotviz`] for //! change graph.
//! describing the graph via [graphviz].
//!
//! [graphviz]: https://graphviz.org/
use std::collections::BTreeSet; use std::collections::BTreeSet;
@ -22,8 +19,6 @@ use super::error;
pub struct ChangeGraphInfo { pub struct ChangeGraphInfo {
/// The ID of the object /// The ID of the object
pub object_id: ObjectId, pub object_id: ObjectId,
/// A graphviz description of the changegraph of the object
pub dotviz: String,
/// The number of nodes in the change graph of the object /// The number of nodes in the change graph of the object
pub number_of_nodes: u64, pub number_of_nodes: u64,
/// The "tips" of the change graph, i.e the object IDs pointed to by /// The "tips" of the change graph, i.e the object IDs pointed to by
@ -54,7 +49,6 @@ where
Ok( Ok(
ChangeGraph::load(storage, tip_refs.iter(), typename, oid).map(|graph| ChangeGraphInfo { ChangeGraph::load(storage, tip_refs.iter(), typename, oid).map(|graph| ChangeGraphInfo {
object_id: *oid, object_id: *oid,
dotviz: graph.graphviz(),
number_of_nodes: graph.number_of_nodes(), number_of_nodes: graph.number_of_nodes(),
tips: graph.tips(), tips: graph.tips(),
}), }),

7
radicle-dag/Cargo.toml Normal file
View File

@ -0,0 +1,7 @@
[package]
name = "radicle-dag"
version = "0.1.0"
edition = "2021"
[dependencies]
fastrand = { version = "1.8.0" }

301
radicle-dag/src/lib.rs Normal file
View File

@ -0,0 +1,301 @@
use std::{
borrow::Borrow,
collections::{HashMap, HashSet},
fmt,
hash::Hash,
ops::{Deref, Index},
};
/// A node in the graph.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Node<K: Eq + Hash, V> {
/// The node value, stored by the user.
pub value: V,
/// Nodes depended on.
pub dependencies: HashSet<K>,
/// Nodes depending on this node.
pub dependents: HashSet<K>,
}
impl<K: Eq + Hash, V> Borrow<V> for &Node<K, V> {
fn borrow(&self) -> &V {
&self.value
}
}
impl<K: Eq + Hash, V> Deref for Node<K, V> {
type Target = V;
fn deref(&self) -> &Self::Target {
&self.value
}
}
/// A directed acyclic graph.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Dag<K: Eq + Hash, V> {
graph: HashMap<K, Node<K, V>>,
tips: HashSet<K>,
roots: HashSet<K>,
}
impl<K: Eq + Copy + Hash, V> Dag<K, V> {
/// Create a new empty DAG.
pub fn new() -> Self {
Self {
graph: HashMap::new(),
tips: HashSet::new(),
roots: HashSet::new(),
}
}
/// Check whether there are any nodes in the graph.
pub fn is_empty(&self) -> bool {
self.graph.is_empty()
}
/// Return the number of nodes in the graph.
pub fn len(&self) -> usize {
self.graph.len()
}
/// Add a node to the graph.
pub fn node(&mut self, key: K, value: V) -> Option<Node<K, V>> {
self.tips.insert(key);
self.roots.insert(key);
self.graph.insert(
key,
Node {
value,
dependencies: HashSet::new(),
dependents: HashSet::new(),
},
)
}
/// Add a dependency from one node to the other.
pub fn dependency(&mut self, from: K, to: K) {
if let Some(node) = self.graph.get_mut(&from) {
node.dependencies.insert(to);
self.roots.remove(&from);
}
if let Some(node) = self.graph.get_mut(&to) {
node.dependents.insert(from);
self.tips.remove(&to);
}
}
/// Get a node.
pub fn get(&self, key: &K) -> Option<&Node<K, V>> {
self.graph.get(key)
}
/// Check whether there is a dependency between two nodes.
pub fn has_dependency(&self, from: &K, to: &K) -> bool {
self.graph
.get(from)
.map(|n| n.dependencies.contains(to))
.unwrap_or_default()
}
/// Get the graph's root nodes, ie. nodes which don't depend on other nodes.
pub fn roots(&self) -> impl Iterator<Item = (&K, &Node<K, V>)> + '_ {
self.roots
.iter()
.filter_map(|k| self.graph.get(k).map(|n| (k, n)))
}
/// Get the graph's tip nodes, ie. nodes which aren't depended on by other nodes.
pub fn tips(&self) -> impl Iterator<Item = (&K, &Node<K, V>)> + '_ {
self.tips
.iter()
.filter_map(|k| self.graph.get(k).map(|n| (k, n)))
}
/// Return a topological ordering of the graph's nodes, using the given RNG.
/// Graphs with more than one partial order will return an arbitrary topological ordering.
///
/// Calling this function over and over will eventually yield all possible orderings.
pub fn sorted(&self, rng: fastrand::Rng) -> Vec<K> {
let mut order = Vec::new(); // Stores the topological order.
let mut visited = HashSet::new(); // Nodes that have been visited.
let mut keys = self.graph.keys().collect::<Vec<_>>();
rng.shuffle(&mut keys);
for node in keys {
self.visit(node, &mut visited, &mut order);
}
order
}
/// Add nodes recursively to the topological order, starting from the given node.
fn visit(&self, key: &K, visited: &mut HashSet<K>, order: &mut Vec<K>) {
if visited.contains(key) {
return;
}
visited.insert(*key);
// Recursively visit all of the node's dependencies.
if let Some(node) = self.graph.get(key) {
for dependency in &node.dependencies {
self.visit(dependency, visited, order);
}
}
// Add the node to the topological order.
order.push(*key);
}
}
impl<K: Eq + Copy + Hash + fmt::Debug, V> Index<&K> for Dag<K, V> {
type Output = Node<K, V>;
fn index(&self, key: &K) -> &Self::Output {
self.get(key)
.unwrap_or_else(|| panic!("Dag::index: node {:?} not found in graph", key))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_len() {
let mut dag = Dag::new();
dag.node(0, ());
dag.node(1, ());
dag.node(2, ());
assert_eq!(dag.len(), 3);
}
#[test]
fn test_is_empty() {
let mut dag = Dag::new();
assert!(dag.is_empty());
dag.node(0, ());
assert!(!dag.is_empty());
}
#[test]
fn test_dependencies() {
let mut dag = Dag::new();
dag.node(0, ());
dag.node(1, ());
dag.dependency(0, 1);
assert!(dag.has_dependency(&0, &1));
assert!(!dag.has_dependency(&1, &0));
}
#[test]
fn test_get() {
let mut dag = Dag::new();
dag.node(0, "rad");
dag.node(1, "dar");
assert_eq!(dag[&0].value, "rad");
assert_eq!(dag[&1].value, "dar");
assert!(dag.get(&2).is_none());
}
#[test]
fn test_cycle() {
let mut dag = Dag::new();
dag.node(0, ());
dag.node(1, ());
dag.dependency(0, 1);
dag.dependency(1, 0);
let sorted = dag.sorted(fastrand::Rng::new());
let expected: &[&[i32]] = &[&[0, 1], &[1, 0]];
assert!(expected.contains(&sorted.as_slice()));
}
#[test]
fn test_diamond() {
let mut dag = Dag::new();
dag.node(0, ());
dag.node(1, ());
dag.node(2, ());
dag.node(3, ());
dag.dependency(1, 0);
dag.dependency(2, 0);
dag.dependency(3, 1);
dag.dependency(3, 2);
assert_eq!(dag.tips().map(|(k, _)| *k).collect::<Vec<_>>(), vec![3]);
assert_eq!(dag.roots().map(|(k, _)| *k).collect::<Vec<_>>(), vec![0]);
// All of the possible sort orders for the above graph.
let expected: &[&[i32]] = &[&[0, 1, 2, 3], &[0, 2, 1, 3]];
let actual = dag.sorted(fastrand::Rng::new());
assert!(expected.contains(&actual.as_slice()), "{:?}", actual);
}
#[test]
fn test_complex() {
let mut dag = Dag::new();
dag.node(0, ());
dag.node(1, ());
dag.node(2, ());
dag.node(3, ());
dag.node(4, ());
dag.node(5, ());
dag.dependency(3, 2);
dag.dependency(1, 3);
dag.dependency(2, 5);
dag.dependency(0, 5);
dag.dependency(0, 4);
dag.dependency(1, 4);
assert_eq!(
dag.tips().map(|(k, _)| *k).collect::<HashSet<_>>(),
HashSet::from_iter([1, 0])
);
assert_eq!(
dag.roots().map(|(k, _)| *k).collect::<HashSet<_>>(),
HashSet::from_iter([4, 5])
);
// All of the possible sort orders for the above graph.
let expected = &[
[4, 5, 0, 2, 3, 1],
[4, 5, 2, 0, 3, 1],
[4, 5, 2, 3, 0, 1],
[4, 5, 2, 3, 1, 0],
[5, 2, 3, 4, 0, 1],
[5, 2, 3, 4, 1, 0],
[5, 2, 4, 0, 3, 1],
[5, 2, 4, 3, 0, 1],
[5, 2, 4, 3, 1, 0],
[5, 4, 0, 2, 3, 1],
[5, 4, 2, 0, 3, 1],
[5, 4, 2, 3, 0, 1],
[5, 4, 2, 3, 1, 0],
];
let rng = fastrand::Rng::new();
let mut sorts = HashSet::new();
while sorts.len() < expected.len() {
sorts.insert(dag.sorted(rng.clone()));
}
for e in expected {
assert!(sorts.remove(e.to_vec().as_slice()));
}
assert!(sorts.is_empty());
}
}