Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
crdt: Require `LWWReg` value to be `Semilattice`
Alexis Sellier committed 3 years ago
commit cc33d04a7ba8ad181ca42b7ab307b8159df883de
parent cc64c1d80bfbd6e4126918ff40f51e6845b78df3
7 files changed +187 -51
modified radicle-crdt/Cargo.toml
@@ -4,12 +4,13 @@ version = "0.1.0"
edition = "2021"

[features]
-
test = ["fastrand"]
+
test = ["fastrand", "quickcheck"]

[dependencies]
fastrand = { version = "1.8.0", optional = true }
num-traits = { version = "0.2.15", default-features = false, features = ["std"] }
olpc-cjson = { version = "0.1.1" }
+
quickcheck = { version = "1", optional = true }
serde = { version = "1" }
serde_json = { version = "1" }

modified radicle-crdt/src/lib.rs
@@ -1,4 +1,5 @@
#![allow(clippy::collapsible_if)]
+
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::collapsible_else_if)]
#![allow(clippy::type_complexity)]
pub mod change;
@@ -15,6 +16,12 @@ pub mod test;
////////////////////////////////////////////////////////////////////////////////

pub use change::*;
+
pub use clock::LClock;
+
pub use lwwmap::LWWMap;
+
pub use lwwreg::LWWReg;
+
pub use lwwset::LWWSet;
+
pub use ord::{Max, Min};
+
pub use redactable::Redactable;

////////////////////////////////////////////////////////////////////////////////

@@ -70,9 +77,78 @@ impl<K: Hash + PartialEq + Eq, V: Semilattice> Semilattice for HashMap<K, V> {
    }
}

+
impl<T: Semilattice> Semilattice for Option<T> {
+
    fn merge(&mut self, other: Self) {
+
        match (self, other) {
+
            (this @ None, other @ Some(_)) => {
+
                *this = other;
+
            }
+
            (Some(ref mut a), Some(b)) => {
+
                a.merge(b);
+
            }
+
            (Some(_), None) => {}
+
            (None, None) => {}
+
        }
+
    }
+
}
+

+
impl Semilattice for () {
+
    fn merge(&mut self, _other: Self) {}
+
}
+

+
impl Semilattice for bool {
+
    fn merge(&mut self, other: Self) {
+
        match (&self, other) {
+
            (false, true) => *self = true,
+
            (true, false) => *self = true,
+
            (false, false) | (true, true) => {}
+
        }
+
    }
+
}
+

pub fn fold<S>(i: impl IntoIterator<Item = S>) -> S
where
    S: Semilattice + Default,
{
    i.into_iter().fold(S::default(), S::join)
}
+

+
#[cfg(test)]
+
mod tests {
+
    use crate::{test, Max, Min, Semilattice};
+
    use quickcheck_macros::quickcheck;
+

+
    #[quickcheck]
+
    fn prop_option_laws(a: Max<u8>, b: Max<u8>, c: Max<u8>) {
+
        test::assert_laws(&a, &b, &c);
+
    }
+

+
    #[quickcheck]
+
    fn prop_bool_laws(a: bool, b: bool, c: bool) {
+
        test::assert_laws(&a, &b, &c);
+
    }
+

+
    #[test]
+
    fn test_bool() {
+
        assert_eq!(false.join(false), false);
+
        assert_eq!(true.join(true), true);
+
        assert_eq!(true.join(false), true);
+
        assert_eq!(false.join(true), true);
+
    }
+

+
    #[test]
+
    fn test_option() {
+
        assert_eq!(None::<()>.join(None), None);
+
        assert_eq!(None::<()>.join(Some(())), Some(()));
+
        assert_eq!(Some(()).join(None), Some(()));
+
        assert_eq!(Some(()).join(Some(())), Some(()));
+
        assert_eq!(
+
            Some(Max::from(0)).join(Some(Max::from(1))),
+
            Some(Max::from(1))
+
        );
+
        assert_eq!(
+
            Some(Min::from(0)).join(Some(Min::from(1))),
+
            Some(Min::from(0))
+
        );
+
    }
+
}
modified radicle-crdt/src/lwwmap.rs
@@ -10,7 +10,7 @@ pub struct LWWMap<K, V, C> {
    inner: BTreeMap<K, LWWReg<Option<V>, C>>,
}

