cob: Compute implicit monotonic clock on changes
Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
parent
1a5b68c2b3
commit
e83d47ff45
|
|
@ -113,11 +113,12 @@ impl ChangeGraph {
|
||||||
let child_commits = outgoing_edges
|
let child_commits = outgoing_edges
|
||||||
.map(|e| *self.graph[e.target()].id())
|
.map(|e| *self.graph[e.target()].id())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
(node, child_commits)
|
|
||||||
|
(node, idx, child_commits)
|
||||||
});
|
});
|
||||||
let history = {
|
let history = {
|
||||||
let root_change = &self.graph[*root];
|
let root_change = &self.graph[*root];
|
||||||
evaluate(*root_change.id(), items)
|
evaluate(*root_change.id(), &self.graph, items)
|
||||||
};
|
};
|
||||||
CollaborativeObject {
|
CollaborativeObject {
|
||||||
typename,
|
typename,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@
|
||||||
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 crate::history::entry::{EntryId, EntryWithClock};
|
||||||
use crate::{change::Change, history, pruning_fold};
|
use crate::{change::Change, history, pruning_fold};
|
||||||
|
|
||||||
/// # Panics
|
/// # Panics
|
||||||
|
|
@ -14,11 +16,13 @@ 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,
|
||||||
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 {
|
) -> history::History {
|
||||||
let entries = pruning_fold::pruning_fold(
|
let entries = pruning_fold::pruning_fold(
|
||||||
HashMap::new(),
|
HashMap::<EntryId, EntryWithClock>::new(),
|
||||||
items.map(|(change, children)| ChangeWithChildren {
|
items.map(|(change, idx, children)| ChangeWithChildren {
|
||||||
|
idx,
|
||||||
change,
|
change,
|
||||||
child_commits: children,
|
child_commits: children,
|
||||||
}),
|
}),
|
||||||
|
|
@ -31,8 +35,18 @@ pub fn evaluate<'b>(
|
||||||
ControlFlow::Break(entries)
|
ControlFlow::Break(entries)
|
||||||
}
|
}
|
||||||
Ok(entry) => {
|
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());
|
log::trace!("change '{}' accepted", c.change.id());
|
||||||
entries.insert((*c.change.id()).into(), entry);
|
|
||||||
|
entries.insert(*entry.id(), EntryWithClock { entry, clock });
|
||||||
|
|
||||||
ControlFlow::Continue(entries)
|
ControlFlow::Continue(entries)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -60,6 +74,7 @@ fn evaluate_change(
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ChangeWithChildren<'a> {
|
struct ChangeWithChildren<'a> {
|
||||||
|
idx: petgraph::graph::NodeIndex<u32>,
|
||||||
change: &'a Change,
|
change: &'a Change,
|
||||||
child_commits: Vec<Oid>,
|
child_commits: Vec<Oid>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use petgraph::visit::Walker as _;
|
||||||
use crate::pruning_fold;
|
use crate::pruning_fold;
|
||||||
|
|
||||||
pub mod entry;
|
pub mod entry;
|
||||||
pub use entry::{Contents, Entry, EntryId};
|
pub use entry::{Clock, Contents, Entry, EntryId, EntryWithClock};
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
|
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.
|
/// 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<Entry, (), petgraph::Directed, u32>,
|
graph: petgraph::Graph<EntryWithClock, (), petgraph::Directed, u32>,
|
||||||
indices: HashMap<EntryId, petgraph::graph::NodeIndex<u32>>,
|
indices: HashMap<EntryId, petgraph::graph::NodeIndex<u32>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,12 +66,12 @@ impl History {
|
||||||
contents,
|
contents,
|
||||||
};
|
};
|
||||||
let mut entries = HashMap::new();
|
let mut entries = HashMap::new();
|
||||||
entries.insert(id, root_entry.clone());
|
entries.insert(id, EntryWithClock::from(root_entry));
|
||||||
let NewGraph { graph, indices } = create_petgraph(&root_entry.id, &entries);
|
let NewGraph { graph, indices } = create_petgraph(&id, &entries);
|
||||||
Self { graph, indices }
|
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
|
where
|
||||||
Id: Into<EntryId>,
|
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
|
/// 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
|
||||||
|
|
@ -92,13 +102,13 @@ impl History {
|
||||||
/// `ControlFlow::Break`.
|
/// `ControlFlow::Break`.
|
||||||
pub fn traverse<F, A>(&self, init: A, f: F) -> A
|
pub fn traverse<F, A>(&self, init: A, f: F) -> A
|
||||||
where
|
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);
|
let topo = petgraph::visit::Topo::new(&self.graph);
|
||||||
#[allow(clippy::let_and_return)]
|
#[allow(clippy::let_and_return)]
|
||||||
let items = topo.iter(&self.graph).map(|idx| {
|
let items = topo.iter(&self.graph).map(|idx| {
|
||||||
let node = &self.graph[idx];
|
let entry = &self.graph[idx];
|
||||||
node
|
entry
|
||||||
});
|
});
|
||||||
pruning_fold::pruning_fold(init, items, f)
|
pruning_fold::pruning_fold(init, items, f)
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +141,10 @@ impl History {
|
||||||
std::iter::empty::<git2::Oid>(),
|
std::iter::empty::<git2::Oid>(),
|
||||||
new_contents,
|
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 {
|
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.update_edge(*tip_ix, new_ix, ());
|
||||||
|
|
@ -140,11 +153,14 @@ impl History {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NewGraph {
|
struct NewGraph {
|
||||||
graph: petgraph::Graph<Entry, (), petgraph::Directed, u32>,
|
graph: petgraph::Graph<EntryWithClock, (), petgraph::Directed, u32>,
|
||||||
indices: HashMap<EntryId, petgraph::graph::NodeIndex<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 graph = petgraph::Graph::new();
|
||||||
let mut indices = HashMap::<EntryId, petgraph::graph::NodeIndex<u32>>::new();
|
let mut indices = HashMap::<EntryId, petgraph::graph::NodeIndex<u32>>::new();
|
||||||
let root = entries.get(root).unwrap().clone();
|
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];
|
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());
|
let child_ix = graph.add_node(child.clone());
|
||||||
indices.insert(child.id, child_ix);
|
indices.insert(child.id, child_ix);
|
||||||
graph.update_edge(entry_ix, child_ix, ());
|
graph.update_edge(entry_ix, child_ix, ());
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ use crate::pruning_fold;
|
||||||
/// This is the change payload.
|
/// This is the change payload.
|
||||||
pub type Contents = Vec<u8>;
|
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.
|
/// A unique identifier for a history entry.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, PartialOrd, Ord)]
|
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, PartialOrd, Ord)]
|
||||||
pub struct EntryId(Oid);
|
pub struct EntryId(Oid);
|
||||||
|
|
@ -107,3 +110,46 @@ impl pruning_fold::GraphNode for Entry {
|
||||||
&self.children
|
&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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue