From b11826b11396efa0802a0055f5a4d3128fce5da7 Mon Sep 17 00:00:00 2001 From: Alexis Sellier Date: Tue, 22 Nov 2022 14:40:01 +0100 Subject: [PATCH] Add `LWWMap` and implement `LWWSet` with it Signed-off-by: Alexis Sellier --- radicle-crdt/src/lib.rs | 1 + radicle-crdt/src/lwwmap.rs | 184 +++++++++++++++++++++++++++++++++++++ radicle-crdt/src/lwwreg.rs | 12 ++- radicle-crdt/src/lwwset.rs | 58 +++--------- 4 files changed, 207 insertions(+), 48 deletions(-) create mode 100644 radicle-crdt/src/lwwmap.rs diff --git a/radicle-crdt/src/lib.rs b/radicle-crdt/src/lib.rs index ae286610..871798e5 100644 --- a/radicle-crdt/src/lib.rs +++ b/radicle-crdt/src/lib.rs @@ -1,6 +1,7 @@ #![allow(clippy::collapsible_if)] #![allow(clippy::collapsible_else_if)] #![allow(clippy::type_complexity)] +pub mod lwwmap; pub mod lwwreg; pub mod lwwset; pub mod ord; diff --git a/radicle-crdt/src/lwwmap.rs b/radicle-crdt/src/lwwmap.rs new file mode 100644 index 00000000..c42aa5ce --- /dev/null +++ b/radicle-crdt/src/lwwmap.rs @@ -0,0 +1,184 @@ +use std::collections::btree_map::Entry; +use std::collections::BTreeMap; + +use crate::lwwreg::LWWReg; +use crate::Semilattice; + +/// Last-Write-Wins Map. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LWWMap { + inner: BTreeMap, C>>, +} + +impl LWWMap { + pub fn singleton(key: K, value: V, clock: C) -> Self { + Self { + inner: BTreeMap::from_iter([(key, LWWReg::new(Some(value), clock))]), + } + } + + pub fn get(&self, key: K) -> Option<&V> { + let Some(value) = self.inner.get(&key) else { + // If the element was never added, return nothing. + return None; + }; + value.get().as_ref() + } + + pub fn insert(&mut self, key: K, value: V, clock: C) { + match self.inner.entry(key) { + Entry::Occupied(mut e) => { + e.get_mut().set(Some(value), clock); + } + Entry::Vacant(e) => { + e.insert(LWWReg::new(Some(value), clock)); + } + } + } + + pub fn remove(&mut self, key: K, clock: C) { + self.inner + .entry(key) + .and_modify(|reg| reg.set(None, clock)) + .or_insert_with(|| LWWReg::new(None, clock)); + } + + pub fn contains_key(&self, key: K) -> bool { + let Some(value) = self.inner.get(&key) else { + // If the element was never added, return false. + return false; + }; + value.get().is_some() + } + + pub fn iter(&self) -> impl Iterator { + self.inner + .iter() + .filter_map(|(k, v)| v.get().as_ref().map(|v| (k, v))) + } +} + +impl Default for LWWMap { + fn default() -> Self { + Self { + inner: BTreeMap::default(), + } + } +} + +impl FromIterator<(K, V, C)> for LWWMap { + fn from_iter>(iter: I) -> Self { + let mut map = LWWMap::default(); + for (k, v, c) in iter.into_iter() { + map.insert(k, v, c); + } + map + } +} + +impl Extend<(K, V, C)> for LWWMap { + fn extend>(&mut self, iter: I) { + for (k, v, c) in iter.into_iter() { + self.insert(k, v, c); + } + } +} + +impl Semilattice for LWWMap +where + K: Ord, + V: PartialOrd + Eq, + C: Ord + Copy + Default, +{ + fn join(mut self, other: Self) -> Self { + for (k, v) in other.inner.into_iter() { + match self.inner.entry(k) { + Entry::Occupied(mut e) => { + e.get_mut().merge(v); + } + Entry::Vacant(e) => { + e.insert(v); + } + } + } + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use quickcheck_macros::quickcheck; + + #[quickcheck] + fn prop_semilattice( + a: Vec<(u8, u8, u16)>, + b: Vec<(u8, u8, u16)>, + c: Vec<(u8, u8, u16)>, + mix: Vec<(u8, u8, u16)>, + ) { + let mut a = LWWMap::from_iter(a); + let mut b = LWWMap::from_iter(b); + let c = LWWMap::from_iter(c); + + a.extend(mix.clone()); + b.extend(mix); + + crate::test::assert_laws(&a, &b, &c); + } + + #[test] + fn test_insert() { + let mut map = LWWMap::default(); + + map.insert('a', 1, 0); + map.insert('b', 2, 0); + map.insert('c', 3, 0); + + assert_eq!(map.get('a'), Some(&1)); + assert_eq!(map.get('b'), Some(&2)); + assert_eq!(map.get('?'), None); + + let values = map.iter().collect::>(); + assert!(values.contains(&(&'a', &1))); + assert!(values.contains(&(&'b', &2))); + assert!(values.contains(&(&'c', &3))); + assert_eq!(values.len(), 3); + } + + #[test] + fn test_insert_remove() { + let mut map = LWWMap::default(); + + map.insert('a', "alice", 1); + assert!(map.contains_key('a')); + + map.remove('a', 0); + assert!(map.contains_key('a')); + + map.remove('a', 1); + assert!(map.contains_key('a')); // Add takes precedence over remove. + assert!(map.iter().any(|(c, _)| *c == 'a')); + + map.remove('a', 2); + assert!(!map.contains_key('a')); + assert!(!map.iter().any(|(c, _)| *c == 'a')); + } + + #[test] + fn test_remove_insert() { + let mut map = LWWMap::default(); + + map.insert('a', "alice", 1); + assert_eq!(map.get('a'), Some(&"alice")); + + map.remove('a', 2); + assert!(!map.contains_key('a')); + + map.insert('a', "alice", 1); + assert!(!map.contains_key('a')); + + map.insert('a', "amy", 2); + assert_eq!(map.get('a'), Some(&"amy")); + } +} diff --git a/radicle-crdt/src/lwwreg.rs b/radicle-crdt/src/lwwreg.rs index fc7f5038..92edf698 100644 --- a/radicle-crdt/src/lwwreg.rs +++ b/radicle-crdt/src/lwwreg.rs @@ -1,6 +1,8 @@ use crate::Semilattice; /// Last-Write-Wins Register. +/// +/// In case of conflict, biased towards larger values. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct LWWReg { value: T, @@ -27,8 +29,12 @@ impl LWWReg { &self.clock } - pub fn into_inner(self) -> T { - self.value + pub fn into_inner(self) -> (T, C) { + (self.value, self.clock) + } + + pub fn merge(&mut self, other: Self) { + self.set(other.value, other.clock); } } @@ -38,7 +44,7 @@ where C: PartialOrd + Default, { fn join(mut self, other: Self) -> Self { - self.set(other.value, other.clock); + self.merge(other); self } } diff --git a/radicle-crdt/src/lwwset.rs b/radicle-crdt/src/lwwset.rs index bf0450c4..1a722ad3 100644 --- a/radicle-crdt/src/lwwset.rs +++ b/radicle-crdt/src/lwwset.rs @@ -1,72 +1,39 @@ -use std::collections::BTreeMap; - -use crate::Semilattice; +use crate::{lwwmap::LWWMap, Semilattice}; /// Last-Write-Wins Set. #[derive(Debug, Clone, PartialEq, Eq)] pub struct LWWSet { - added: BTreeMap, - removed: BTreeMap, + inner: LWWMap, } impl LWWSet { pub fn singleton(value: T, clock: C) -> Self { Self { - added: BTreeMap::from_iter([(value, clock)]), - removed: BTreeMap::default(), + inner: LWWMap::from_iter([(value, (), clock)]), } } pub fn insert(&mut self, value: T, clock: C) { - self.added - .entry(value) - .and_modify(|t| *t = C::max(*t, clock)) - .or_insert(clock); + self.inner.insert(value, (), clock); } pub fn remove(&mut self, value: T, clock: C) { - // TODO: Should we remove from 'added' set if timestamp is newer? - self.removed - .entry(value) - .and_modify(|t| *t = C::max(*t, clock)) - .or_insert(clock); + self.inner.remove(value, clock); } pub fn contains(&self, value: T) -> bool { - let Some(added) = self.added.get(&value) else { - // If the element was never added, return false. - return false; - }; - - if let Some(removed) = self.removed.get(&value) { - // If the element was added and also removed, whichever came last - // is the winner, or if they came at the same time, we bias towards - // it having been added last. - return added >= removed; - } - // If it was only added and never removed, return true. - true + self.inner.contains_key(value) } pub fn iter(&self) -> impl Iterator { - self.added.iter().filter_map(|(value, added)| { - if let Some(removed) = self.removed.get(value) { - // Note, in case the element was added and removed at the same time, - // we bias towards it being added, ie. this won't return `None`. - if removed > added { - return None; - } - } - Some(value) - }) + self.inner.iter().map(|(k, _)| k) } } impl Default for LWWSet { fn default() -> Self { Self { - added: BTreeMap::default(), - removed: BTreeMap::default(), + inner: LWWMap::default(), } } } @@ -92,11 +59,12 @@ impl Extend<(T, C)> for LWWSet { impl Semilattice for LWWSet where T: Ord, - C: Ord + Copy, + C: Ord + Copy + Default, { - fn join(mut self, other: Self) -> Self { - self.extend(other.added.into_iter()); - self + fn join(self, other: Self) -> Self { + Self { + inner: self.inner.join(other.inner), + } } }