-
impl<K: Ord, V: PartialOrd + Eq, C: PartialOrd + Ord + Copy> LWWMap<K, V, C> {
+
impl<K: Ord, V: Semilattice + PartialOrd + Eq, C: PartialOrd + Ord + Copy> LWWMap<K, V, C> {
    pub fn singleton(key: K, value: V, clock: C) -> Self {
        Self {
            inner: BTreeMap::from_iter([(key, LWWReg::new(Some(value), clock))]),
@@ -66,7 +66,9 @@ impl<K, V, C> Default for LWWMap<K, V, C> {
    }
}

-
impl<K: Ord, V: PartialOrd + Eq, C: Copy + Ord> FromIterator<(K, V, C)> for LWWMap<K, V, C> {
+
impl<K: Ord, V: Semilattice + PartialOrd + Eq, C: Copy + Ord> FromIterator<(K, V, C)>
+
    for LWWMap<K, V, C>
+
{
    fn from_iter<I: IntoIterator<Item = (K, V, C)>>(iter: I) -> Self {
        let mut map = LWWMap::default();
        for (k, v, c) in iter.into_iter() {
@@ -76,7 +78,9 @@ impl<K: Ord, V: PartialOrd + Eq, C: Copy + Ord> FromIterator<(K, V, C)> for LWWM
    }
}

-
impl<K: Ord, V: PartialOrd + Eq, C: Ord + Copy> Extend<(K, V, C)> for LWWMap<K, V, C> {
+
impl<K: Ord, V: Semilattice + PartialOrd + Eq, C: Ord + Copy> Extend<(K, V, C)>
+
    for LWWMap<K, V, C>
+
{
    fn extend<I: IntoIterator<Item = (K, V, C)>>(&mut self, iter: I) {
        for (k, v, c) in iter.into_iter() {
            self.insert(k, v, c);
@@ -87,7 +91,7 @@ impl<K: Ord, V: PartialOrd + Eq, C: Ord + Copy> Extend<(K, V, C)> for LWWMap<K,
impl<K, V, C> Semilattice for LWWMap<K, V, C>
where
    K: Ord,
-
    V: PartialOrd + Eq,
+
    V: Semilattice + PartialOrd + Eq,
    C: Ord + Copy + Default,
{
    fn merge(&mut self, other: Self) {
@@ -106,15 +110,17 @@ where

#[cfg(test)]
mod tests {
-
    use super::*;
    use quickcheck_macros::quickcheck;

+
    use super::*;
+
    use crate::ord::Max;
+

    #[quickcheck]
    fn prop_semilattice(
-
        a: Vec<(u8, u8, u16)>,
-
        b: Vec<(u8, u8, u16)>,
-
        c: Vec<(u8, u8, u16)>,
-
        mix: Vec<(u8, u8, u16)>,
+
        a: Vec<(u8, Max<u8>, u16)>,
+
        b: Vec<(u8, Max<u8>, u16)>,
+
        c: Vec<(u8, Max<u8>, u16)>,
+
        mix: Vec<(u8, Max<u8>, u16)>,
    ) {
        let mut a = LWWMap::from_iter(a);
        let mut b = LWWMap::from_iter(b);
@@ -130,18 +136,18 @@ mod tests {
    fn test_insert() {
        let mut map = LWWMap::default();

-
        map.insert('a', 1, 0);
-
        map.insert('b', 2, 0);
-
        map.insert('c', 3, 0);
+
        map.insert('a', Max::from(1), 0);
+
        map.insert('b', Max::from(2), 0);
+
        map.insert('c', Max::from(3), 0);

-
        assert_eq!(map.get('a'), Some(&1));
-
        assert_eq!(map.get('b'), Some(&2));
+
        assert_eq!(map.get('a'), Some(&Max::from(1)));
+
        assert_eq!(map.get('b'), Some(&Max::from(2)));
        assert_eq!(map.get('?'), None);

-
        let values = map.iter().collect::<Vec<(&char, &u8)>>();
-
        assert!(values.contains(&(&'a', &1)));
-
        assert!(values.contains(&(&'b', &2)));
-
        assert!(values.contains(&(&'c', &3)));
+
        let values = map.iter().collect::<Vec<(&char, &Max<u8>)>>();
+
        assert!(values.contains(&(&'a', &Max::from(1))));
+
        assert!(values.contains(&(&'b', &Max::from(2))));
+
        assert!(values.contains(&(&'c', &Max::from(3))));
        assert_eq!(values.len(), 3);
    }

@@ -149,7 +155,7 @@ mod tests {
    fn test_insert_remove() {
        let mut map = LWWMap::default();

-
        map.insert('a', "alice", 1);
+
        map.insert('a', Max::from("alice"), 1);
        assert!(map.contains_key('a'));

        map.remove('a', 0);
@@ -168,16 +174,16 @@ mod tests {
    fn test_remove_insert() {
        let mut map = LWWMap::default();

-
        map.insert('a', "alice", 1);
-
        assert_eq!(map.get('a'), Some(&"alice"));
+
        map.insert('a', Max::from("alice"), 1);
+
        assert_eq!(map.get('a'), Some(&Max::from("alice")));

        map.remove('a', 2);
        assert!(!map.contains_key('a'));

-
        map.insert('a', "alice", 1);
+
        map.insert('a', Max::from("alice"), 1);
        assert!(!map.contains_key('a'));

-
        map.insert('a', "amy", 2);
-
        assert_eq!(map.get('a'), Some(&"amy"));
+
        map.insert('a', Max::from("amy"), 2);
+
        assert_eq!(map.get('a'), Some(&Max::from("amy")));
    }
}
modified radicle-crdt/src/lwwreg.rs
@@ -3,55 +3,65 @@ use crate::Semilattice;

/// Last-Write-Wins Register.
///
-
/// In case of conflict, biased towards larger values.
+
/// In case of conflict, uses the [`Semilattice`] instance of `T` to merge.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LWWReg<T, C> {
-
    /// Nb. the order of the tuple types ensures we bias towards the clock before the value.
-
    inner: Max<(C, T)>,
+
    clock: Max<C>,
+
    value: T,
}

-
impl<T: PartialOrd, C: PartialOrd> LWWReg<T, C> {
+
impl<T: PartialOrd + Semilattice, C: PartialOrd> LWWReg<T, C> {
    pub fn new(value: T, clock: C) -> Self {
        Self {
-
            inner: Max::from((clock, value)),
+
            clock: Max::from(clock),
+
            value,
        }
    }

-
    pub fn set(&mut self, value: T, clock: C) {
-
        self.inner.merge(Max::from((clock, value)));
+
    pub fn set(&mut self, value: impl Into<T>, clock: C) {
+
        let clock = Max::from(clock);
+
        let value = value.into();
+

+
        if clock == self.clock {
+
            self.value.merge(value);
+
        } else if clock > self.clock {
+
            self.clock.merge(clock);
+
            self.value = value;
+
        }
    }

    pub fn get(&self) -> &T {
-
        &self.inner.get().1
+
        &self.value
    }

-
    pub fn clock(&self) -> &C {
-
        &self.inner.get().0
+
    pub fn clock(&self) -> &Max<C> {
+
        &self.clock
    }

    pub fn into_inner(self) -> (T, C) {
-
        let (t, c) = self.inner.into_inner();
-
        (c, t)
+
        (self.value, self.clock.into_inner())
    }
}

impl<T, C> Semilattice for LWWReg<T, C>
where
-
    T: PartialOrd + Default,
+
    T: PartialOrd + Default + Semilattice,
    C: PartialOrd + Default,
{
    fn merge(&mut self, other: Self) {
-
        self.inner.merge(other.inner);
+
        self.set(other.value, other.clock.into_inner());
    }
}

#[cfg(test)]
mod tests {
-
    use super::*;
    use quickcheck_macros::quickcheck;

+
    use super::*;
+
    use crate::Min;
+

    #[quickcheck]
-
    fn prop_semilattice(a: (u8, u16), b: (u8, u16), c: (u8, u16)) {
+
    fn prop_semilattice(a: (Max<u8>, u16), b: (Max<u8>, u16), c: (Max<u8>, u16)) {
        let a = LWWReg::new(a.0, a.1);
        let b = LWWReg::new(b.0, b.1);
        let c = LWWReg::new(c.0, c.1);
@@ -60,30 +70,43 @@ mod tests {
    }

    #[test]
+
    fn test_merge() {
+
        let a = LWWReg::new(Max::from(0), 0);
+
        let b = LWWReg::new(Max::from(1), 0);
+

+
        assert_eq!(a.join(b).get(), &Max::from(1));
+

+
        let a = LWWReg::new(Min::from(0), 0);
+
        let b = LWWReg::new(Min::from(1), 0);
+

+
        assert_eq!(a.join(b).get(), &Min::from(0));
+
    }
+

+
    #[test]
    fn test_set_get() {
-
        let mut reg = LWWReg::new(42, 1);
-
        assert_eq!(*reg.get(), 42);
+
        let mut reg = LWWReg::new(Max::from(42), 1);
+
        assert_eq!(*reg.get(), Max::from(42));

        reg.set(84, 0);
-
        assert_eq!(*reg.get(), 42);
+
        assert_eq!(*reg.get(), Max::from(42));

        reg.set(84, 2);
-
        assert_eq!(*reg.get(), 84);
+
        assert_eq!(*reg.get(), Max::from(84));

        // Smaller value, same clock: smaller value loses.
        reg.set(42, 2);
-
        assert_eq!(*reg.get(), 84);
+
        assert_eq!(*reg.get(), Max::from(84));

        // Bigger value, same clock: bigger value wins.
        reg.set(168, 2);
-
        assert_eq!(*reg.get(), 168);
+
        assert_eq!(*reg.get(), Max::from(168));

        // Smaller value, newer clock: smaller value wins.
        reg.set(42, 3);
-
        assert_eq!(*reg.get(), 42);
+
        assert_eq!(*reg.get(), Max::from(42));

        // Same value, newer clock: newer clock is set.
        reg.set(42, 4);
-
        assert_eq!(*reg.clock(), 4);
+
        assert_eq!(*reg.clock(), Max::from(4));
    }
}
modified radicle-crdt/src/ord.rs
@@ -99,3 +99,28 @@ where
        other.0.partial_cmp(&self.0)
    }
}
+

+
impl<T: PartialOrd> Semilattice for Min<T> {
+
    fn merge(&mut self, other: Self) {
+
        if other.0 < self.0 {
+
            self.0 = other.0;
+
        }
+
    }
+
}
+

+
#[cfg(any(test, feature = "test"))]
+
mod arbitrary {
+
    use super::*;
+

+
    impl<T: quickcheck::Arbitrary> quickcheck::Arbitrary for Max<T> {
+
        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
            Self::from(T::arbitrary(g))
+
        }
+
    }
+

+
    impl<T: quickcheck::Arbitrary> quickcheck::Arbitrary for Min<T> {
+
        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
            Self::from(T::arbitrary(g))
+
        }
+
    }
+
}
modified radicle-crdt/src/redactable.rs
@@ -2,15 +2,19 @@ use crate::Semilattice;

/// An object that can be either present or removed.
///
+
/// The "redacted" state is the top-most element and takes precedence
+
/// over other states.
+
///
+
/// There is no `Default` instance, as it wouldn't obbey semilattice laws.
+
///
/// Nb. The merge rules are such that if two redactables with different
/// values present are merged; the result is redacted. This is the preserve
/// the semilattice laws.
-
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Redactable<T> {
    /// When the object is present.
    Present(T),
    /// When the object has been removed.
-
    #[default]
    Redacted,
}

modified radicle/src/cob/thread.rs
@@ -95,6 +95,7 @@ impl store::FromHistory for Thread {
        }))
    }
}
+

impl Semilattice for Thread {
    fn merge(&mut self, other: Self) {
        self.comments.merge(other.comments);