logicaffeine_data/crdt/
gcounter.rs

1//! G-Counter (Grow-only Counter) CRDT.
2//!
3//! A counter that can only be incremented, never decremented.
4//! Each replica maintains its own local count, and the total value
5//! is the sum of all replica counts.
6//!
7//! Uses `u64` replica IDs for efficient vector clock operations.
8
9use super::replica::{generate_replica_id, ReplicaId};
10use super::Merge;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14/// A grow-only counter that supports distributed increment operations.
15///
16/// Each replica has a unique ID and maintains its own count.
17/// The total value is the sum across all replicas.
18/// Merging takes the maximum count for each replica ID.
19#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
20pub struct GCounter {
21    /// Map from replica ID to local count
22    counts: HashMap<ReplicaId, u64>,
23    /// This replica's ID (set on first increment)
24    replica_id: ReplicaId,
25}
26
27impl GCounter {
28    /// Create a new empty counter.
29    pub fn new() -> Self {
30        Self {
31            counts: HashMap::new(),
32            replica_id: generate_replica_id(),
33        }
34    }
35
36    /// Create a counter with a specific replica ID.
37    pub fn with_replica_id(id: ReplicaId) -> Self {
38        Self {
39            counts: HashMap::new(),
40            replica_id: id,
41        }
42    }
43
44    /// Increment the counter by the given amount.
45    pub fn increment(&mut self, amount: u64) {
46        *self.counts.entry(self.replica_id).or_insert(0) += amount;
47    }
48
49    /// Get the current value (sum of all replica counts).
50    pub fn value(&self) -> u64 {
51        self.counts.values().sum()
52    }
53
54    /// Get the replica ID for this counter.
55    pub fn replica_id(&self) -> ReplicaId {
56        self.replica_id
57    }
58}
59
60impl Merge for GCounter {
61    /// Merge another counter into this one.
62    ///
63    /// For each replica ID, takes the maximum count between the two counters.
64    /// This ensures convergence: merging A into B or B into A yields the same result.
65    fn merge(&mut self, other: &Self) {
66        for (&replica, &count) in &other.counts {
67            let entry = self.counts.entry(replica).or_insert(0);
68            *entry = (*entry).max(count);
69        }
70    }
71}
72
73// NOTE: Showable impl is in logicaffeine_system (io module)
74
75/// Compare a GCounter directly to a `u64` value.
76///
77/// Enables ergonomic conditionals like `counter == 5` by comparing
78/// the counter's aggregated value to the integer.
79impl PartialEq<u64> for GCounter {
80    fn eq(&self, other: &u64) -> bool {
81        self.value() == *other
82    }
83}
84
85/// Compare a GCounter directly to an `i32` value.
86///
87/// Enables ergonomic conditionals with smaller integer types.
88impl PartialEq<i32> for GCounter {
89    fn eq(&self, other: &i32) -> bool {
90        self.value() == (*other as u64)
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_gcounter_new() {
100        let c = GCounter::new();
101        assert_eq!(c.value(), 0);
102    }
103
104    #[test]
105    fn test_gcounter_increment() {
106        let mut c = GCounter::with_replica_id(1);
107        c.increment(5);
108        c.increment(3);
109        assert_eq!(c.value(), 8);
110    }
111
112    #[test]
113    fn test_gcounter_merge_disjoint() {
114        let mut c1 = GCounter::with_replica_id(1);
115        let mut c2 = GCounter::with_replica_id(2);
116
117        c1.increment(5);
118        c2.increment(3);
119
120        c1.merge(&c2);
121        assert_eq!(c1.value(), 8);
122    }
123
124    #[test]
125    fn test_gcounter_merge_commutative() {
126        let mut c1 = GCounter::with_replica_id(1);
127        let mut c2 = GCounter::with_replica_id(2);
128
129        c1.increment(5);
130        c2.increment(3);
131
132        let mut c1_copy = c1.clone();
133        let mut c2_copy = c2.clone();
134
135        c1_copy.merge(&c2);
136        c2_copy.merge(&c1);
137
138        assert_eq!(c1_copy.value(), c2_copy.value());
139    }
140
141    #[test]
142    fn test_gcounter_merge_idempotent() {
143        let mut c1 = GCounter::with_replica_id(1);
144        c1.increment(5);
145
146        let before = c1.value();
147        c1.merge(&c1.clone());
148        assert_eq!(c1.value(), before);
149    }
150
151    #[test]
152    fn test_gcounter_merge_same_replica() {
153        // When two counters have the same replica ID (simulating sync after divergence)
154        let mut c1 = GCounter::with_replica_id(1);
155        let mut c2 = GCounter::with_replica_id(1);
156
157        c1.increment(5);
158        c2.increment(3);
159
160        // After merge, should have max(5, 3) = 5
161        c1.merge(&c2);
162        assert_eq!(c1.value(), 5);
163    }
164}