Skip to main content

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, 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 Default for GCounter {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl Merge for GCounter {
67    /// Merge another counter into this one.
68    ///
69    /// For each replica ID, takes the maximum count between the two counters.
70    /// This ensures convergence: merging A into B or B into A yields the same result.
71    fn merge(&mut self, other: &Self) {
72        for (&replica, &count) in &other.counts {
73            let entry = self.counts.entry(replica).or_insert(0);
74            *entry = (*entry).max(count);
75        }
76    }
77}
78
79// NOTE: Showable impl is in logicaffeine_system (io module)
80
81/// Compare a GCounter directly to a `u64` value.
82///
83/// Enables ergonomic conditionals like `counter == 5` by comparing
84/// the counter's aggregated value to the integer.
85impl PartialEq<u64> for GCounter {
86    fn eq(&self, other: &u64) -> bool {
87        self.value() == *other
88    }
89}
90
91/// Compare a GCounter directly to an `i32` value.
92///
93/// Enables ergonomic conditionals with smaller integer types.
94impl PartialEq<i32> for GCounter {
95    fn eq(&self, other: &i32) -> bool {
96        self.value() == (*other as u64)
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_gcounter_new() {
106        let c = GCounter::new();
107        assert_eq!(c.value(), 0);
108    }
109
110    #[test]
111    fn test_gcounter_increment() {
112        let mut c = GCounter::with_replica_id(1);
113        c.increment(5);
114        c.increment(3);
115        assert_eq!(c.value(), 8);
116    }
117
118    #[test]
119    fn test_gcounter_merge_disjoint() {
120        let mut c1 = GCounter::with_replica_id(1);
121        let mut c2 = GCounter::with_replica_id(2);
122
123        c1.increment(5);
124        c2.increment(3);
125
126        c1.merge(&c2);
127        assert_eq!(c1.value(), 8);
128    }
129
130    #[test]
131    fn test_gcounter_merge_commutative() {
132        let mut c1 = GCounter::with_replica_id(1);
133        let mut c2 = GCounter::with_replica_id(2);
134
135        c1.increment(5);
136        c2.increment(3);
137
138        let mut c1_copy = c1.clone();
139        let mut c2_copy = c2.clone();
140
141        c1_copy.merge(&c2);
142        c2_copy.merge(&c1);
143
144        assert_eq!(c1_copy.value(), c2_copy.value());
145    }
146
147    #[test]
148    fn test_gcounter_merge_idempotent() {
149        let mut c1 = GCounter::with_replica_id(1);
150        c1.increment(5);
151
152        let before = c1.value();
153        c1.merge(&c1.clone());
154        assert_eq!(c1.value(), before);
155    }
156
157    #[test]
158    fn test_gcounter_merge_same_replica() {
159        // When two counters have the same replica ID (simulating sync after divergence)
160        let mut c1 = GCounter::with_replica_id(1);
161        let mut c2 = GCounter::with_replica_id(1);
162
163        c1.increment(5);
164        c2.increment(3);
165
166        // After merge, should have max(5, 3) = 5
167        c1.merge(&c2);
168        assert_eq!(c1.value(), 5);
169    }
170}