crdt: Require `LWWReg` value to be `Semilattice`

* Also derive instances of `Semilattice` for built-in types.

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-11-25 14:56:48 +01:00
parent cc64c1d80b
commit cc33d04a7b
No known key found for this signature in database
7 changed files with 187 additions and 51 deletions

View File

@ -4,12 +4,13 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[features] [features]
test = ["fastrand"] test = ["fastrand", "quickcheck"]
[dependencies] [dependencies]
fastrand = { version = "1.8.0", optional = true } fastrand = { version = "1.8.0", optional = true }
num-traits = { version = "0.2.15", default-features = false, features = ["std"] } num-traits = { version = "0.2.15", default-features = false, features = ["std"] }
olpc-cjson = { version = "0.1.1" } olpc-cjson = { version = "0.1.1" }
quickcheck = { version = "1", optional = true }
serde = { version = "1" } serde = { version = "1" }
serde_json = { version = "1" } serde_json = { version = "1" }

View File

@ -1,4 +1,5 @@
#![allow(clippy::collapsible_if)] #![allow(clippy::collapsible_if)]
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::collapsible_else_if)] #![allow(clippy::collapsible_else_if)]
#![allow(clippy::type_complexity)] #![allow(clippy::type_complexity)]
pub mod change; pub mod change;
@ -15,6 +16,12 @@ pub mod test;
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
pub use change::*; 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 pub fn fold<S>(i: impl IntoIterator<Item = S>) -> S
where where
S: Semilattice + Default, S: Semilattice + Default,
{ {
i.into_iter().fold(S::default(), S::join) 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))
);
}
}

View File

