From b8b1dea38de84d7647921cd1859b7d58303e9b86 Mon Sep 17 00:00:00 2001 From: Niklas Hoefflin <122729995+itakurah@users.noreply.github.com> Date: Fri, 8 Dec 2023 19:57:07 +0100 Subject: [PATCH] Add LWW Element Set (Last Write Wins Element Set) (#4979) --- DIRECTORY.md | 2 + .../datastructures/crdt/LWWElementSet.java | 138 ++++++++++++++++++ .../crdt/LWWElementSetTest.java | 107 ++++++++++++++ 3 files changed, 247 insertions(+) create mode 100644 src/main/java/com/thealgorithms/datastructures/crdt/LWWElementSet.java create mode 100644 src/test/java/com/thealgorithms/datastructures/crdt/LWWElementSetTest.java diff --git a/DIRECTORY.md b/DIRECTORY.md index 703642a0..f033416d 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -85,6 +85,7 @@ * crdt * [GCounter](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/crdt/GCounter.java) * [GSet](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/crdt/GSet.java) + * [LWWElementSet](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/crdt/LWWElementSet.java) * [PNCounter](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/crdt/PNCounter.java) * [TwoPSet](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/crdt/TwoPSet.java) * disjointsetunion @@ -621,6 +622,7 @@ * crdt * [GCounterTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/crdt/GCounterTest.java) * [GSetTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/crdt/GSetTest.java) + * [LWWElementSetTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/crdt/LWWElementSetTest.java) * [PNCounterTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/crdt/PNCounterTest.java) * [TwoPSetTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/crdt/TwoPSetTest.java) * disjointsetunion diff --git a/src/main/java/com/thealgorithms/datastructures/crdt/LWWElementSet.java b/src/main/java/com/thealgorithms/datastructures/crdt/LWWElementSet.java new file mode 100644 index 00000000..722c916a --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/crdt/LWWElementSet.java @@ -0,0 +1,138 @@ +package com.thealgorithms.datastructures.crdt; + +import java.util.HashMap; +import java.util.Map; + +/** + * Last-Write-Wins Element Set (LWWElementSet) is a state-based CRDT (Conflict-free Replicated Data Type) + * designed for managing sets in a distributed and concurrent environment. It supports the addition and removal + * of elements, using timestamps to determine the order of operations. The set is split into two subsets: + * the add set for elements to be added and the remove set for elements to be removed. + * + * @author itakurah (Niklas Hoefflin) (https://github.com/itakurah) + * @see Conflict-free_replicated_data_type + * @see itakurah (Niklas Hoefflin) + */ + +class Element { + String key; + int timestamp; + Bias bias; + + /** + * Constructs a new Element with the specified key, timestamp and bias. + * + * @param key The key of the element. + * @param timestamp The timestamp associated with the element. + * @param bias The bias of the element (ADDS or REMOVALS). + */ + public Element(String key, int timestamp, Bias bias) { + this.key = key; + this.timestamp = timestamp; + this.bias = bias; + } +} + +enum Bias { + /** + * ADDS bias for the add set. + * REMOVALS bias for the remove set. + */ + ADDS, + REMOVALS +} + +class LWWElementSet { + private final Map addSet; + private final Map removeSet; + + /** + * Constructs an empty LWWElementSet. + */ + public LWWElementSet() { + this.addSet = new HashMap<>(); + this.removeSet = new HashMap<>(); + } + + /** + * Adds an element to the addSet. + * + * @param e The element to be added. + */ + public void add(Element e) { + addSet.put(e.key, e); + } + + /** + * Removes an element from the removeSet. + * + * @param e The element to be removed. + */ + public void remove(Element e) { + if (lookup(e)) { + removeSet.put(e.key, e); + } + } + + /** + * Checks if an element is in the LWWElementSet by comparing timestamps in the addSet and removeSet. + * + * @param e The element to be checked. + * @return True if the element is present, false otherwise. + */ + public boolean lookup(Element e) { + Element inAddSet = addSet.get(e.key); + Element inRemoveSet = removeSet.get(e.key); + + return (inAddSet != null && (inRemoveSet == null || inAddSet.timestamp > inRemoveSet.timestamp)); + } + + /** + * Compares the LWWElementSet with another LWWElementSet to check if addSet and removeSet are a subset. + * + * @param other The LWWElementSet to compare. + * @return True if the set is subset, false otherwise. + */ + public boolean compare(LWWElementSet other) { + return other.addSet.keySet().containsAll(addSet.keySet()) && other.removeSet.keySet().containsAll(removeSet.keySet()); + } + + /** + * Merges another LWWElementSet into this set by resolving conflicts based on timestamps. + * + * @param other The LWWElementSet to merge. + */ + public void merge(LWWElementSet other) { + for (Element e : other.addSet.values()) { + if (!addSet.containsKey(e.key) || compareTimestamps(addSet.get(e.key), e)) { + addSet.put(e.key, e); + } + } + + for (Element e : other.removeSet.values()) { + if (!removeSet.containsKey(e.key) || compareTimestamps(removeSet.get(e.key), e)) { + removeSet.put(e.key, e); + } + } + } + + /** + * Compares timestamps of two elements based on their bias (ADDS or REMOVALS). + * + * @param e The first element. + * @param other The second element. + * @return True if the first element's timestamp is greater or the bias is ADDS and timestamps are equal. + */ + public boolean compareTimestamps(Element e, Element other) { + if (!e.bias.equals(other.bias)) { + throw new IllegalArgumentException("Invalid bias value"); + } + Bias bias = e.bias; + int timestampComparison = Integer.compare(e.timestamp, other.timestamp); + + if (timestampComparison == 0) { + return !bias.equals(Bias.ADDS); + } + return timestampComparison < 0; + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/crdt/LWWElementSetTest.java b/src/test/java/com/thealgorithms/datastructures/crdt/LWWElementSetTest.java new file mode 100644 index 00000000..6fb227bd --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/crdt/LWWElementSetTest.java @@ -0,0 +1,107 @@ +package com.thealgorithms.datastructures.crdt; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class LWWElementSetTest { + + private LWWElementSet set; + private final Bias bias = Bias.ADDS; + + @BeforeEach + void setUp() { + set = new LWWElementSet(); + } + + @Test + void testAdd() { + Element element = new Element("key1", 1, bias); + set.add(element); + + assertTrue(set.lookup(element)); + } + + @Test + void testRemove() { + Element element = new Element("key1", 1, bias); + set.add(element); + set.remove(element); + + assertFalse(set.lookup(element)); + } + + @Test + void testRemoveNonexistentElement() { + Element element = new Element("key1", 1, bias); + set.remove(element); + + assertFalse(set.lookup(element)); + } + + @Test + void testLookupNonexistentElement() { + Element element = new Element("key1", 1, bias); + + assertFalse(set.lookup(element)); + } + + @Test + void testCompareEqualSets() { + LWWElementSet otherSet = new LWWElementSet(); + + Element element = new Element("key1", 1, bias); + set.add(element); + otherSet.add(element); + + assertTrue(set.compare(otherSet)); + + otherSet.add(new Element("key2", 2, bias)); + assertTrue(set.compare(otherSet)); + } + + @Test + void testCompareDifferentSets() { + LWWElementSet otherSet = new LWWElementSet(); + + Element element1 = new Element("key1", 1, bias); + Element element2 = new Element("key2", 2, bias); + + set.add(element1); + otherSet.add(element2); + + assertFalse(set.compare(otherSet)); + } + + @Test + void testMerge() { + LWWElementSet otherSet = new LWWElementSet(); + + Element element1 = new Element("key1", 1, bias); + Element element2 = new Element("key2", 2, bias); + + set.add(element1); + otherSet.add(element2); + + set.merge(otherSet); + + assertTrue(set.lookup(element1)); + assertTrue(set.lookup(element2)); + } + + @Test + void testCompareTimestampsEqualTimestamps() { + LWWElementSet lwwElementSet = new LWWElementSet(); + + Element e1 = new Element("key1", 10, Bias.REMOVALS); + Element e2 = new Element("key1", 10, Bias.REMOVALS); + + assertTrue(lwwElementSet.compareTimestamps(e1, e2)); + + e1 = new Element("key1", 10, Bias.ADDS); + e2 = new Element("key1", 10, Bias.ADDS); + + assertFalse(lwwElementSet.compareTimestamps(e1, e2)); + } +}