cob: Compute implicit monotonic clock on changes

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-11-29 23:52:14 +01:00
parent 1a5b68c2b3
commit e83d47ff45
No known key found for this signature in database
4 changed files with 97 additions and 19 deletions

View File

@ -113,11 +113,12 @@ impl ChangeGraph {
let child_commits = outgoing_edges
.map(|e| *self.graph[e.target()].id())
.collect::<Vec<_>>();
(node, child_commits)
(node, idx, child_commits)
});
let history = {
let root_change = &self.graph[*root];
evaluate(*root_change.id(), items)
evaluate(*root_change.id(), &self.graph, items)
};
CollaborativeObject {
typename,

View File

@ -6,7 +6,9 @@
use std::{collections::HashMap, ops::ControlFlow};
use git_ext::Oid;
use petgraph::{visit::EdgeRef, EdgeDirection};
use crate::history::entry::{EntryId, EntryWithClock};
use crate::{change::Change, history, pruning_fold};
/// # Panics
@ -14,11 +16,13 @@ use crate::{change::Change, history, pruning_fold};
/// If the change corresponding to the root OID is not in `items`
pub fn evaluate<'b>(
root: Oid,
items: impl Iterator<Item = (&'b Change, Vec<Oid>)>,
graph: &petgraph::Graph<Change, ()>,
items: impl Iterator<Item = (&'b Change, petgraph::graph::NodeIndex<u32>, Vec<Oid>)>,
) -> history::History {
let entries = pruning_fold::pruning_fold(
HashMap::new(),
items.map(|(change, children)| ChangeWithChildren {
HashMap::<EntryId, EntryWithClock>::new(),
items.map(|(change, idx, children)| ChangeWithChildren {
idx,
change,
child_commits: children,
}),
@ -31,8 +35,18 @@ pub fn evaluate<'b>(
ControlFlow::Break(entries)
}
Ok(entry) => {
// Get parent commits and calculate this node's clock based on theirs.
let incoming = graph.edges_directed(c.idx, EdgeDirection::Incoming);
let clock = incoming
.into_iter()
.map(|e| entries[&graph[e.source()].id.into()].clock())
.max()
.map(|n| n + 1)
.unwrap_or_default();
log::trace!("change '{}' accepted", c.change.id());
entries.insert((*c.change.id()).into(), entry);
entries.insert(*entry.id(), EntryWithClock { entry, clock });
ControlFlow::Continue(entries)
}
},
@ -60,6 +74,7 @@ fn evaluate_change(
}
struct ChangeWithChildren<'a> {
idx: petgraph::graph::NodeIndex<u32>,
change: &'a Change,
child_commits: Vec<Oid>,
}

View File

@ -14,7 +14,7 @@ use petgraph::visit::Walker as _;
use crate::pruning_fold;
pub mod entry;
pub use entry::{Contents, Entry, EntryId};
pub use entry::{Clock, Contents, Entry, EntryId, EntryWithClock};
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
@ -29,7 +29,7 @@ pub enum HistoryType {
/// The DAG of changes making up the history of a collaborative object.
#[derive(Clone, Debug)]
pub struct History {
graph: petgraph::Graph<Entry, (), petgraph::Directed, u32>,
graph: petgraph::Graph<EntryWithClock, (), petgraph::Directed, u32>,
indices: HashMap<EntryId, petgraph::graph::NodeIndex<u32>>,
}
@ -66,12 +66,12 @@ impl History {
contents,
};
let mut entries = HashMap::new();
entries.insert(id, root_entry.clone());
let NewGraph { graph, indices } = create_petgraph(&root_entry.id, &entries);
entries.insert(id, EntryWithClock::from(root_entry));
let NewGraph { graph, indices } = create_petgraph(&id, &entries);
Self { graph, indices }
}
pub fn new<Id>(root: Id, entries: HashMap<EntryId, Entry>) -> Result<Self, CreateError>
pub fn new<Id>(root: Id, entries: HashMap<EntryId, EntryWithClock>) -> Result<Self, CreateError>
where
Id: Into<EntryId>,
{
@ -84,6 +84,16 @@ impl History {
}
}
/// Get the current value of the logical clock.
/// This is the maximum value of all tips.
pub fn clock(&self) -> Clock {
self.graph
.externals(petgraph::Direction::Outgoing)
.map(|n| self.graph[n].clock)
.max()
.unwrap_or_default()
}
/// A topological (parents before children) traversal of the dependency
/// graph of this history. This is analagous to
/// [`std::iter::Iterator::fold`] in that it folds every change into an
@ -92,13 +102,13 @@ impl History {
/// `ControlFlow::Break`.
pub fn traverse<F, A>(&self, init: A, f: F) -> A
where
F: for<'r> FnMut(A, &'r Entry) -> ControlFlow<A, A>,
F: for<'r> FnMut(A, &'r EntryWithClock) -> ControlFlow<A, A>,
{
let topo = petgraph::visit::Topo::new(&self.graph);
#[allow(clippy::let_and_return)]
let items = topo.iter(&self.graph).map(|idx| {
let node = &self.graph[idx];
node
let entry = &self.graph[idx];
entry
});
pruning_fold::pruning_fold(init, items, f)
}
@ -131,7 +141,10 @@ impl History {
std::iter::empty::<git2::Oid>(),
new_contents,
);
let new_ix = self.graph.add_node(new_entry);
let new_ix = self.graph.add_node(EntryWithClock {
entry: new_entry,
clock: self.clock() + 1,
});
for tip in tips {
let tip_ix = self.indices.get(&tip.into()).unwrap();
self.graph.update_edge(*tip_ix, new_ix, ());
@ -140,11 +153,14 @@ impl History {
}
struct NewGraph {
graph: petgraph::Graph<Entry, (), petgraph::Directed, u32>,
graph: petgraph::Graph<EntryWithClock, (), petgraph::Directed, u32>,
indices: HashMap<EntryId, petgraph::graph::NodeIndex<u32>>,
}
fn create_petgraph<'a>(root: &'a EntryId, entries: &'a HashMap<EntryId, Entry>) -> NewGraph {
fn create_petgraph<'a>(
root: &'a EntryId,
entries: &'a HashMap<EntryId, EntryWithClock>,
) -> NewGraph {
let mut graph = petgraph::Graph::new();
let mut indices = HashMap::<EntryId, petgraph::graph::NodeIndex<u32>>::new();
let root = entries.get(root).unwrap().clone();
@ -153,8 +169,8 @@ fn create_petgraph<'a>(root: &'a EntryId, entries: &'a HashMap<EntryId, Entry>)
let mut to_process = vec![root];
while let Some(entry) = to_process.pop() {
let entry_ix = indices[&entry.id];
for child_id in entry.children {
let child = entries[&child_id].clone();
for child_id in entry.children() {
let child = entries[child_id].clone();
let child_ix = graph.add_node(child.clone());
indices.insert(child.id, child_ix);
graph.update_edge(entry_ix, child_ix, ());

View File

@ -11,6 +11,9 @@ use crate::pruning_fold;
/// This is the change payload.
pub type Contents = Vec<u8>;
/// Logical clock used to track causality in change graph.
pub type Clock = u64;
/// A unique identifier for a history entry.
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, PartialOrd, Ord)]
pub struct EntryId(Oid);
@ -107,3 +110,46 @@ impl pruning_fold::GraphNode for Entry {
&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 From<Entry> for EntryWithClock {
fn from(entry: Entry) -> Self {
Self {
entry,
clock: Clock::default(),
}
}
}
impl EntryWithClock {
/// Get the clock value.
pub fn clock(&self) -> Clock {
self.clock
}
}
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
}
}