@ -10,7 +10,7 @@ pub struct LWWMap<K, V, C> {
inner: BTreeMap<K, LWWReg<Option<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 { pub fn singleton(key: K, value: V, clock: C) -> Self {
Self { Self {
inner: BTreeMap::from_iter([(key, LWWReg::new(Some(value), clock))]), 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 { fn from_iter<I: IntoIterator<Item = (K, V, C)>>(iter: I) -> Self {
let mut map = LWWMap::default(); let mut map = LWWMap::default();
for (k, v, c) in iter.into_iter() { 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) { fn extend<I: IntoIterator<Item = (K, V, C)>>(&mut self, iter: I) {
for (k, v, c) in iter.into_iter() { for (k, v, c) in iter.into_iter() {
self.insert(k, v, c); 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> impl<K, V, C> Semilattice for LWWMap<K, V, C>
where where
K: Ord, K: Ord,
V: PartialOrd + Eq, V: Semilattice + PartialOrd + Eq,
C: Ord + Copy + Default, C: Ord + Copy + Default,
{ {
fn merge(&mut self, other: Self) { fn merge(&mut self, other: Self) {
@ -106,15 +110,17 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use quickcheck_macros::quickcheck; use quickcheck_macros::quickcheck;
use super::*;
use crate::ord::Max;
#[quickcheck] #[quickcheck]
fn prop_semilattice( fn prop_semilattice(
a: Vec<(u8, u8, u16)>, a: Vec<(u8, Max<u8>, u16)>,
b: Vec<(u8, u8, u16)>, b: Vec<(u8, Max<u8>, u16)>,
c: Vec<(u8, u8, u16)>, c: Vec<(u8, Max<u8>, u16)>,
mix: Vec<(u8, u8, u16)>, mix: Vec<(u8, Max<u8>, u16)>,
) { ) {
let mut a = LWWMap::from_iter(a); let mut a = LWWMap::from_iter(a);
let mut b = LWWMap::from_iter(b); let mut b = LWWMap::from_iter(b);
@ -130,18 +136,18 @@ mod tests {
fn test_insert() { fn test_insert() {
let mut map = LWWMap::default(); let mut map = LWWMap::default();
map.insert('a', 1, 0); map.insert('a', Max::from(1), 0);
map.insert('b', 2, 0); map.insert('b', Max::from(2), 0);
map.insert('c', 3, 0); map.insert('c', Max::from(3), 0);
assert_eq!(map.get('a'), Some(&1)); assert_eq!(map.get('a'), Some(&Max::from(1)));
assert_eq!(map.get('b'), Some(&2)); assert_eq!(map.get('b'), Some(&Max::from(2)));
assert_eq!(map.get('?'), None); assert_eq!(map.get('?'), None);
let values = map.iter().collect::<Vec<(&char, &u8)>>(); let values = map.iter().collect::<Vec<(&char, &Max<u8>)>>();
assert!(values.contains(&(&'a', &1))); assert!(values.contains(&(&'a', &Max::from(1))));
assert!(values.contains(&(&'b', &2))); assert!(values.contains(&(&'b', &Max::from(2))));
assert!(values.contains(&(&'c', &3))); assert!(values.contains(&(&'c', &Max::from(3))));
assert_eq!(values.len(), 3); assert_eq!(values.len(), 3);
} }
@ -149,7 +155,7 @@ mod tests {
fn test_insert_remove() { fn test_insert_remove() {
let mut map = LWWMap::default(); let mut map = LWWMap::default();
map.insert('a', "alice", 1); map.insert('a', Max::from("alice"), 1);
assert!(map.contains_key('a')); assert!(map.contains_key('a'));
map.remove('a', 0); map.remove('a', 0);
@ -168,16 +174,16 @@ mod tests {
fn test_remove_insert() { fn test_remove_insert() {
let mut map = LWWMap::default(); let mut map = LWWMap::default();
map.insert('a', "alice", 1); map.insert('a', Max::from("alice"), 1);
assert_eq!(map.get('a'), Some(&"alice")); assert_eq!(map.get('a'), Some(&Max::from("alice")));
map.remove('a', 2); map.remove('a', 2);
assert!(!map.contains_key('a')); assert!(!map.contains_key('a'));
map.insert('a', "alice", 1); map.insert('a', Max::from("alice"), 1);
assert!(!map.contains_key('a')); assert!(!map.contains_key('a'));
map.insert('a', "amy", 2); map.insert('a', Max::from("amy"), 2);
assert_eq!(map.get('a'), Some(&"amy")); assert_eq!(map.get('a'), Some(&Max::from("amy")));
} }
} }

View File

@ -3,55 +3,65 @@ use crate::Semilattice;
/// Last-Write-Wins Register. /// 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)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct LWWReg<T, C> { pub struct LWWReg<T, C> {
/// Nb. the order of the tuple types ensures we bias towards the clock before the value. clock: Max<C>,
inner: Max<(C, T)>, 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 { pub fn new(value: T, clock: C) -> Self {
Self { Self {
inner: Max::from((clock, value)), clock: Max::from(clock),
value,
} }
} }
pub fn set(&mut self, value: T, clock: C) { pub fn set(&mut self, value: impl Into<T>, clock: C) {
self.inner.merge(Max::from((clock, value))); 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 { pub fn get(&self) -> &T {
&self.inner.get().1 &self.value
} }
pub fn clock(&self) -> &C { pub fn clock(&self) -> &Max<C> {
&self.inner.get().0 &self.clock
} }
pub fn into_inner(self) -> (T, C) { pub fn into_inner(self) -> (T, C) {
let (t, c) = self.inner.into_inner(); (self.value, self.clock.into_inner())
(c, t)
} }
} }
impl<T, C> Semilattice for LWWReg<T, C> impl<T, C> Semilattice for LWWReg<T, C>
where where
T: PartialOrd + Default, T: PartialOrd + Default + Semilattice,
C: PartialOrd + Default, C: PartialOrd + Default,
{ {
fn merge(&mut self, other: Self) { fn merge(&mut self, other: Self) {
self.inner.merge(other.inner); self.set(other.value, other.clock.into_inner());
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use quickcheck_macros::quickcheck; use quickcheck_macros::quickcheck;
use super::*;
use crate::Min;
#[quickcheck] #[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 a = LWWReg::new(a.0, a.1);
let b = LWWReg::new(b.0, b.1); let b = LWWReg::new(b.0, b.1);
let c = LWWReg::new(c.0, c.1); let c = LWWReg::new(c.0, c.1);
@ -59,31 +69,44 @@ mod tests {
crate::test::assert_laws(&a, &b, &c); crate::test::assert_laws(&a, &b, &c);
} }
#[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] #[test]
fn test_set_get() { fn test_set_get() {
let mut reg = LWWReg::new(42, 1); let mut reg = LWWReg::new(Max::from(42), 1);
assert_eq!(*reg.get(), 42); assert_eq!(*reg.get(), Max::from(42));
reg.set(84, 0); reg.set(84, 0);
assert_eq!(*reg.get(), 42); assert_eq!(*reg.get(), Max::from(42));
reg.set(84, 2); reg.set(84, 2);
assert_eq!(*reg.get(), 84); assert_eq!(*reg.get(), Max::from(84));
// Smaller value, same clock: smaller value loses. // Smaller value, same clock: smaller value loses.
reg.set(42, 2); reg.set(42, 2);
assert_eq!(*reg.get(), 84); assert_eq!(*reg.get(), Max::from(84));
// Bigger value, same clock: bigger value wins. // Bigger value, same clock: bigger value wins.
reg.set(168, 2); reg.set(168, 2);
assert_eq!(*reg.get(), 168); assert_eq!(*reg.get(), Max::from(168));
// Smaller value, newer clock: smaller value wins. // Smaller value, newer clock: smaller value wins.
reg.set(42, 3); reg.set(42, 3);
assert_eq!(*reg.get(), 42); assert_eq!(*reg.get(), Max::from(42));
// Same value, newer clock: newer clock is set. // Same value, newer clock: newer clock is set.
reg.set(42, 4); reg.set(42, 4);
assert_eq!(*reg.clock(), 4); assert_eq!(*reg.clock(), Max::from(4));
} }
} }

View File

@ -99,3 +99,28 @@ where
other.0.partial_cmp(&self.0) 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))
}
}
}

View File

@ -2,15 +2,19 @@ use crate::Semilattice;
/// An object that can be either present or removed. /// 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 /// Nb. The merge rules are such that if two redactables with different
/// values present are merged; the result is redacted. This is the preserve /// values present are merged; the result is redacted. This is the preserve
/// the semilattice laws. /// the semilattice laws.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Redactable<T> { pub enum Redactable<T> {
/// When the object is present. /// When the object is present.
Present(T), Present(T),
/// When the object has been removed. /// When the object has been removed.
#[default]
Redacted, Redacted,
} }

View File

@ -95,6 +95,7 @@ impl store::FromHistory for Thread {
})) }))
} }
} }
impl Semilattice for Thread { impl Semilattice for Thread {
fn merge(&mut self, other: Self) { fn merge(&mut self, other: Self) {
self.comments.merge(other.comments); self.comments.merge(other.comments);