Skip to content

Added G-Counter (Grow-only Counter) for tracking counts in distributed systems #4965

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 15 commits into from
Nov 24, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.thealgorithms.datastructures.crdt;

import java.util.HashMap;
import java.util.Map;

/**
* G-Counter (Grow-only Counter) is a state-based CRDT (Conflict-free Replicated Data Type)
* designed for tracking counts in a distributed and concurrent environment.
* Each process maintains its own counter, allowing only increments. The total count
* is obtained by summing individual process counts.
* This implementation supports incrementing, querying the total count,
* comparing with other G-Counters, and merging with another G-Counter
* to compute the element-wise maximum.
* (https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type)
*
* @author itakurah (https://github.com/itakurah)
*/

class GCounter {
private final Map<Integer, Integer> P;
private final int myId;
private final int n;

/**
* Constructs a G-Counter for a cluster of n nodes.
*
* @param n The number of nodes in the cluster.
*/
public GCounter(int myId, int n) {
this.myId = myId;
this.n = n;
this.P = new HashMap<>();

for (int i = 0; i < n; i++) {
P.put(i, 0);
}
}

/**
* Increments the counter for the current node.
*/
public void increment() {
P.put(myId, P.get(myId) + 1);
}

/**
* Gets the total value of the counter by summing up values from all nodes.
*
* @return The total value of the counter.
*/
public int value() {
int sum = 0;
for (int v : P.values()) {
sum += v;
}
return sum;
}

/**
* Compares the state of this G-Counter with another G-Counter.
*
* @param other The other G-Counter to compare with.
* @return True if the state of this G-Counter is less than or equal to the state of the other G-Counter.
*/
public boolean compare(GCounter other) {
for (int i = 0; i < n; i++) {
if (this.P.get(i) > other.P.get(i)) {
return false;
}
}
return true;
}

/**
* Merges the state of this G-Counter with another G-Counter.
*
* @param other The other G-Counter to merge with.
*/
public void merge(GCounter other) {
for (int i = 0; i < n; i++) {
this.P.put(i, Math.max(this.P.get(i), other.P.get(i)));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.thealgorithms.datastructures.crdt;

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

import org.junit.jupiter.api.Test;

public class GCounterTest {
@Test
void increment() {
GCounter counter = new GCounter(0, 3);
counter.increment();
counter.increment();
counter.increment();
assertEquals(3, counter.value());
}

@Test
void merge() {
GCounter counter1 = new GCounter(0, 3);
counter1.increment();
GCounter counter2 = new GCounter(1, 3);
counter2.increment();
counter2.increment();
GCounter counter3 = new GCounter(2, 3);
counter3.increment();
counter3.increment();
counter3.increment();
counter1.merge(counter2);
counter1.merge(counter3);
counter2.merge(counter1);
counter3.merge(counter2);
assertEquals(6, counter1.value());
assertEquals(6, counter2.value());
assertEquals(6, counter3.value());
}

@Test
void compare() {
GCounter counter1 = new GCounter(0, 5);
GCounter counter2 = new GCounter(3, 5);
counter1.increment();
counter1.increment();
counter2.merge(counter1);
counter2.increment();
counter2.increment();
assertTrue(counter1.compare(counter2));
counter1.increment();
counter2.increment();
counter2.merge(counter1);
assertTrue(counter1.compare(counter2));
counter1.increment();
assertFalse(counter1.compare(counter2));
}
}