Skip to content

Introduce OR-Set (Observed-Remove Set) for managing sets in a distributed and concurrent environment #4980

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions DIRECTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
* [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)
* [ORSet](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/crdt/ORSet.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
Expand Down Expand Up @@ -623,6 +624,7 @@
* [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)
* [ORSetTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/crdt/ORSetTest.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
Expand Down
191 changes: 191 additions & 0 deletions src/main/java/com/thealgorithms/datastructures/crdt/ORSet.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package com.thealgorithms.datastructures.crdt;

import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

/**
* ORSet (Observed-Removed Set) is a state-based CRDT (Conflict-free Replicated Data Type)
* that supports both addition and removal of elements. This particular implementation follows
* the Add-Wins strategy, meaning that in case of conflicting add and remove operations,
* the add operation takes precedence. The merge operation of two OR-Sets ensures that
* elements added at any replica are eventually observed at all replicas. Removed elements,
* once observed, are never reintroduced.
* This OR-Set implementation provides methods for adding elements, removing elements,
* checking for element existence, retrieving the set of elements, comparing with other OR-Sets,
* and merging with another OR-Set to create a new OR-Set containing all unique elements
* from both sets.
*
* @author itakurah (Niklas Hoefflin) (https://github.com/itakurah)
* @see <a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type">Conflict-free_replicated_data_type</a>
* @see <a href="https://github.com/itakurah">itakurah (Niklas Hoefflin)</a>
*/

public class ORSet<T> {

private final Set<Pair<T>> elements;
private final Set<Pair<T>> tombstones;

/**
* Constructs an empty OR-Set.
*/
public ORSet() {
this.elements = new HashSet<>();
this.tombstones = new HashSet<>();
}

/**
* Checks if the set contains the specified element.
*
* @param element the element to check for
* @return true if the set contains the element, false otherwise
*/
public boolean contains(T element) {
return elements.stream().anyMatch(pair -> pair.getElement().equals(element));
}

/**
* Retrieves the elements in the set.
*
* @return a set containing the elements
*/
public Set<T> elements() {
Set<T> result = new HashSet<>();
elements.forEach(pair -> result.add(pair.getElement()));
return result;
}

/**
* Adds the specified element to the set.
*
* @param element the element to add
*/
public void add(T element) {
String n = prepare();
effect(element, n);
}

/**
* Removes the specified element from the set.
*
* @param element the element to remove
*/
public void remove(T element) {
Set<Pair<T>> pairsToRemove = prepare(element);
effect(pairsToRemove);
}

/**
* Collect all pairs with the specified element.
*
* @param element the element to collect pairs for
* @return a set of pairs with the specified element to be removed
*/
private Set<Pair<T>> prepare(T element) {
Set<Pair<T>> pairsToRemove = new HashSet<>();
for (Pair<T> pair : elements) {
if (pair.getElement().equals(element)) {
pairsToRemove.add(pair);
}
}
return pairsToRemove;
}

/**
* Generates a unique tag for the element.
*
* @return the unique tag
*/
private String prepare() {
return generateUniqueTag();
}

/**
* Adds the element with the specified unique tag to the set.
*
* @param element the element to add
* @param n the unique tag associated with the element
*/
private void effect(T element, String n) {
Pair<T> pair = new Pair<>(element, n);
elements.add(pair);
elements.removeAll(tombstones);
}

/**
* Removes the specified pairs from the set.
*
* @param pairsToRemove the pairs to remove
*/
private void effect(Set<Pair<T>> pairsToRemove) {
elements.removeAll(pairsToRemove);
tombstones.addAll(pairsToRemove);
}

/**
* Generates a unique tag.
*
* @return the unique tag
*/
private String generateUniqueTag() {
return UUID.randomUUID().toString();
}

/**
* Compares this Add-Wins OR-Set with another OR-Set to check if elements and tombstones are a subset.
*
* @param other the other OR-Set to compare
* @return true if the sets are subset, false otherwise
*/
public boolean compare(ORSet<T> other) {
Set<Pair<T>> union = new HashSet<>(elements);
union.addAll(tombstones);

Set<Pair<T>> otherUnion = new HashSet<>(other.elements);
otherUnion.addAll(other.tombstones);

return otherUnion.containsAll(union) && other.tombstones.containsAll(tombstones);
}

/**
* Merges this Add-Wins OR-Set with another OR-Set.
*
* @param other the other OR-Set to merge
*/
public void merge(ORSet<T> other) {
elements.removeAll(other.tombstones);
other.elements.removeAll(tombstones);
elements.addAll(other.elements);
tombstones.addAll(other.tombstones);
}

/**
* Represents a pair containing an element and a unique tag.
*
* @param <T> the type of the element in the pair
*/
public static class Pair<T> {
private final T element;
private final String uniqueTag;

/**
* Constructs a pair with the specified element and unique tag.
*
* @param element the element in the pair
* @param uniqueTag the unique tag associated with the element
*/
public Pair(T element, String uniqueTag) {
this.element = element;
this.uniqueTag = uniqueTag;
}

/**
* Gets the element from the pair.
*
* @return the element
*/
public T getElement() {
return element;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.thealgorithms.datastructures.crdt;

import static org.junit.jupiter.api.Assertions.*;

import java.util.Set;
import org.junit.jupiter.api.Test;

class ORSetTest {

@Test
void testContains() {
ORSet<String> orSet = new ORSet<>();
orSet.add("A");
assertTrue(orSet.contains("A"));
}

@Test
void testAdd() {
ORSet<String> orSet = new ORSet<>();
orSet.add("A");
assertTrue(orSet.contains("A"));
}

@Test
void testRemove() {
ORSet<String> orSet = new ORSet<>();
orSet.add("A");
orSet.add("A");
orSet.remove("A");
assertFalse(orSet.contains("A"));
}

@Test
void testElements() {
ORSet<String> orSet = new ORSet<>();
orSet.add("A");
orSet.add("B");
assertEquals(Set.of("A", "B"), orSet.elements());
}

@Test
void testCompareEqualSets() {
ORSet<String> orSet1 = new ORSet<>();
ORSet<String> orSet2 = new ORSet<>();

orSet1.add("A");
orSet2.add("A");
orSet2.add("B");
orSet2.add("C");
orSet2.remove("C");
orSet1.merge(orSet2);
orSet2.merge(orSet1);
orSet2.remove("B");

assertTrue(orSet1.compare(orSet2));
}

@Test
void testCompareDifferentSets() {
ORSet<String> orSet1 = new ORSet<>();
ORSet<String> orSet2 = new ORSet<>();

orSet1.add("A");
orSet2.add("B");

assertFalse(orSet1.compare(orSet2));
}

@Test
void testMerge() {
ORSet<String> orSet1 = new ORSet<>();
ORSet<String> orSet2 = new ORSet<>();

orSet1.add("A");
orSet1.add("A");
orSet1.add("B");
orSet1.remove("B");
orSet2.add("B");
orSet2.add("C");
orSet2.remove("C");
orSet1.merge(orSet2);

assertTrue(orSet1.contains("A"));
assertTrue(orSet1.contains("B"));
}
}