Reputation: 6413
Let's say I've got a CRDT which I can add and remove elements from at will. It's a state-based CRDT, so I can merge
two of these sets together.
Assuming the set implementation is correct, I've seen folks claim that you can implement a map implementation on top of it by doing something like this:
pub struct LWWMap<K, V>
where
K: Ord,
V: Merge,
{
keys: LWWSet<K>,
values: BTreeMap<K, V>,
}
Then defining merge
like so:
impl<K, V> Merge for LWWMap<K, V>
where
K: Ord + Clone,
V: Merge,
{
fn merge_mut(&mut self, mut other: Self) {
self.keys.merge_mut(other.keys);
// retain and merge keys
let mut new_values = BTreeMap::new();
for key in self.keys.iter().cloned().collect::<Vec<K>>() {
match (self.values.remove(&key), other.values.remove(&key)) {
(Some(mut self_value), Some(other_value)) => {
self_value.merge_mut(other_value);
new_values.insert(key, self_value);
}
(Some(self_value), None) => {
new_values.insert(key, self_value);
}
(None, Some(other_value)) => {
new_values.insert(key, other_value);
}
(None, None) => {}
}
}
self.values = new_values
}
}
Anyway, this is idempotent and commutative, but it doesn't appear to be associative. For example, imagine I have another CRDT Max
whose merge function is just self.max(other)
. For the following three values of LWWMap<bool, Max<bool>>
, commutativity does not hold (i.e. a.merge(b.merge(c)) != a.merge(b).merge(c)
)
a = LWWMap {
keys: LWWSet {
adds: {
true: 1970-01-01T00:00:00Z_0_00000000-0000-0000-0000-000000000000,
},
removes: {
true: 1970-01-01T00:00:01Z_1_00000000-0000-0000-0000-000000000002,
},
},
values: {},
}
b = LWWMap {
keys: LWWSet {
adds: {
true: 1970-01-01T00:00:01Z_0_00000000-0000-0000-0000-000000000000,
},
removes: {},
},
values: {
true: Max(
true,
),
},
}
c = LWWMap {
keys: LWWSet {
adds: {
true: 1970-01-01T00:00:01Z_2_00000000-0000-0000-0000-000000000000,
},
removes: {},
},
values: {
true: Max(
false,
),
},
}
Unfortunately, this makes sense to me!
a.merge(b.merge(c))
, b.merge(c)
ends up containing {true: true}
with a higher LWW timestamp than the removal in a
, so we end up with {true: true}
.a.merge(b).merge(c)
, a.merge(b)
ends up empty, but then merging with {true: false}
results in {true: false}
.I can fix this by keeping all the keys and values around forever, but the descriptions I can find of this algorithm say that you should be able to remove unused values on merge without consequence.
Am I missing something here? What else is necessary for commutativity to hold?
Upvotes: 0
Views: 7