Version vectors with ternary comparison. Know whether your state is newer, older, or in conflict—instantly.
Distributed systems need to compare state across nodes. With version vectors, the answer is always one of three things: I'm newer (+1), you're newer (-1), or we're concurrent (0). That ternary comparison isn't just convenient—it's the mathematically complete classification. Two version vectors can relate in exactly three ways.
This crate gives you version vectors, ternary comparison, merge (component-wise max), and a ConflictResolver that tracks merge history. All in ~170 lines with no dependencies beyond HashMap.
Every distributed GPU node needs to know if its state is current. Timestamps don't work (clock skew). Vector clocks work but give you more information than you need. Version vectors are the minimal abstraction: per-node counters that you compare and merge.
The ternary insight is that compare() returns a VersionOrder enum: Newer, Older, or Concurrent. This maps directly to {-1, 0, +1} and directly to the action you should take:
| Comparison | Value | Action |
|---|---|---|
Newer |
+1 | Your state wins, push it |
Older |
-1 | Their state wins, pull it |
Concurrent |
0 | Conflict—merge required |
Version vectors give you causality, not just ordering. When A.compare(&B) returns Newer, it means A causally precedes B—A's updates are a subset of B's. When it returns Concurrent, it means neither causally precedes the other. Both have updates the other doesn't have. You must merge.
This is why the ternary comparison is complete: there is no fourth possibility. Either A dominates, B dominates, or they're concurrent. The code reflects the math.
use ternary_version::*;
// Two nodes track their own versions
let mut node_a = VersionVector::new();
let mut node_b = VersionVector::new();
// Each node increments its own counter on updates
node_a.increment("gpu-0"); // gpu-0: 1
node_a.increment("gpu-0"); // gpu-0: 2
node_b.increment("gpu-1"); // gpu-1: 1
// Compare: both have different nodes incremented → concurrent
assert_eq!(node_a.compare(&node_b), VersionOrder::Concurrent);
// Node B receives A's updates and merges
node_b.merge(&node_a);
node_b.increment("gpu-1"); // mark the merge
// Now B dominates A
assert_eq!(node_a.compare(&node_b), VersionOrder::Older);let mut vv = VersionVector::new();
// Per-node counter operations
vv.increment("node-1"); // increment and return new value
vv.set("node-2", 5); // set explicit version
vv.get("node-1"); // → u64 (0 if absent)
// Ternary comparison
vv.compare(&other); // → VersionOrder::Newer | Older | Concurrent
// Merge (component-wise max, modifies self)
vv.merge(&other);
// Inspection
vv.is_empty();
vv.node_count();pub enum VersionOrder {
Newer = 1, // self dominates other
Concurrent = 0, // neither dominates
Older = -1, // other dominates self
}A higher-level abstraction that tracks merge history:
let mut resolver = ConflictResolver::new("merge-node");
let merged = resolver.resolve(&version_a, &version_b);
// Merges A and B, then increments the resolver's own counter
// Returns the merged vector
resolver.resolved_count(); // how many conflicts resolvedThe algorithm is straightforward:
- Collect all node IDs from both vectors
- For each node, compare counters:
a > bsetsself_greater,b > asetsother_greater - If only one side has greater values → that side wins
- If both sides have some greater values → concurrent
- If all equal → concurrent (equal versions are treated as concurrent, which is semantically correct—no one dominates)
A: {x:2, y:1} vs B: {x:1}
x: A > B → self_greater
y: A > 0 → self_greater
Result: Newer (A dominates)
A: {x:2} vs B: {y:2}
x: A > B → self_greater
y: B > A → other_greater
Result: Concurrent (neither dominates)
use ternary_version::*;
struct GpuNode {
id: String,
state_version: VersionVector,
model_weights: Vec<i8>, // ternary weights
}
impl GpuNode {
fn new(id: &str) -> Self {
Self {
id: id.into(),
state_version: VersionVector::new(),
model_weights: vec![],
}
}
fn train_step(&mut self) {
// ... update model weights ...
self.state_version.increment(&self.id);
}
fn sync_with(&mut self, other: &GpuNode) -> SyncResult {
match self.state_version.compare(&other.state_version) {
VersionOrder::Newer => SyncResult::Push,
VersionOrder::Older => {
self.state_version.merge(&other.state_version);
SyncResult::Pull
}
VersionOrder::Concurrent => {
self.state_version.merge(&other.state_version);
self.state_version.increment(&self.id);
SyncResult::Merge
}
}
}
}
enum SyncResult { Push, Pull, Merge }
// Two nodes train independently, then sync
let mut node_0 = GpuNode::new("gpu-0");
let mut node_1 = GpuNode::new("gpu-1");
node_0.train_step(); // gpu-0: 1
node_0.train_step(); // gpu-0: 2
node_1.train_step(); // gpu-1: 1
// node_0 has updates node_1 doesn't have, and vice versa
let result = node_1.sync_with(&node_0);
assert!(matches!(result, SyncResult::Merge));VersionVector ──compare──→ VersionOrder {Newer, Older, Concurrent}
│ │
└──merge──→ merged VersionVector
│
ConflictResolver
(tracks merge history, auto-increments)
The VersionVector is a HashMap<String, u64> under the hood. Comparison is O(n) where n = total unique node IDs. Merge is O(n) where n = entries in the other vector.
- ternary-paxos — consensus decisions produce versioned values; use version vectors to track which round produced which decision
- ternary-rate-limiter — rate limiter state across distributed nodes needs version tracking
- ternary-resilience — network health state is versioned; cascading failures propagate versioned updates
- Garbage collection: Version vectors grow unboundedly as new nodes join. Dotted version vectors (VVV) or interval tree clocks could bound the size.
- Partial merge semantics: Current merge takes component-wise max. Some applications need CRDT-style merge (OR-Set, LWW-Register) layered on top.
- Byzantine comparison: What if a node lies about its version? Signed version vectors with merkle proofs would help.
| Metric | Value |
|---|---|
| Tests | 8 |
| Lines of code | 166 |
| Public API surface | 14 items |
| License | Apache-2.0 |
| Unsafe | 0 |
[dependencies]
ternary-version = "0.1.0"Apache-2.0