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"
[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" }

View File

@ -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))
);
}
}

View File

@ -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")));
}
}

View File

@ -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);
@ -59,31 +69,44 @@ mod tests {
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]
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));
}
}

View File

@ -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))
}
}
}

View File

@ -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,
}

View File

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