cob: Simplify graph evaluation
Simplify graph traversal and evaluation by building in some of the functionality into `radicle-dag`, namely the pruning fold. We avoid building vectors of graph nodes this way, and simply iterate over the graph in one go.
This commit is contained in:
parent
0361ca3818
commit
0e14dacb99
|
|
@ -1,17 +1,16 @@
|
||||||
// Copyright © 2021 The Radicle Link Contributors
|
// Copyright © 2021 The Radicle Link Contributors
|
||||||
|
|
||||||
|
use std::ops::ControlFlow;
|
||||||
use std::{collections::BTreeSet, convert::TryInto};
|
use std::{collections::BTreeSet, convert::TryInto};
|
||||||
|
|
||||||
use git_ext::Oid;
|
use git_ext::Oid;
|
||||||
use radicle_dag::{Dag, Node};
|
use radicle_dag::Dag;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
change, object, signatures::ExtendedSignature, Change, CollaborativeObject, ObjectId, TypeName,
|
change, history::EntryId, object, signatures::ExtendedSignature, Change, CollaborativeObject,
|
||||||
|
Entry, History, ObjectId, TypeName,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod evaluation;
|
|
||||||
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,
|
||||||
|
|
@ -82,19 +81,46 @@ impl ChangeGraph {
|
||||||
|
|
||||||
/// Given a graph evaluate it to produce a collaborative object. This will
|
/// Given a graph evaluate it to produce a collaborative object. This will
|
||||||
/// filter out branches of the graph which do not have valid signatures.
|
/// filter out branches of the graph which do not have valid signatures.
|
||||||
pub(crate) fn evaluate(&self) -> CollaborativeObject {
|
pub(crate) fn evaluate(self) -> CollaborativeObject {
|
||||||
let mut roots: Vec<(&Oid, &Node<_, _>)> = self.graph.roots().collect();
|
let root = *self.object_id;
|
||||||
roots.sort_by_key(|(k, _)| *k);
|
let root_node = self
|
||||||
// This is okay because we check that the graph has a root node in
|
.graph
|
||||||
// GraphBuilder::build
|
.get(&root)
|
||||||
let (root, root_node) = roots.first().unwrap();
|
.expect("ChangeGraph::evaluate: root must be part of change graph");
|
||||||
|
|
||||||
let manifest = root_node.manifest.clone();
|
let manifest = root_node.manifest.clone();
|
||||||
let rng = fastrand::Rng::new();
|
let graph = self
|
||||||
let history = evaluate(*self.graph[*root].id(), &self.graph, rng);
|
.graph
|
||||||
|
.fold(&root, Dag::new(), |mut graph, _, change, depth| {
|
||||||
|
// Check the change signatures are valid.
|
||||||
|
if !change.valid_signatures() {
|
||||||
|
return ControlFlow::Break(graph);
|
||||||
|
}
|
||||||
|
let clock = depth as u64 + 1;
|
||||||
|
let entry = Entry::new(
|
||||||
|
*change.id(),
|
||||||
|
change.signature.key,
|
||||||
|
change.resource,
|
||||||
|
change.contents().clone(),
|
||||||
|
change.timestamp,
|
||||||
|
clock,
|
||||||
|
);
|
||||||
|
let id = *entry.id();
|
||||||
|
|
||||||
|
graph.node(id, entry);
|
||||||
|
|
||||||
|
for k in &change.dependents {
|
||||||
|
graph.dependency(EntryId::from(*k), id);
|
||||||
|
}
|
||||||
|
for k in &change.dependencies {
|
||||||
|
graph.dependency(id, EntryId::from(*k));
|
||||||
|
}
|
||||||
|
ControlFlow::Continue(graph)
|
||||||
|
});
|
||||||
|
|
||||||
CollaborativeObject {
|
CollaborativeObject {
|
||||||
manifest,
|
manifest,
|
||||||
history,
|
history: History::new((*root).into(), graph),
|
||||||
id: self.object_id,
|
id: self.object_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
// Copyright © 2021 The Radicle Link Contributors
|
|
||||||
|
|
||||||
use std::{collections::HashMap, ops::ControlFlow};
|
|
||||||
|
|
||||||
use git_ext::Oid;
|
|
||||||
use radicle_dag::Dag;
|
|
||||||
|
|
||||||
use crate::history::entry::{EntryId, EntryWithClock};
|
|
||||||
use crate::{change::Change, history, pruning_fold};
|
|
||||||
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// If the change corresponding to the root OID is not in `items`
|
|
||||||
pub fn evaluate(root: Oid, graph: &Dag<Oid, Change>, rng: fastrand::Rng) -> history::History {
|
|
||||||
let entries = pruning_fold::pruning_fold(
|
|
||||||
HashMap::<EntryId, EntryWithClock>::new(),
|
|
||||||
graph.sorted(rng).into_iter().map(|oid| {
|
|
||||||
let node = &graph[&oid];
|
|
||||||
let child_commits = node.dependents.iter().copied().collect();
|
|
||||||
|
|
||||||
ChangeWithChildren {
|
|
||||||
oid,
|
|
||||||
change: &node.value,
|
|
||||||
child_commits,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|mut entries, c| match evaluate_change(c.change, &c.child_commits) {
|
|
||||||
Err(RejectionReason::InvalidSignatures) => {
|
|
||||||
log::warn!(
|
|
||||||
"rejecting change '{}' because its signatures were invalid",
|
|
||||||
c.change.id(),
|
|
||||||
);
|
|
||||||
ControlFlow::Break(entries)
|
|
||||||
}
|
|
||||||
Ok(entry) => {
|
|
||||||
// Get parent commits and calculate this node's clock based on theirs.
|
|
||||||
let clock = graph[&c.oid]
|
|
||||||
.dependencies
|
|
||||||
.iter()
|
|
||||||
.map(|e| entries[&EntryId::from(*e)].clock())
|
|
||||||
.max()
|
|
||||||
.unwrap_or_default() // When there are no operations, the clock is zero.
|
|
||||||
+ 1;
|
|
||||||
log::trace!("change '{}' accepted", c.change.id());
|
|
||||||
|
|
||||||
entries.insert(*entry.id(), EntryWithClock { entry, clock });
|
|
||||||
|
|
||||||
ControlFlow::Continue(entries)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// SAFETY: The caller must guarantee that `root` is in `items`
|
|
||||||
history::History::new(root, entries).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn evaluate_change(
|
|
||||||
change: &Change,
|
|
||||||
child_commits: &[Oid],
|
|
||||||
) -> Result<history::Entry, RejectionReason> {
|
|
||||||
// Check the change signatures are valid
|
|
||||||
if !change.valid_signatures() {
|
|
||||||
return Err(RejectionReason::InvalidSignatures);
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(history::Entry::new(
|
|
||||||
*change.id(),
|
|
||||||
change.signature.key,
|
|
||||||
change.resource,
|
|
||||||
child_commits.iter().cloned(),
|
|
||||||
change.contents().clone(),
|
|
||||||
change.timestamp,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ChangeWithChildren<'a> {
|
|
||||||
oid: Oid,
|
|
||||||
change: &'a Change,
|
|
||||||
child_commits: Vec<Oid>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> pruning_fold::GraphNode for ChangeWithChildren<'a> {
|
|
||||||
type Id = Oid;
|
|
||||||
|
|
||||||
fn id(&self) -> &Self::Id {
|
|
||||||
self.change.id()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn child_ids(&self) -> &[Self::Id] {
|
|
||||||
&self.child_commits
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum RejectionReason {
|
|
||||||
InvalidSignatures,
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +1,19 @@
|
||||||
// Copyright © 2021 The Radicle Link Contributors
|
// Copyright © 2021 The Radicle Link Contributors
|
||||||
|
|
||||||
use std::{
|
use std::{cmp::Ordering, collections::BTreeSet, ops::ControlFlow};
|
||||||
collections::{BTreeSet, HashMap},
|
|
||||||
ops::ControlFlow,
|
|
||||||
};
|
|
||||||
|
|
||||||
use git_ext::Oid;
|
use git_ext::Oid;
|
||||||
use radicle_crypto::PublicKey;
|
use radicle_crypto::PublicKey;
|
||||||
use radicle_dag::Dag;
|
use radicle_dag::Dag;
|
||||||
|
|
||||||
use crate::pruning_fold;
|
|
||||||
|
|
||||||
pub mod entry;
|
pub mod entry;
|
||||||
pub use entry::{Clock, Contents, Entry, EntryId, EntryWithClock, Timestamp};
|
pub use entry::{Clock, Contents, Entry, EntryId, 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: Dag<EntryId, EntryWithClock>,
|
graph: Dag<EntryId, Entry>,
|
||||||
|
root: EntryId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for History {
|
impl PartialEq for History {
|
||||||
|
|
@ -28,13 +24,16 @@ impl PartialEq for History {
|
||||||
|
|
||||||
impl Eq for History {}
|
impl Eq for History {}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum CreateError {
|
|
||||||
#[error("no entry for the root ID in the entries")]
|
|
||||||
MissingRoot,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl History {
|
impl History {
|
||||||
|
/// Create a new history from a DAG. Panics if the root is not part of the graph.
|
||||||
|
pub fn new(root: EntryId, graph: Dag<EntryId, Entry>) -> Self {
|
||||||
|
assert!(
|
||||||
|
graph.contains(&root),
|
||||||
|
"History::new: root must be present in graph"
|
||||||
|
);
|
||||||
|
Self { root, graph }
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new_from_root<Id>(
|
pub fn new_from_root<Id>(
|
||||||
id: Id,
|
id: Id,
|
||||||
actor: PublicKey,
|
actor: PublicKey,
|
||||||
|
|
@ -46,29 +45,18 @@ impl History {
|
||||||
Id: Into<EntryId>,
|
Id: Into<EntryId>,
|
||||||
{
|
{
|
||||||
let id = id.into();
|
let id = id.into();
|
||||||
let root_entry = Entry {
|
let root = Entry {
|
||||||
id,
|
id,
|
||||||
actor,
|
actor,
|
||||||
resource,
|
resource,
|
||||||
children: vec![],
|
|
||||||
contents,
|
contents,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
clock: 1,
|
||||||
};
|
};
|
||||||
let mut entries = HashMap::new();
|
|
||||||
entries.insert(id, EntryWithClock::root(root_entry));
|
|
||||||
|
|
||||||
create_dag(&id, &entries)
|
Self {
|
||||||
}
|
root: id,
|
||||||
|
graph: Dag::root(id, root),
|
||||||
pub fn new<Id>(root: Id, entries: HashMap<EntryId, EntryWithClock>) -> Result<Self, CreateError>
|
|
||||||
where
|
|
||||||
Id: Into<EntryId>,
|
|
||||||
{
|
|
||||||
let root = root.into();
|
|
||||||
if !entries.contains_key(&root) {
|
|
||||||
Err(CreateError::MissingRoot)
|
|
||||||
} else {
|
|
||||||
Ok(create_dag(&root, &entries))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,30 +80,37 @@ impl History {
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all the tips of the graph.
|
||||||
|
pub fn tips(&self) -> BTreeSet<Oid> {
|
||||||
|
self.graph
|
||||||
|
.tips()
|
||||||
|
.map(|(_, entry)| (*entry.id()).into())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// A topological (parents before children) traversal of the dependency
|
/// A topological (parents before children) traversal of the dependency
|
||||||
/// graph of this history. This is analagous to
|
/// graph of this history. This is analagous to
|
||||||
/// [`std::iter::Iterator::fold`] in that it folds every change into an
|
/// [`std::iter::Iterator::fold`] in that it folds every change into an
|
||||||
/// accumulator value of type `A`. However, unlike `fold` the function `f`
|
/// accumulator value of type `A`. However, unlike `fold` the function `f`
|
||||||
/// may prune branches from the dependency graph by returning
|
/// may prune branches from the dependency graph by returning
|
||||||
/// `ControlFlow::Break`.
|
/// `ControlFlow::Break`.
|
||||||
pub fn traverse<F, A>(&self, init: A, f: F) -> A
|
pub fn traverse<F, A>(&self, init: A, mut f: F) -> A
|
||||||
where
|
where
|
||||||
F: for<'r> FnMut(A, &'r EntryWithClock) -> ControlFlow<A, A>,
|
F: for<'r> FnMut(A, &'r EntryId, &'r Entry) -> ControlFlow<A, A>,
|
||||||
{
|
{
|
||||||
let items = self
|
self.graph
|
||||||
.graph
|
.fold(&self.root, init, |acc, k, v, _| f(acc, k, v))
|
||||||
.sorted(fastrand::Rng::new())
|
|
||||||
.into_iter()
|
|
||||||
.map(|idx| &self.graph[&idx]);
|
|
||||||
|
|
||||||
pruning_fold::pruning_fold(init, items, f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tips(&self) -> BTreeSet<Oid> {
|
pub fn sorted<F>(&self, compare: F) -> impl Iterator<Item = &Entry>
|
||||||
|
where
|
||||||
|
F: FnMut(&EntryId, &EntryId) -> Ordering,
|
||||||
|
{
|
||||||
self.graph
|
self.graph
|
||||||
.tips()
|
.sorted(compare)
|
||||||
.map(|(_, entry)| (*entry.id()).into())
|
.into_iter()
|
||||||
.collect()
|
.filter_map(|k| self.graph.get(&k))
|
||||||
|
.map(|node| &node.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extend<Id>(
|
pub fn extend<Id>(
|
||||||
|
|
@ -134,17 +129,12 @@ impl History {
|
||||||
new_id,
|
new_id,
|
||||||
new_actor,
|
new_actor,
|
||||||
new_resource,
|
new_resource,
|
||||||
std::iter::empty::<git2::Oid>(),
|
|
||||||
new_contents,
|
new_contents,
|
||||||
new_timestamp,
|
new_timestamp,
|
||||||
|
self.clock() + 1,
|
||||||
);
|
);
|
||||||
self.graph.node(
|
self.graph.node(new_id, new_entry);
|
||||||
new_id,
|
|
||||||
EntryWithClock {
|
|
||||||
entry: new_entry,
|
|
||||||
clock: self.clock() + 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
for tip in tips {
|
for tip in tips {
|
||||||
self.graph.dependency(new_id, (*tip).into());
|
self.graph.dependency(new_id, (*tip).into());
|
||||||
}
|
}
|
||||||
|
|
@ -154,19 +144,3 @@ impl History {
|
||||||
self.graph.merge(other.graph);
|
self.graph.merge(other.graph);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_dag<'a>(root: &'a EntryId, entries: &'a HashMap<EntryId, EntryWithClock>) -> History {
|
|
||||||
let root_entry = entries.get(root).unwrap().clone();
|
|
||||||
let mut graph: Dag<EntryId, EntryWithClock> = Dag::root(*root, root_entry.clone());
|
|
||||||
let mut to_process = vec![root_entry];
|
|
||||||
|
|
||||||
while let Some(entry) = to_process.pop() {
|
|
||||||
for child_id in entry.children() {
|
|
||||||
let child = entries[child_id].clone();
|
|
||||||
graph.node(*child_id, child.clone());
|
|
||||||
graph.dependency(*child_id, entry.id);
|
|
||||||
to_process.push(child.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
History { graph }
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ use nonempty::NonEmpty;
|
||||||
use radicle_crypto::PublicKey;
|
use radicle_crypto::PublicKey;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::pruning_fold;
|
|
||||||
use crate::{object, ObjectId};
|
use crate::{object, ObjectId};
|
||||||
|
|
||||||
/// Entry contents.
|
/// Entry contents.
|
||||||
|
|
@ -90,43 +89,36 @@ pub struct Entry {
|
||||||
/// The content-address for the resource this entry lives under.
|
/// The content-address for the resource this entry lives under.
|
||||||
/// If the resource was updated, this should point to its latest version.
|
/// If the resource was updated, this should point to its latest version.
|
||||||
pub(super) resource: Oid,
|
pub(super) resource: Oid,
|
||||||
/// The child entries for this entry.
|
|
||||||
pub(super) children: Vec<EntryId>,
|
|
||||||
/// The contents of this entry.
|
/// The contents of this entry.
|
||||||
pub(super) contents: Contents,
|
pub(super) contents: Contents,
|
||||||
/// The entry timestamp, as seconds since epoch.
|
/// The entry timestamp, as seconds since epoch.
|
||||||
pub(super) timestamp: Timestamp,
|
pub(super) timestamp: Timestamp,
|
||||||
|
/// Logical clock.
|
||||||
|
pub(super) clock: Clock,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entry {
|
impl Entry {
|
||||||
pub fn new<Id1, Id2, ChildIds>(
|
pub fn new<Id>(
|
||||||
id: Id1,
|
id: Id,
|
||||||
actor: PublicKey,
|
actor: PublicKey,
|
||||||
resource: Oid,
|
resource: Oid,
|
||||||
children: ChildIds,
|
|
||||||
contents: Contents,
|
contents: Contents,
|
||||||
timestamp: Timestamp,
|
timestamp: Timestamp,
|
||||||
|
clock: Clock,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
Id1: Into<EntryId>,
|
Id: Into<EntryId>,
|
||||||
Id2: Into<EntryId>,
|
|
||||||
ChildIds: IntoIterator<Item = Id2>,
|
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
actor,
|
actor,
|
||||||
resource,
|
resource,
|
||||||
children: children.into_iter().map(|id| id.into()).collect(),
|
|
||||||
contents,
|
contents,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
clock,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The ids of the changes this change depends on
|
|
||||||
pub fn children(&self) -> impl Iterator<Item = &EntryId> {
|
|
||||||
self.children.iter()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The current `Oid` of the resource this change lives under.
|
/// The current `Oid` of the resource this change lives under.
|
||||||
pub fn resource(&self) -> Oid {
|
pub fn resource(&self) -> Oid {
|
||||||
self.resource
|
self.resource
|
||||||
|
|
@ -147,67 +139,13 @@ impl Entry {
|
||||||
&self.contents
|
&self.contents
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Entry ID.
|
||||||
pub fn id(&self) -> &EntryId {
|
pub fn id(&self) -> &EntryId {
|
||||||
&self.id
|
&self.id
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl pruning_fold::GraphNode for Entry {
|
/// Logical clock.
|
||||||
type Id = EntryId;
|
|
||||||
|
|
||||||
fn id(&self) -> &Self::Id {
|
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn child_ids(&self) -> &[Self::Id] {
|
|
||||||
&self.children
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wraps an [`Entry`], adding a logical clock to it.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
||||||
pub struct EntryWithClock {
|
|
||||||
pub entry: Entry,
|
|
||||||
pub clock: Clock,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EntryWithClock {
|
|
||||||
pub fn root(entry: Entry) -> Self {
|
|
||||||
Self {
|
|
||||||
entry,
|
|
||||||
clock: 1 as Clock, // The root entry has a clock value of `1`.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EntryWithClock {
|
|
||||||
/// Get the clock value.
|
|
||||||
pub fn clock(&self) -> Clock {
|
pub fn clock(&self) -> Clock {
|
||||||
self.clock
|
self.clock
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterator over the changes, including the clock.
|
|
||||||
pub fn changes(&self) -> impl Iterator<Item = &[u8]> {
|
|
||||||
self.contents.iter().map(|blob| blob.as_slice())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl pruning_fold::GraphNode for EntryWithClock {
|
|
||||||
type Id = EntryId;
|
|
||||||
|
|
||||||
fn id(&self) -> &Self::Id {
|
|
||||||
&self.entry.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn child_ids(&self) -> &[Self::Id] {
|
|
||||||
&self.entry.children
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Deref for EntryWithClock {
|
|
||||||
type Target = Entry;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.entry
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,6 @@ pub use change::Change;
|
||||||
pub mod history;
|
pub mod history;
|
||||||
pub use history::{Contents, Entry, History};
|
pub use history::{Contents, Entry, History};
|
||||||
|
|
||||||
mod pruning_fold;
|
|
||||||
|
|
||||||
pub mod signatures;
|
pub mod signatures;
|
||||||
use signatures::ExtendedSignature;
|
use signatures::ExtendedSignature;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
// Copyright © 2021 The Radicle Link Contributors
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
borrow::Borrow,
|
|
||||||
collections::{BTreeSet, HashMap},
|
|
||||||
ops::ControlFlow,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) trait GraphNode {
|
|
||||||
type Id: Clone + Eq + Ord + std::hash::Hash + std::fmt::Debug;
|
|
||||||
|
|
||||||
fn id(&self) -> &Self::Id;
|
|
||||||
fn child_ids(&self) -> &[Self::Id];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fold a topological sort of a directed acyclic graph, pruning some branches.
|
|
||||||
///
|
|
||||||
/// `items` must be an iterator over the nodes of the graph in topological
|
|
||||||
/// order. Assuming this is the case `fold` will only be called with nodes whose
|
|
||||||
/// ancestors have already been evaluated. Returning `ControlFlow::Break(..)`
|
|
||||||
/// from `fold` will omit evaluation of the current node and consequently omit
|
|
||||||
/// processing of any nodes who have the current node as an ancestor.
|
|
||||||
pub(crate) fn pruning_fold<'a, BN, Node, It, F, O>(init: O, items: It, mut f: F) -> O
|
|
||||||
where
|
|
||||||
BN: Borrow<Node> + 'a,
|
|
||||||
Node: 'a + GraphNode,
|
|
||||||
It: Iterator<Item = BN>,
|
|
||||||
F: for<'r> FnMut(O, &'r Node) -> std::ops::ControlFlow<O, O>,
|
|
||||||
{
|
|
||||||
let mut rejected = RejectedNodes::new();
|
|
||||||
let mut state = init;
|
|
||||||
for node in items {
|
|
||||||
// There can be multiple paths to a change so in a topological traversal we
|
|
||||||
// might encounter a change which we have already rejected
|
|
||||||
// previously
|
|
||||||
if rejected.is_rejected(node.borrow().id()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Some(rejected_ancestor) = rejected.rejected_ancestor(node.borrow().id()) {
|
|
||||||
let ancestor = rejected_ancestor.clone();
|
|
||||||
log::warn!(
|
|
||||||
"rejecting node because an ancestor change was rejected id='{:?}', ancestor='{:?}'",
|
|
||||||
node.borrow().id(),
|
|
||||||
rejected_ancestor
|
|
||||||
);
|
|
||||||
for child in node.borrow().child_ids() {
|
|
||||||
rejected.transitively_reject(child, &ancestor);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
state = match f(state, node.borrow()) {
|
|
||||||
ControlFlow::Continue(state) => state,
|
|
||||||
ControlFlow::Break(state) => {
|
|
||||||
rejected.directly_reject(node.borrow().id(), node.borrow().child_ids());
|
|
||||||
state
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
state
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RejectedNodes<NodeId> {
|
|
||||||
/// Changes which are directly rejected by the fold function
|
|
||||||
direct: BTreeSet<NodeId>,
|
|
||||||
/// A map from node IDs to the IDs of ancestor nodes which are
|
|
||||||
/// direct rejections
|
|
||||||
transitive: HashMap<NodeId, NodeId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<NodeId: Clone + Eq + Ord + std::hash::Hash> RejectedNodes<NodeId> {
|
|
||||||
fn new() -> RejectedNodes<NodeId> {
|
|
||||||
RejectedNodes {
|
|
||||||
direct: BTreeSet::new(),
|
|
||||||
transitive: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rejected_ancestor(&self, node: &NodeId) -> Option<&NodeId> {
|
|
||||||
self.transitive.get(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_rejected(&self, node: &NodeId) -> bool {
|
|
||||||
self.direct.contains(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn directly_reject(&mut self, node: &NodeId, children: &[NodeId]) {
|
|
||||||
self.direct.insert(node.clone());
|
|
||||||
for child in children {
|
|
||||||
self.transitive.insert(child.clone(), node.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transitively_reject(&mut self, child: &NodeId, rejected_ancestor: &NodeId) {
|
|
||||||
self.transitive
|
|
||||||
.insert(child.clone(), rejected_ancestor.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -207,7 +207,7 @@ fn traverse_cobs() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// traverse over the history and filter by changes that were only authorized by terry
|
// traverse over the history and filter by changes that were only authorized by terry
|
||||||
let contents = object.history().traverse(Vec::new(), |mut acc, entry| {
|
let contents = object.history().traverse(Vec::new(), |mut acc, _, entry| {
|
||||||
if entry.actor() == terry_signer.public_key() {
|
if entry.actor() == terry_signer.public_key() {
|
||||||
acc.push(entry.contents().head.clone());
|
acc.push(entry.contents().head.clone());
|
||||||
}
|
}
|
||||||
|
|
@ -217,7 +217,7 @@ fn traverse_cobs() {
|
||||||
assert_eq!(contents, vec![b"issue 1".to_vec()]);
|
assert_eq!(contents, vec![b"issue 1".to_vec()]);
|
||||||
|
|
||||||
// traverse over the history and filter by changes that were only authorized by neil
|
// traverse over the history and filter by changes that were only authorized by neil
|
||||||
let contents = object.history().traverse(Vec::new(), |mut acc, entry| {
|
let contents = object.history().traverse(Vec::new(), |mut acc, _, entry| {
|
||||||
acc.push(entry.contents().head.clone());
|
acc.push(entry.contents().head.clone());
|
||||||
ControlFlow::Continue(acc)
|
ControlFlow::Continue(acc)
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,31 @@
|
||||||
|
//! Directed-acyclic graph implementation.
|
||||||
|
#![warn(missing_docs)]
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Borrow,
|
borrow::Borrow,
|
||||||
collections::{HashMap, HashSet},
|
cmp::Ordering,
|
||||||
|
collections::{BTreeMap, BTreeSet, VecDeque},
|
||||||
fmt,
|
fmt,
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
ops::{Deref, Index},
|
ops::{ControlFlow, Deref, Index},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A node in the graph.
|
/// A node in the graph.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct Node<K: Eq + Hash, V> {
|
pub struct Node<K: Eq, V> {
|
||||||
/// The node value, stored by the user.
|
/// The node value, stored by the user.
|
||||||
pub value: V,
|
pub value: V,
|
||||||
/// Nodes depended on.
|
/// Nodes depended on.
|
||||||
pub dependencies: HashSet<K>,
|
pub dependencies: BTreeSet<K>,
|
||||||
/// Nodes depending on this node.
|
/// Nodes depending on this node.
|
||||||
pub dependents: HashSet<K>,
|
pub dependents: BTreeSet<K>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<K: Eq + Hash, V> Node<K, V> {
|
impl<K: Eq + Hash, V> Node<K, V> {
|
||||||
fn new(value: V) -> Self {
|
fn new(value: V) -> Self {
|
||||||
Self {
|
Self {
|
||||||
value,
|
value,
|
||||||
dependencies: HashSet::new(),
|
dependencies: BTreeSet::new(),
|
||||||
dependents: HashSet::new(),
|
dependents: BTreeSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -44,26 +47,27 @@ impl<K: Eq + Hash, V> Deref for Node<K, V> {
|
||||||
/// A directed acyclic graph.
|
/// A directed acyclic graph.
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
pub struct Dag<K: Eq + Hash, V> {
|
pub struct Dag<K: Eq + Hash, V> {
|
||||||
graph: HashMap<K, Node<K, V>>,
|
graph: BTreeMap<K, Node<K, V>>,
|
||||||
tips: HashSet<K>,
|
tips: BTreeSet<K>,
|
||||||
roots: HashSet<K>,
|
roots: BTreeSet<K>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<K: Eq + Copy + Hash, V> Dag<K, V> {
|
impl<K: PartialOrd + Ord + Eq + Copy + Hash, V> Dag<K, V> {
|
||||||
/// Create a new empty DAG.
|
/// Create a new empty DAG.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
graph: HashMap::new(),
|
graph: BTreeMap::new(),
|
||||||
tips: HashSet::new(),
|
tips: BTreeSet::new(),
|
||||||
roots: HashSet::new(),
|
roots: BTreeSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a DAG with a root node.
|
||||||
pub fn root(key: K, value: V) -> Self {
|
pub fn root(key: K, value: V) -> Self {
|
||||||
Self {
|
Self {
|
||||||
graph: HashMap::from_iter([(key, Node::new(value))]),
|
graph: BTreeMap::from_iter([(key, Node::new(value))]),
|
||||||
tips: HashSet::from_iter([key]),
|
tips: BTreeSet::from_iter([key]),
|
||||||
roots: HashSet::from_iter([key]),
|
roots: BTreeSet::from_iter([key]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,8 +89,8 @@ impl<K: Eq + Copy + Hash, V> Dag<K, V> {
|
||||||
key,
|
key,
|
||||||
Node {
|
Node {
|
||||||
value,
|
value,
|
||||||
dependencies: HashSet::new(),
|
dependencies: BTreeSet::new(),
|
||||||
dependents: HashSet::new(),
|
dependents: BTreeSet::new(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -136,30 +140,43 @@ impl<K: Eq + Copy + Hash, V> Dag<K, V> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge a DAG into this one.
|
/// Merge a DAG into this one.
|
||||||
///
|
pub fn merge(&mut self, mut other: Self) {
|
||||||
/// If a key exists in both graphs, its value is set to that of the other graph.
|
let Some((root, _)) = other.roots().next() else {
|
||||||
pub fn merge(&mut self, other: Self) {
|
return;
|
||||||
for k in other.tips.into_iter() {
|
};
|
||||||
self.tips.insert(k);
|
let mut visited = BTreeSet::new();
|
||||||
}
|
let mut queue = VecDeque::<K>::from([*root]);
|
||||||
for k in other.roots.into_iter() {
|
|
||||||
self.roots.insert(k);
|
while let Some(next) = queue.pop_front() {
|
||||||
}
|
if !visited.insert(next) {
|
||||||
for (k, v) in other.graph.into_iter() {
|
continue;
|
||||||
self.graph.insert(k, v);
|
}
|
||||||
|
if let Some(node) = other.graph.remove(&next) {
|
||||||
|
if !self.contains(&next) {
|
||||||
|
self.node(next, node.value);
|
||||||
|
}
|
||||||
|
for k in &node.dependents {
|
||||||
|
self.dependency(*k, next);
|
||||||
|
}
|
||||||
|
for k in &node.dependencies {
|
||||||
|
self.dependency(next, *k);
|
||||||
|
}
|
||||||
|
queue.extend(node.dependents.iter());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a topological ordering of the graph's nodes, using the given RNG.
|
/// Return a topological ordering of the graph's nodes.
|
||||||
/// Graphs with more than one partial order will return an arbitrary topological ordering.
|
/// Uses a comparison function to sort partially ordered nodes.
|
||||||
///
|
pub fn sorted<F>(&self, mut compare: F) -> Vec<K>
|
||||||
/// Calling this function over and over will eventually yield all possible orderings.
|
where
|
||||||
pub fn sorted(&self, rng: fastrand::Rng) -> Vec<K> {
|
F: FnMut(&K, &K) -> Ordering,
|
||||||
|
{
|
||||||
let mut order = Vec::new(); // Stores the topological order.
|
let mut order = Vec::new(); // Stores the topological order.
|
||||||
let mut visited = HashSet::new(); // Nodes that have been visited.
|
let mut visited = BTreeSet::new(); // Nodes that have been visited.
|
||||||
let mut keys = self.graph.keys().collect::<Vec<_>>();
|
let mut keys = self.graph.keys().collect::<Vec<_>>();
|
||||||
|
|
||||||
rng.shuffle(&mut keys);
|
keys.sort_by(|a, b| compare(a, b));
|
||||||
|
|
||||||
for node in keys {
|
for node in keys {
|
||||||
self.visit(node, &mut visited, &mut order);
|
self.visit(node, &mut visited, &mut order);
|
||||||
|
|
@ -167,8 +184,62 @@ impl<K: Eq + Copy + Hash, V> Dag<K, V> {
|
||||||
order
|
order
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fold over the graph in topological order, pruning branches along the way.
|
||||||
|
///
|
||||||
|
/// To continue traversing a branch, return [`ControlFlow::Continue`] from the
|
||||||
|
/// filter function. To stop traversal of a branch, return [`ControlFlow::Break`].
|
||||||
|
pub fn fold<A, F>(&self, root: &K, mut acc: A, mut filter: F) -> A
|
||||||
|
where
|
||||||
|
F: for<'r> FnMut(A, &'r K, &'r Node<K, V>, usize) -> ControlFlow<A, A>,
|
||||||
|
{
|
||||||
|
let mut visited = BTreeSet::new();
|
||||||
|
let mut queue = VecDeque::<(K, usize)>::from([(*root, 0)]);
|
||||||
|
|
||||||
|
while let Some((next, depth)) = queue.pop_front() {
|
||||||
|
if !visited.insert(next) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(node) = self.graph.get(&next) {
|
||||||
|
match filter(acc, &next, node, depth) {
|
||||||
|
ControlFlow::Continue(a) => {
|
||||||
|
queue.extend(node.dependents.iter().map(|k| (*k, depth + 1)));
|
||||||
|
acc = a;
|
||||||
|
}
|
||||||
|
ControlFlow::Break(a) => {
|
||||||
|
// When filtering out a node, we filter out all transitive dependents on
|
||||||
|
// that node by adding them to the already visited list.
|
||||||
|
visited.extend(self.descendants_of(node));
|
||||||
|
acc = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
acc
|
||||||
|
}
|
||||||
|
|
||||||
|
fn descendants_of(&self, from: &Node<K, V>) -> Vec<K> {
|
||||||
|
let mut visited = BTreeSet::new();
|
||||||
|
let mut stack = VecDeque::new();
|
||||||
|
let mut nodes = Vec::new();
|
||||||
|
|
||||||
|
stack.extend(from.dependents.iter());
|
||||||
|
|
||||||
|
while let Some(key) = stack.pop_front() {
|
||||||
|
if let Some(node) = self.graph.get(&key) {
|
||||||
|
if visited.insert(key) {
|
||||||
|
nodes.push(key);
|
||||||
|
|
||||||
|
for &neighbour in &node.dependents {
|
||||||
|
stack.push_back(neighbour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes
|
||||||
|
}
|
||||||
|
|
||||||
/// Add nodes recursively to the topological order, starting from the given node.
|
/// 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>) {
|
fn visit(&self, key: &K, visited: &mut BTreeSet<K>, order: &mut Vec<K>) {
|
||||||
if visited.contains(key) {
|
if visited.contains(key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -185,7 +256,7 @@ impl<K: Eq + Copy + Hash, V> Dag<K, V> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<K: Eq + Copy + Hash + fmt::Debug, V> Index<&K> for Dag<K, V> {
|
impl<K: PartialOrd + Ord + Eq + Copy + Hash + fmt::Debug, V> Index<&K> for Dag<K, V> {
|
||||||
type Output = Node<K, V>;
|
type Output = Node<K, V>;
|
||||||
|
|
||||||
fn index(&self, key: &K) -> &Self::Output {
|
fn index(&self, key: &K) -> &Self::Output {
|
||||||
|
|
@ -252,14 +323,14 @@ mod tests {
|
||||||
dag.dependency(0, 1);
|
dag.dependency(0, 1);
|
||||||
dag.dependency(1, 0);
|
dag.dependency(1, 0);
|
||||||
|
|
||||||
let sorted = dag.sorted(fastrand::Rng::new());
|
let sorted = dag.sorted(|a, b| a.cmp(b));
|
||||||
let expected: &[&[i32]] = &[&[0, 1], &[1, 0]];
|
let expected: &[&[i32]] = &[&[0, 1], &[1, 0]];
|
||||||
|
|
||||||
assert!(expected.contains(&sorted.as_slice()));
|
assert!(expected.contains(&sorted.as_slice()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_merge() {
|
fn test_merge_1() {
|
||||||
let mut a = Dag::new();
|
let mut a = Dag::new();
|
||||||
let mut b = Dag::new();
|
let mut b = Dag::new();
|
||||||
let mut c = Dag::new();
|
let mut c = Dag::new();
|
||||||
|
|
@ -282,6 +353,46 @@ mod tests {
|
||||||
assert!(c.has_dependency(&2, &0));
|
assert!(c.has_dependency(&2, &0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge_2() {
|
||||||
|
let mut a = Dag::new();
|
||||||
|
let mut b = Dag::new();
|
||||||
|
|
||||||
|
a.node(0, ());
|
||||||
|
a.node(1, ());
|
||||||
|
a.node(2, ());
|
||||||
|
a.dependency(1, 0);
|
||||||
|
a.dependency(2, 0);
|
||||||
|
|
||||||
|
b.node(0, ());
|
||||||
|
b.node(1, ());
|
||||||
|
b.node(2, ());
|
||||||
|
b.node(3, ());
|
||||||
|
b.node(4, ());
|
||||||
|
b.dependency(1, 0);
|
||||||
|
b.dependency(2, 0);
|
||||||
|
b.dependency(3, 0);
|
||||||
|
b.dependency(4, 2);
|
||||||
|
|
||||||
|
assert!(a.tips.contains(&2));
|
||||||
|
|
||||||
|
a.merge(b);
|
||||||
|
|
||||||
|
assert!(a.get(&0).is_some());
|
||||||
|
assert!(a.get(&1).is_some());
|
||||||
|
assert!(a.get(&2).is_some());
|
||||||
|
assert!(a.get(&3).is_some());
|
||||||
|
assert!(a.get(&4).is_some());
|
||||||
|
assert!(a.has_dependency(&4, &2));
|
||||||
|
assert!(a.get(&2).unwrap().dependents.contains(&4));
|
||||||
|
assert!(a.get(&0).unwrap().dependents.contains(&3));
|
||||||
|
assert!(a.tips.contains(&1));
|
||||||
|
assert!(!a.tips.contains(&2));
|
||||||
|
assert!(a.tips.contains(&3));
|
||||||
|
assert!(a.tips.contains(&4));
|
||||||
|
assert!(a.roots.contains(&0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_diamond() {
|
fn test_diamond() {
|
||||||
let mut dag = Dag::new();
|
let mut dag = Dag::new();
|
||||||
|
|
@ -301,7 +412,7 @@ mod tests {
|
||||||
|
|
||||||
// All of the possible sort orders for the above graph.
|
// All of the possible sort orders for the above graph.
|
||||||
let expected: &[&[i32]] = &[&[0, 1, 2, 3], &[0, 2, 1, 3]];
|
let expected: &[&[i32]] = &[&[0, 1, 2, 3], &[0, 2, 1, 3]];
|
||||||
let actual = dag.sorted(fastrand::Rng::new());
|
let actual = dag.sorted(|a, b| a.cmp(b));
|
||||||
|
|
||||||
assert!(expected.contains(&actual.as_slice()), "{actual:?}");
|
assert!(expected.contains(&actual.as_slice()), "{actual:?}");
|
||||||
}
|
}
|
||||||
|
|
@ -325,12 +436,12 @@ mod tests {
|
||||||
dag.dependency(1, 4);
|
dag.dependency(1, 4);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dag.tips().map(|(k, _)| *k).collect::<HashSet<_>>(),
|
dag.tips().map(|(k, _)| *k).collect::<BTreeSet<_>>(),
|
||||||
HashSet::from_iter([1, 0])
|
BTreeSet::from_iter([1, 0])
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dag.roots().map(|(k, _)| *k).collect::<HashSet<_>>(),
|
dag.roots().map(|(k, _)| *k).collect::<BTreeSet<_>>(),
|
||||||
HashSet::from_iter([4, 5])
|
BTreeSet::from_iter([4, 5])
|
||||||
);
|
);
|
||||||
|
|
||||||
// All of the possible sort orders for the above graph.
|
// All of the possible sort orders for the above graph.
|
||||||
|
|
@ -349,15 +460,126 @@ mod tests {
|
||||||
[5, 4, 2, 3, 0, 1],
|
[5, 4, 2, 3, 0, 1],
|
||||||
[5, 4, 2, 3, 1, 0],
|
[5, 4, 2, 3, 1, 0],
|
||||||
];
|
];
|
||||||
|
let mut sorts = BTreeSet::new();
|
||||||
let rng = fastrand::Rng::new();
|
let rng = fastrand::Rng::new();
|
||||||
let mut sorts = HashSet::new();
|
|
||||||
|
|
||||||
while sorts.len() < expected.len() {
|
while sorts.len() < expected.len() {
|
||||||
sorts.insert(dag.sorted(rng.clone()));
|
sorts.insert(dag.sorted(|a, b| if rng.bool() { a.cmp(b) } else { b.cmp(a) }));
|
||||||
}
|
}
|
||||||
for e in expected {
|
for e in expected {
|
||||||
assert!(sorts.remove(e.to_vec().as_slice()));
|
assert!(sorts.remove(e.to_vec().as_slice()));
|
||||||
}
|
}
|
||||||
assert!(sorts.is_empty());
|
assert!(sorts.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fold_sorting() {
|
||||||
|
let mut dag = Dag::new();
|
||||||
|
|
||||||
|
dag.node("R", ());
|
||||||
|
dag.node("A1", ());
|
||||||
|
dag.node("A2", ());
|
||||||
|
dag.node("A3", ());
|
||||||
|
dag.node("B1", ());
|
||||||
|
dag.node("B2", ());
|
||||||
|
dag.node("B3", ());
|
||||||
|
dag.node("C1", ());
|
||||||
|
|
||||||
|
dag.dependency("A1", "R");
|
||||||
|
dag.dependency("A2", "R");
|
||||||
|
dag.dependency("A3", "R");
|
||||||
|
|
||||||
|
dag.dependency("B1", "A1");
|
||||||
|
dag.dependency("B2", "A1");
|
||||||
|
dag.dependency("B3", "A2");
|
||||||
|
dag.dependency("B3", "A3");
|
||||||
|
|
||||||
|
dag.dependency("C1", "B1");
|
||||||
|
dag.dependency("C1", "B2");
|
||||||
|
dag.dependency("C1", "B3");
|
||||||
|
|
||||||
|
let acc = dag.fold(&"R", Vec::new(), |mut acc, key, _, _| {
|
||||||
|
acc.push(*key);
|
||||||
|
ControlFlow::Continue(acc)
|
||||||
|
});
|
||||||
|
assert_eq!(acc, vec!["R", "A1", "A2", "A3", "B1", "B2", "B3", "C1"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fold_depth() {
|
||||||
|
let mut dag = Dag::new();
|
||||||
|
|
||||||
|
dag.node("R", ());
|
||||||
|
dag.node("A1", ());
|
||||||
|
dag.node("A2", ());
|
||||||
|
dag.node("A3", ());
|
||||||
|
dag.node("B1", ());
|
||||||
|
dag.node("B2", ());
|
||||||
|
dag.node("B3", ());
|
||||||
|
dag.node("C1", ());
|
||||||
|
|
||||||
|
dag.dependency("A1", "R");
|
||||||
|
dag.dependency("A2", "R");
|
||||||
|
dag.dependency("A3", "R");
|
||||||
|
|
||||||
|
dag.dependency("B1", "A1");
|
||||||
|
dag.dependency("B2", "A1");
|
||||||
|
dag.dependency("B3", "A2");
|
||||||
|
dag.dependency("B3", "A3");
|
||||||
|
|
||||||
|
dag.dependency("C1", "B1");
|
||||||
|
dag.dependency("C1", "B2");
|
||||||
|
dag.dependency("C1", "B3");
|
||||||
|
|
||||||
|
let acc = dag.fold(&"R", Vec::new(), |mut acc, key, _, depth| {
|
||||||
|
acc.push((*key, depth));
|
||||||
|
ControlFlow::Continue(acc)
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
acc,
|
||||||
|
vec![
|
||||||
|
("R", 0),
|
||||||
|
("A1", 1),
|
||||||
|
("A2", 1),
|
||||||
|
("A3", 1),
|
||||||
|
("B1", 2),
|
||||||
|
("B2", 2),
|
||||||
|
("B3", 2),
|
||||||
|
("C1", 3)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fold_reject() {
|
||||||
|
let mut dag = Dag::new();
|
||||||
|
|
||||||
|
dag.node("R", true);
|
||||||
|
dag.node("A1", false); // Reject.
|
||||||
|
dag.node("A2", true);
|
||||||
|
dag.node("B1", true);
|
||||||
|
dag.node("C1", true);
|
||||||
|
dag.node("D1", true);
|
||||||
|
|
||||||
|
dag.dependency("A1", "R");
|
||||||
|
dag.dependency("A2", "R");
|
||||||
|
dag.dependency("B1", "A1");
|
||||||
|
dag.dependency("C1", "B1");
|
||||||
|
dag.dependency("D1", "C1");
|
||||||
|
dag.dependency("D1", "A2");
|
||||||
|
|
||||||
|
let a1 = dag.get(&"A1").unwrap();
|
||||||
|
assert_eq!(dag.descendants_of(a1), vec!["B1", "C1", "D1"]);
|
||||||
|
|
||||||
|
let acc = dag.fold(&"R", Vec::new(), |mut acc, key, accept, _| {
|
||||||
|
if !accept.value {
|
||||||
|
ControlFlow::Break(acc)
|
||||||
|
} else {
|
||||||
|
acc.push(*key);
|
||||||
|
ControlFlow::Continue(acc)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert_eq!(acc, vec!["R", "A2"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use nonempty::NonEmpty;
|
use nonempty::NonEmpty;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use radicle_cob::history::{EntryId, EntryWithClock};
|
use radicle_cob::history::{Entry, EntryId};
|
||||||
use radicle_crdt::clock;
|
use radicle_crdt::clock;
|
||||||
use radicle_crdt::clock::Lamport;
|
use radicle_crdt::clock::Lamport;
|
||||||
use radicle_crypto::PublicKey;
|
use radicle_crypto::PublicKey;
|
||||||
|
|
@ -78,19 +78,20 @@ impl<A> Op<A> {
|
||||||
|
|
||||||
pub struct Ops<A>(pub NonEmpty<Op<A>>);
|
pub struct Ops<A>(pub NonEmpty<Op<A>>);
|
||||||
|
|
||||||
impl<'a, A> TryFrom<&'a EntryWithClock> for Ops<A>
|
impl<'a, A> TryFrom<&'a Entry> for Ops<A>
|
||||||
where
|
where
|
||||||
for<'de> A: serde::Deserialize<'de>,
|
for<'de> A: serde::Deserialize<'de>,
|
||||||
{
|
{
|
||||||
type Error = OpEncodingError;
|
type Error = OpEncodingError;
|
||||||
|
|
||||||
fn try_from(entry: &'a EntryWithClock) -> Result<Self, Self::Error> {
|
fn try_from(entry: &'a Entry) -> Result<Self, Self::Error> {
|
||||||
let id = *entry.id();
|
let id = *entry.id();
|
||||||
let identity = entry.resource();
|
let identity = entry.resource();
|
||||||
let ops = entry
|
let ops = entry
|
||||||
.changes()
|
.contents()
|
||||||
|
.iter()
|
||||||
.map(|blob| {
|
.map(|blob| {
|
||||||
let action = serde_json::from_slice(blob)?;
|
let action = serde_json::from_slice(blob.as_slice())?;
|
||||||
let op = Op {
|
let op = Op {
|
||||||
id,
|
id,
|
||||||
action,
|
action,
|
||||||
|
|
@ -101,10 +102,19 @@ where
|
||||||
};
|
};
|
||||||
Ok::<_, Self::Error>(op)
|
Ok::<_, Self::Error>(op)
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
// SAFETY: Entry is guaranteed to have at least one operation.
|
// SAFETY: Entry is guaranteed to have at least one operation.
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
Ok(Self(NonEmpty::from_vec(ops).unwrap()))
|
Ok(Self(NonEmpty::from_vec(ops).unwrap()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<A: 'static> IntoIterator for Ops<A> {
|
||||||
|
type Item = Op<A>;
|
||||||
|
type IntoIter = <NonEmpty<Op<A>> as IntoIterator>::IntoIter;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.0.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ pub trait FromHistory: Sized + Default + PartialEq {
|
||||||
history: &History,
|
history: &History,
|
||||||
repo: &R,
|
repo: &R,
|
||||||
) -> Result<(Self, Lamport), Self::Error> {
|
) -> Result<(Self, Lamport), Self::Error> {
|
||||||
let obj = history.traverse(Self::default(), |mut acc, entry| {
|
|
||||||
match Ops::try_from(entry) {
|
match Ops::try_from(entry) {
|
||||||
Ok(Ops(ops)) => {
|
Ok(Ops(ops)) => {
|
||||||
if let Err(err) = acc.apply(ops, repo) {
|
if let Err(err) = acc.apply(ops, repo) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use std::ops::{ControlFlow, Deref};
|
use std::ops::Deref;
|
||||||
|
|
||||||
use nonempty::NonEmpty;
|
use nonempty::NonEmpty;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
@ -30,7 +30,7 @@ pub struct HistoryBuilder<T> {
|
||||||
|
|
||||||
impl<T: FromHistory> HistoryBuilder<T>
|
impl<T: FromHistory> HistoryBuilder<T>
|
||||||
where
|
where
|
||||||
T::Action: Serialize + Eq,
|
T::Action: Serialize + Eq + 'static,
|
||||||
{
|
{
|
||||||
pub fn new(op: &Op<T::Action>) -> HistoryBuilder<T> {
|
pub fn new(op: &Op<T::Action>) -> HistoryBuilder<T> {
|
||||||
let entry = arbitrary::oid();
|
let entry = arbitrary::oid();
|
||||||
|
|
@ -68,22 +68,23 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a sorted list of operations by traversing the history in topological order.
|
/// Return a sorted list of operations by traversing the history in topological order.
|
||||||
pub fn sorted(&self) -> Vec<Op<T::Action>> {
|
pub fn sorted(&self, rng: &mut fastrand::Rng) -> Vec<Op<T::Action>> {
|
||||||
self.history.traverse(Vec::new(), |mut acc, entry| {
|
self.history
|
||||||
let Ops(ops) =
|
.sorted(|a, b| if rng.bool() { a.cmp(b) } else { b.cmp(a) })
|
||||||
Ops::try_from(entry).expect("HistoryBuilder::sorted: operations must be valid");
|
.flat_map(|entry| {
|
||||||
acc.extend(ops);
|
Ops::try_from(entry).expect("HistoryBuilder::sorted: operations must be valid")
|
||||||
|
})
|
||||||
ControlFlow::Continue(acc)
|
.collect()
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `n` permutations of the topological ordering of operations.
|
/// Return `n` permutations of the topological ordering of operations.
|
||||||
/// *This function will never return if less than `n` permutations exist.*
|
/// *This function will never return if less than `n` permutations exist.*
|
||||||
pub fn permutations(&self, n: usize) -> impl IntoIterator<Item = Vec<Op<T::Action>>> {
|
pub fn permutations(&self, n: usize) -> impl IntoIterator<Item = Vec<Op<T::Action>>> {
|
||||||
let mut permutations = BTreeSet::new();
|
let mut permutations = BTreeSet::new();
|
||||||
|
let mut rng = fastrand::Rng::new();
|
||||||
|
|
||||||
while permutations.len() < n {
|
while permutations.len() < n {
|
||||||
permutations.insert(self.sorted());
|
permutations.insert(self.sorted(&mut rng));
|
||||||
}
|
}
|
||||||
permutations.into_iter()
|
permutations.into_iter()
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +101,7 @@ impl<A> Deref for HistoryBuilder<A> {
|
||||||
/// Create a new test history.
|
/// Create a new test history.
|
||||||
pub fn history<T: FromHistory>(op: &Op<T::Action>) -> HistoryBuilder<T>
|
pub fn history<T: FromHistory>(op: &Op<T::Action>) -> HistoryBuilder<T>
|
||||||
where
|
where
|
||||||
T::Action: Serialize + Eq,
|
T::Action: Serialize + Eq + 'static,
|
||||||
{
|
{
|
||||||
HistoryBuilder::new(op)
|
HistoryBuilder::new(op)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue