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:
parent
cc64c1d80b
commit
cc33d04a7b
|
|
@ -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" }
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ impl store::FromHistory for Thread {
|
|||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Semilattice for Thread {
|
||||
fn merge(&mut self, other: Self) {
|
||||
self.comments.merge(other.comments);
|
||||
|
|
|
|||
Loading…
Reference in New Issue