crdt: Include timestamp in `Change`

We can then use the real change timestamp for our COBs.

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-12-01 12:57:13 +01:00
parent d061afdab1
commit 64ce15dbe2
No known key found for this signature in database
9 changed files with 103 additions and 79 deletions

View File

@ -2,10 +2,9 @@ use std::{fmt, time};
pub use dialoguer::console::style;
use radicle::cob::Timestamp;
use radicle::cob::{ObjectId, Timestamp};
use radicle::node::NodeId;
use radicle::profile::Profile;
use radicle_cob::ObjectId;
use crate::terminal as term;

View File

@ -89,7 +89,7 @@ impl History {
/// Get the current history timestamp.
/// This is the latest timestamp of any tip.
pub fn timestamp(&self) -> Clock {
pub fn timestamp(&self) -> Timestamp {
self.graph
.externals(petgraph::Direction::Outgoing)
.map(|n| self.graph[n].timestamp())

View File

@ -3,6 +3,7 @@ use std::collections::BTreeMap;
use radicle_crypto::{PublicKey, Signature, Signer};
use serde::{Deserialize, Serialize};
use crate::clock;
use crate::clock::Lamport;
/// Identifies a change.
@ -21,6 +22,8 @@ pub struct Change<A> {
pub author: ActorId,
/// Lamport clock.
pub clock: Lamport,
/// Timestamp of this change.
pub timestamp: clock::Physical,
}
impl<A> Change<A> {
@ -101,10 +104,12 @@ impl<G: Signer, A: Clone + Serialize> Actor<G, A> {
pub fn change(&mut self, action: A) -> Change<A> {
let author = *self.signer.public_key();
let clock = self.clock;
let timestamp = clock::Physical::now();
let change = Change {
action,
author,
clock,
timestamp,
};
self.changes.insert((self.clock, author), change.clone());
self.clock.tick();

View File

@ -1,3 +1,6 @@
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use num_traits::Bounded;
use serde::{Deserialize, Serialize};
@ -54,3 +57,59 @@ impl Bounded for Lamport {
Self::from(u64::max_value())
}
}
/// Physical clock. Tracks real-time by the second.
#[derive(Debug, Default, Copy, Clone, PartialOrd, PartialEq, Ord, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Physical {
seconds: u64,
}
impl Physical {
pub fn new(seconds: u64) -> Self {
Self { seconds }
}
pub fn now() -> Self {
#[allow(clippy::unwrap_used)] // Safe because Unix was already invented!
let duration = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
Self {
seconds: duration.as_secs(),
}
}
pub fn as_secs(&self) -> u64 {
self.seconds
}
}
impl From<u64> for Physical {
fn from(seconds: u64) -> Self {
Self { seconds }
}
}
impl std::ops::Add<u64> for Physical {
type Output = Self;
fn add(self, rhs: u64) -> Self::Output {
Self {
seconds: self.seconds + rhs,
}
}
}
impl Bounded for Physical {
fn min_value() -> Self {
Self {
seconds: u64::min_value(),
}
}
fn max_value() -> Self {
Self {
seconds: u64::max_value(),
}
}
}

View File

@ -4,6 +4,8 @@ pub mod patch;
pub mod store;
pub mod thread;
pub use radicle_crdt::clock::Physical as Timestamp;
pub use cob::{
identity, object::collaboration::error, CollaborativeObject, Contents, Create, Entry, History,
ObjectId, TypeName, Update,

View File

@ -1,12 +1,12 @@
use std::fmt;
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
use num_traits::Bounded;
use serde::{Deserialize, Serialize};
use crate::prelude::*;
pub use radicle_crdt::clock::Physical as Timestamp;
/// Author.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Author {
@ -23,55 +23,6 @@ impl Author {
}
}
#[derive(Debug, Default, Copy, Clone, PartialOrd, PartialEq, Ord, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Timestamp {
seconds: u64,
}
impl Timestamp {
pub fn new(seconds: u64) -> Self {
Self { seconds }
}
pub fn now() -> Self {
#[allow(clippy::unwrap_used)] // Safe because Unix was already invented!
let duration = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
Self {
seconds: duration.as_secs(),
}
}
pub fn as_secs(&self) -> u64 {
self.seconds
}
}
impl std::ops::Add<u64> for Timestamp {
type Output = Self;
fn add(self, rhs: u64) -> Self::Output {
Self {
seconds: self.seconds + rhs,
}
}
}
impl Bounded for Timestamp {
fn min_value() -> Self {
Self {
seconds: u64::min_value(),
}
}
fn max_value() -> Self {
Self {
seconds: u64::max_value(),
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum ReactionError {
#[error("invalid reaction")]

View File

@ -113,6 +113,7 @@ impl store::FromHistory for Issue {
action,
author: *entry.actor(),
clock: entry.clock().into(),
timestamp: entry.timestamp().into(),
}) {
log::warn!("Error applying change to issue state: {err}");
return ControlFlow::Break(acc);
@ -168,6 +169,7 @@ impl Issue {
action,
author: change.author,
clock: change.clock,
timestamp: change.timestamp,
}]);
}
}
@ -275,16 +277,17 @@ impl<'a, 'g> IssueMut<'a, 'g> {
.store
.update(self.id, msg, action.clone(), signer)
.map_err(Error::Store)?;
let clock = cob.history().clock();
let clock = cob.history().clock().into();
let timestamp = cob.history().timestamp().into();
let change = Change {
author: *signer.public_key(),
action,
clock: clock.into(),
author: *signer.public_key(),
clock,
timestamp,
};
self.issue.apply(change)?;
Ok((clock.into(), *signer.public_key()))
Ok((clock, *signer.public_key()))
}
}

View File

@ -13,11 +13,10 @@ use radicle_crdt::{ActorId, ChangeId, LWWReg, LWWSet, Max, Redactable, Semilatti
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::cob::common::{Author, Tag};
use crate::cob::common::{Author, Tag, Timestamp};
use crate::cob::thread;
use crate::cob::thread::CommentId;
use crate::cob::thread::Thread;
use crate::cob::Timestamp;
use crate::cob::{store, ObjectId, TypeName};
use crate::crypto::{PublicKey, Signer};
use crate::git;
@ -31,6 +30,7 @@ pub use clock::Lamport as Clock;
pub static TYPENAME: Lazy<TypeName> =
Lazy::new(|| FromStr::from_str("xyz.radicle.patch").expect("type name is valid"));
/// Patch change.
pub type Change = crdt::Change<Action>;
/// Identifier for a patch.
@ -240,8 +240,7 @@ impl Patch {
pub fn apply_one(&mut self, change: Change) -> Result<(), ApplyError> {
let id = change.id();
let author = Author::new(change.author);
// FIXME(cloudhead): Use commit timestamp.
let timestamp = Timestamp::default();
let timestamp = change.timestamp;
match change.action {
Action::Edit {
@ -318,6 +317,7 @@ impl Patch {
action,
author: change.author,
clock: change.clock,
timestamp,
}]);
} else {
return Err(ApplyError::Missing(revision));
@ -344,6 +344,7 @@ impl store::FromHistory for Patch {
action,
author: *entry.actor(),
clock: entry.clock().into(),
timestamp: entry.timestamp().into(),
}]) {
log::warn!("Error applying change to patch state: {err}");
return ControlFlow::Break(acc);
@ -669,20 +670,21 @@ impl<'a, 'g> PatchMut<'a, 'g> {
action: Action,
signer: &G,
) -> Result<ChangeId, Error> {
let change = Change {
author: *signer.public_key(),
action: action.clone(),
clock: self.clock.tick(),
};
self.patch.apply([change])?;
let cob = self
.store
.update(self.id, msg, action, signer)
.update(self.id, msg, action.clone(), signer)
.map_err(Error::Store)?;
let clock = cob.history().clock();
let clock = cob.history().clock().into();
let timestamp = cob.history().timestamp().into();
let change = Change {
action,
author: *signer.public_key(),
clock,
timestamp,
};
self.patch.apply_one(change)?;
Ok((clock.into(), *signer.public_key()))
Ok((clock, *signer.public_key()))
}
}
@ -868,12 +870,14 @@ mod test {
let mut changes = Vec::new();
let mut permutations: [Vec<Change>; N] = array::from_fn(|_| Vec::new());
let timestamp = Timestamp::now() + rng.u64(..60);
for (clock, action) in gen.take(g.size()) {
changes.push(Change {
action,
author,
clock,
timestamp,
});
}

View File

@ -8,9 +8,9 @@ use once_cell::sync::Lazy;
use radicle_crdt as crdt;
use serde::{Deserialize, Serialize};
use crate::cob::common::{Reaction, Tag};
use crate::cob::common::{Reaction, Tag, Timestamp};
use crate::cob::store;
use crate::cob::{History, Timestamp, TypeName};
use crate::cob::{History, TypeName};
use crate::crypto::Signer;
use crdt::clock::Lamport;
@ -114,6 +114,7 @@ impl store::FromHistory for Thread {
action,
author: *entry.actor(),
clock: entry.clock().into(),
timestamp: entry.timestamp().into(),
}]);
ControlFlow::Continue(acc)
} else {
@ -187,15 +188,13 @@ impl Thread {
}
pub fn apply(&mut self, changes: impl IntoIterator<Item = Change<Action>>) {
// FIXME(cloudhead): Use commit timestamp.
let timestamp = Timestamp::default();
for change in changes.into_iter() {
let id = change.id();
match change.action {
Action::Comment { body, reply_to } => {
let present = Redactable::Present(Comment::new(body, reply_to, timestamp));
let present =
Redactable::Present(Comment::new(body, reply_to, change.timestamp));
match self.comments.entry(id) {
Entry::Vacant(e) => {
@ -436,12 +435,14 @@ mod tests {
let mut changes = Vec::new();
let mut permutations: [Vec<Change<Action>>; N] = array::from_fn(|_| Vec::new());
let timestamp = Timestamp::now() + rng.u64(..60);
for (clock, action) in gen.take(g.size().min(8)) {
changes.push(Change {
action,
author,
clock,
timestamp,
});
}