Skip to main content

llm_sync/
vclock.rs

1// SPDX-License-Identifier: MIT
2//! Vector clock for causal ordering in distributed agent systems.
3//!
4//! ## Responsibility
5//! Provide a per-node logical timestamp that allows agents to establish causal
6//! ordering of events without relying on synchronized wall clocks.
7//!
8//! ## Guarantees
9//! - Merge is commutative, associative, and idempotent.
10//! - `tick` is non-blocking and O(1).
11//! - `compare` is O(n) in the number of distinct nodes.
12
13use std::collections::HashMap;
14use serde::{Deserialize, Serialize};
15
16/// A vector clock: maps node_id to logical timestamp.
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
18pub struct VectorClock {
19    clock: HashMap<String, u64>,
20}
21
22/// The causal ordering relationship between two vector clocks.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ClockOrder {
25    /// `self` happened before `other` (self ≤ other, strict on at least one component).
26    Before,
27    /// `self` happened after `other`.
28    After,
29    /// Neither dominates; events are concurrent.
30    Concurrent,
31    /// The clocks are identical.
32    Equal,
33}
34
35impl VectorClock {
36    /// Create a new, empty vector clock.
37    pub fn new() -> Self { Self::default() }
38
39    /// Increment this node's logical timestamp by 1.
40    ///
41    /// # Arguments
42    /// * `node` — Identifier for the node performing the event.
43    pub fn tick(&mut self, node: impl Into<String>) {
44        let n = node.into();
45        *self.clock.entry(n).or_insert(0) += 1;
46    }
47
48    /// Get a node's current timestamp. Returns 0 for unknown nodes.
49    ///
50    /// # Arguments
51    /// * `node` — Node identifier to query.
52    pub fn get(&self, node: &str) -> u64 {
53        *self.clock.get(node).unwrap_or(&0)
54    }
55
56    /// Merge two clocks, taking the component-wise maximum.
57    ///
58    /// The operation is commutative, associative, and idempotent.
59    ///
60    /// # Returns
61    /// A new `VectorClock` representing the merged state.
62    pub fn merge(&self, other: &VectorClock) -> VectorClock {
63        let mut result = self.clock.clone();
64        for (node, &ts) in &other.clock {
65            let entry = result.entry(node.clone()).or_insert(0);
66            if ts > *entry { *entry = ts; }
67        }
68        VectorClock { clock: result }
69    }
70
71    /// Compare two clocks to determine causal ordering.
72    ///
73    /// # Returns
74    /// A [`ClockOrder`] variant describing the relationship.
75    pub fn compare(&self, other: &VectorClock) -> ClockOrder {
76        let self_nodes: std::collections::HashSet<&str> =
77            self.clock.keys().map(|s| s.as_str()).collect();
78        let other_nodes: std::collections::HashSet<&str> =
79            other.clock.keys().map(|s| s.as_str()).collect();
80        let all_nodes: std::collections::HashSet<&str> =
81            self_nodes.union(&other_nodes).copied().collect();
82
83        let mut self_less = false;
84        let mut other_less = false;
85
86        for node in all_nodes {
87            let s = self.get(node);
88            let o = other.get(node);
89            if s < o { self_less = true; }
90            if s > o { other_less = true; }
91        }
92
93        match (self_less, other_less) {
94            (false, false) => ClockOrder::Equal,
95            (true, false) => ClockOrder::Before,
96            (false, true) => ClockOrder::After,
97            (true, true) => ClockOrder::Concurrent,
98        }
99    }
100
101    /// Return the number of distinct nodes tracked by this clock.
102    pub fn node_count(&self) -> usize { self.clock.len() }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_vclock_tick_increments_counter() {
111        let mut c = VectorClock::new();
112        c.tick("a");
113        c.tick("a");
114        assert_eq!(c.get("a"), 2);
115    }
116
117    #[test]
118    fn test_vclock_get_unknown_node_returns_zero() {
119        let c = VectorClock::new();
120        assert_eq!(c.get("nobody"), 0);
121    }
122
123    #[test]
124    fn test_vclock_merge_takes_max_component() {
125        let mut c1 = VectorClock::new();
126        c1.tick("a"); c1.tick("a"); // a=2
127        let mut c2 = VectorClock::new();
128        c2.tick("a"); c2.tick("b"); // a=1, b=1
129        let merged = c1.merge(&c2);
130        assert_eq!(merged.get("a"), 2);
131        assert_eq!(merged.get("b"), 1);
132    }
133
134    #[test]
135    fn test_vclock_merge_is_commutative() {
136        let mut c1 = VectorClock::new();
137        c1.tick("x"); c1.tick("x");
138        let mut c2 = VectorClock::new();
139        c2.tick("y");
140        let m1 = c1.merge(&c2);
141        let m2 = c2.merge(&c1);
142        assert_eq!(m1, m2);
143    }
144
145    #[test]
146    fn test_vclock_merge_is_idempotent() {
147        let mut c = VectorClock::new();
148        c.tick("a");
149        let m = c.merge(&c.clone());
150        assert_eq!(m, c);
151    }
152
153    #[test]
154    fn test_vclock_merge_is_associative() {
155        let mut a = VectorClock::new(); a.tick("a");
156        let mut b = VectorClock::new(); b.tick("b");
157        let mut c = VectorClock::new(); c.tick("c");
158        let lhs = a.merge(&b).merge(&c);
159        let rhs = a.merge(&b.merge(&c));
160        assert_eq!(lhs, rhs);
161    }
162
163    #[test]
164    fn test_vclock_compare_before() {
165        let c1 = VectorClock::new();
166        let mut c2 = VectorClock::new();
167        c2.tick("a");
168        assert_eq!(c1.compare(&c2), ClockOrder::Before);
169    }
170
171    #[test]
172    fn test_vclock_compare_after() {
173        let mut c1 = VectorClock::new();
174        c1.tick("a");
175        let c2 = VectorClock::new();
176        assert_eq!(c1.compare(&c2), ClockOrder::After);
177    }
178
179    #[test]
180    fn test_vclock_compare_concurrent() {
181        let mut c1 = VectorClock::new();
182        c1.tick("a");
183        let mut c2 = VectorClock::new();
184        c2.tick("b");
185        assert_eq!(c1.compare(&c2), ClockOrder::Concurrent);
186    }
187
188    #[test]
189    fn test_vclock_compare_equal_empty() {
190        let c1 = VectorClock::new();
191        let c2 = VectorClock::new();
192        assert_eq!(c1.compare(&c2), ClockOrder::Equal);
193    }
194
195    #[test]
196    fn test_vclock_compare_equal_nonempty() {
197        let mut c1 = VectorClock::new();
198        c1.tick("x"); c1.tick("y");
199        let c2 = c1.clone();
200        assert_eq!(c1.compare(&c2), ClockOrder::Equal);
201    }
202
203    #[test]
204    fn test_vclock_node_count() {
205        let mut c = VectorClock::new();
206        c.tick("a"); c.tick("b");
207        assert_eq!(c.node_count(), 2);
208    }
209}