Add `LWWMap` and implement `LWWSet` with it
Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
parent
35f061746c
commit
b11826b113
|
|
@ -1,6 +1,7 @@
|
||||||
#![allow(clippy::collapsible_if)]
|
#![allow(clippy::collapsible_if)]
|
||||||
#![allow(clippy::collapsible_else_if)]
|
#![allow(clippy::collapsible_else_if)]
|
||||||
#![allow(clippy::type_complexity)]
|
#![allow(clippy::type_complexity)]
|
||||||
|
pub mod lwwmap;
|
||||||
pub mod lwwreg;
|
pub mod lwwreg;
|
||||||
pub mod lwwset;
|
pub mod lwwset;
|
||||||
pub mod ord;
|
pub mod ord;
|
||||||
|
|
|
||||||
|
|
@ -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<K, V, C> {
|
||||||
|
inner: BTreeMap<K, LWWReg<Option<V>, C>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K: Ord, V: 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))]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Item = (&K, &V)> {
|
||||||
|
self.inner
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(k, v)| v.get().as_ref().map(|v| (k, v)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, V, C> Default for LWWMap<K, V, C> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: BTreeMap::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K: Ord, V: 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() {
|
||||||
|
map.insert(k, v, c);
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K: Ord, V: 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, V, C> Semilattice for LWWMap<K, V, C>
|
||||||
|
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::<Vec<(&char, &u8)>>();
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
use crate::Semilattice;
|
use crate::Semilattice;
|
||||||
|
|
||||||
/// Last-Write-Wins Register.
|
/// Last-Write-Wins Register.
|
||||||
|
///
|
||||||
|
/// In case of conflict, biased towards larger values.
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
pub struct LWWReg<T, C> {
|
pub struct LWWReg<T, C> {
|
||||||
value: T,
|
value: T,
|
||||||
|
|
@ -27,8 +29,12 @@ impl<T: PartialOrd, C: PartialOrd> LWWReg<T, C> {
|
||||||
&self.clock
|
&self.clock
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_inner(self) -> T {
|
pub fn into_inner(self) -> (T, C) {
|
||||||
self.value
|
(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,
|
C: PartialOrd + Default,
|
||||||
{
|
{
|
||||||
fn join(mut self, other: Self) -> Self {
|
fn join(mut self, other: Self) -> Self {
|
||||||
self.set(other.value, other.clock);
|
self.merge(other);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,39 @@
|
||||||
use std::collections::BTreeMap;
|
use crate::{lwwmap::LWWMap, Semilattice};
|
||||||
|
|
||||||
use crate::Semilattice;
|
|
||||||
|
|
||||||
/// Last-Write-Wins Set.
|
/// Last-Write-Wins Set.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct LWWSet<T, C> {
|
pub struct LWWSet<T, C> {
|
||||||
added: BTreeMap<T, C>,
|
inner: LWWMap<T, (), C>,
|
||||||
removed: BTreeMap<T, C>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Ord, C: Ord + Copy> LWWSet<T, C> {
|
impl<T: Ord, C: Ord + Copy> LWWSet<T, C> {
|
||||||
pub fn singleton(value: T, clock: C) -> Self {
|
pub fn singleton(value: T, clock: C) -> Self {
|
||||||
Self {
|
Self {
|
||||||
added: BTreeMap::from_iter([(value, clock)]),
|
inner: LWWMap::from_iter([(value, (), clock)]),
|
||||||
removed: BTreeMap::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, value: T, clock: C) {
|
pub fn insert(&mut self, value: T, clock: C) {
|
||||||
self.added
|
self.inner.insert(value, (), clock);
|
||||||
.entry(value)
|
|
||||||
.and_modify(|t| *t = C::max(*t, clock))
|
|
||||||
.or_insert(clock);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(&mut self, value: T, clock: C) {
|
pub fn remove(&mut self, value: T, clock: C) {
|
||||||
// TODO: Should we remove from 'added' set if timestamp is newer?
|
self.inner.remove(value, clock);
|
||||||
self.removed
|
|
||||||
.entry(value)
|
|
||||||
.and_modify(|t| *t = C::max(*t, clock))
|
|
||||||
.or_insert(clock);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn contains(&self, value: T) -> bool {
|
pub fn contains(&self, value: T) -> bool {
|
||||||
let Some(added) = self.added.get(&value) else {
|
self.inner.contains_key(value)
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||||
self.added.iter().filter_map(|(value, added)| {
|
self.inner.iter().map(|(k, _)| k)
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, C> Default for LWWSet<T, C> {
|
impl<T, C> Default for LWWSet<T, C> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
added: BTreeMap::default(),
|
inner: LWWMap::default(),
|
||||||
removed: BTreeMap::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -92,11 +59,12 @@ impl<T: Ord, C: Ord + Copy> Extend<(T, C)> for LWWSet<T, C> {
|
||||||
impl<T, C> Semilattice for LWWSet<T, C>
|
impl<T, C> Semilattice for LWWSet<T, C>
|
||||||
where
|
where
|
||||||
T: Ord,
|
T: Ord,
|
||||||
C: Ord + Copy,
|
C: Ord + Copy + Default,
|
||||||
{
|
{
|
||||||
fn join(mut self, other: Self) -> Self {
|
fn join(self, other: Self) -> Self {
|
||||||
self.extend(other.added.into_iter());
|
Self {
|
||||||
self
|
inner: self.inner.join(other.inner),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue