Brian Hicks
Brian Hicks

Reputation: 6413

How can you make a CRDT Map from a CRDT set?

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!

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

Answers (0)

Related Questions