finished initial networking impl
Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
263
crates/lib/src/networking/merge.rs
Normal file
263
crates/lib/src/networking/merge.rs
Normal file
@@ -0,0 +1,263 @@
|
||||
//! CRDT merge logic for conflict resolution
|
||||
//!
|
||||
//! This module implements the merge semantics for different CRDT types:
|
||||
//! - Last-Write-Wins (LWW) for simple components
|
||||
//! - OR-Set for concurrent add/remove
|
||||
//! - Sequence CRDT (RGA) for ordered lists
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::networking::{
|
||||
operations::ComponentOp,
|
||||
vector_clock::{
|
||||
NodeId,
|
||||
VectorClock,
|
||||
},
|
||||
};
|
||||
|
||||
/// Result of comparing two operations for merge
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MergeDecision {
|
||||
/// The local operation wins (keep local, discard remote)
|
||||
KeepLocal,
|
||||
|
||||
/// The remote operation wins (apply remote, discard local)
|
||||
ApplyRemote,
|
||||
|
||||
/// Operations are concurrent, need CRDT-specific merge
|
||||
Concurrent,
|
||||
|
||||
/// Operations are identical
|
||||
Equal,
|
||||
}
|
||||
|
||||
/// Compare two operations using vector clocks to determine merge decision
|
||||
///
|
||||
/// This implements Last-Write-Wins (LWW) semantics with node ID tiebreaking.
|
||||
///
|
||||
/// # Algorithm
|
||||
///
|
||||
/// 1. If local happened-before remote: ApplyRemote
|
||||
/// 2. If remote happened-before local: KeepLocal
|
||||
/// 3. If concurrent: use node ID as tiebreaker (higher node ID wins)
|
||||
/// 4. If equal: Equal
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use lib::networking::{VectorClock, compare_operations_lww};
|
||||
/// use uuid::Uuid;
|
||||
///
|
||||
/// let node1 = Uuid::new_v4();
|
||||
/// let node2 = Uuid::new_v4();
|
||||
///
|
||||
/// let mut clock1 = VectorClock::new();
|
||||
/// clock1.increment(node1);
|
||||
///
|
||||
/// let mut clock2 = VectorClock::new();
|
||||
/// clock2.increment(node2);
|
||||
///
|
||||
/// // Concurrent operations use node ID as tiebreaker
|
||||
/// let decision = compare_operations_lww(&clock1, node1, &clock2, node2);
|
||||
/// ```
|
||||
pub fn compare_operations_lww(
|
||||
local_clock: &VectorClock,
|
||||
local_node: NodeId,
|
||||
remote_clock: &VectorClock,
|
||||
remote_node: NodeId,
|
||||
) -> MergeDecision {
|
||||
// Check if clocks are equal
|
||||
if local_clock == remote_clock && local_node == remote_node {
|
||||
return MergeDecision::Equal;
|
||||
}
|
||||
|
||||
// Check happens-before relationship
|
||||
if local_clock.happened_before(remote_clock) {
|
||||
return MergeDecision::ApplyRemote;
|
||||
}
|
||||
|
||||
if remote_clock.happened_before(local_clock) {
|
||||
return MergeDecision::KeepLocal;
|
||||
}
|
||||
|
||||
// Concurrent operations - use node ID as tiebreaker
|
||||
// Higher node ID wins for deterministic resolution
|
||||
if remote_node > local_node {
|
||||
MergeDecision::ApplyRemote
|
||||
} else if local_node > remote_node {
|
||||
MergeDecision::KeepLocal
|
||||
} else {
|
||||
MergeDecision::Concurrent
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine if a remote Set operation should be applied
|
||||
///
|
||||
/// This is a convenience wrapper around `compare_operations_lww` for Set
|
||||
/// operations specifically.
|
||||
pub fn should_apply_set(local_op: &ComponentOp, remote_op: &ComponentOp) -> bool {
|
||||
// Extract vector clocks and node IDs
|
||||
let (local_clock, local_data) = match local_op {
|
||||
| ComponentOp::Set {
|
||||
vector_clock, data, ..
|
||||
} => (vector_clock, data),
|
||||
| _ => return false,
|
||||
};
|
||||
|
||||
let (remote_clock, remote_data) = match remote_op {
|
||||
| ComponentOp::Set {
|
||||
vector_clock, data, ..
|
||||
} => (vector_clock, data),
|
||||
| _ => return false,
|
||||
};
|
||||
|
||||
// If data is identical, no need to apply
|
||||
if local_data == remote_data {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use the sequence number from the clocks as a simple tiebreaker
|
||||
// In a real implementation, we'd use the full node IDs
|
||||
let local_seq: u64 = local_clock.clocks.values().sum();
|
||||
let remote_seq: u64 = remote_clock.clocks.values().sum();
|
||||
|
||||
// Compare clocks
|
||||
match compare_operations_lww(
|
||||
local_clock,
|
||||
uuid::Uuid::nil(), // Simplified - would use actual node IDs
|
||||
remote_clock,
|
||||
uuid::Uuid::nil(),
|
||||
) {
|
||||
| MergeDecision::ApplyRemote => true,
|
||||
| MergeDecision::KeepLocal => false,
|
||||
| MergeDecision::Concurrent => remote_seq > local_seq,
|
||||
| MergeDecision::Equal => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Log a merge conflict for debugging
|
||||
///
|
||||
/// This helps track when concurrent operations occur and how they're resolved.
|
||||
pub fn log_merge_conflict(
|
||||
component_type: &str,
|
||||
local_clock: &VectorClock,
|
||||
remote_clock: &VectorClock,
|
||||
decision: MergeDecision,
|
||||
) {
|
||||
info!(
|
||||
"Merge conflict on {}: local={:?}, remote={:?}, decision={:?}",
|
||||
component_type, local_clock, remote_clock, decision
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::networking::messages::ComponentData;
|
||||
|
||||
#[test]
|
||||
fn test_lww_happened_before() {
|
||||
let node1 = uuid::Uuid::new_v4();
|
||||
let node2 = uuid::Uuid::new_v4();
|
||||
|
||||
let mut clock1 = VectorClock::new();
|
||||
clock1.increment(node1);
|
||||
|
||||
let mut clock2 = VectorClock::new();
|
||||
clock2.increment(node1);
|
||||
clock2.increment(node1);
|
||||
|
||||
let decision = compare_operations_lww(&clock1, node1, &clock2, node2);
|
||||
assert_eq!(decision, MergeDecision::ApplyRemote);
|
||||
|
||||
let decision = compare_operations_lww(&clock2, node1, &clock1, node2);
|
||||
assert_eq!(decision, MergeDecision::KeepLocal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lww_concurrent() {
|
||||
let node1 = uuid::Uuid::new_v4();
|
||||
let node2 = uuid::Uuid::new_v4();
|
||||
|
||||
let mut clock1 = VectorClock::new();
|
||||
clock1.increment(node1);
|
||||
|
||||
let mut clock2 = VectorClock::new();
|
||||
clock2.increment(node2);
|
||||
|
||||
// Concurrent operations use node ID tiebreaker
|
||||
let decision = compare_operations_lww(&clock1, node1, &clock2, node2);
|
||||
|
||||
// Should use node ID as tiebreaker
|
||||
assert!(
|
||||
decision == MergeDecision::ApplyRemote || decision == MergeDecision::KeepLocal
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lww_equal() {
|
||||
let node1 = uuid::Uuid::new_v4();
|
||||
|
||||
let mut clock1 = VectorClock::new();
|
||||
clock1.increment(node1);
|
||||
|
||||
let clock2 = clock1.clone();
|
||||
|
||||
let decision = compare_operations_lww(&clock1, node1, &clock2, node1);
|
||||
assert_eq!(decision, MergeDecision::Equal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_apply_set_same_data() {
|
||||
let node_id = uuid::Uuid::new_v4();
|
||||
let mut clock = VectorClock::new();
|
||||
clock.increment(node_id);
|
||||
|
||||
let data = vec![1, 2, 3];
|
||||
|
||||
let op1 = ComponentOp::Set {
|
||||
component_type: "Transform".to_string(),
|
||||
data: ComponentData::Inline(data.clone()),
|
||||
vector_clock: clock.clone(),
|
||||
};
|
||||
|
||||
let op2 = ComponentOp::Set {
|
||||
component_type: "Transform".to_string(),
|
||||
data: ComponentData::Inline(data.clone()),
|
||||
vector_clock: clock,
|
||||
};
|
||||
|
||||
// Same data, should not apply
|
||||
assert!(!should_apply_set(&op1, &op2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_apply_set_newer_wins() {
|
||||
let node_id = uuid::Uuid::new_v4();
|
||||
|
||||
let mut clock1 = VectorClock::new();
|
||||
clock1.increment(node_id);
|
||||
|
||||
let mut clock2 = VectorClock::new();
|
||||
clock2.increment(node_id);
|
||||
clock2.increment(node_id);
|
||||
|
||||
let op1 = ComponentOp::Set {
|
||||
component_type: "Transform".to_string(),
|
||||
data: ComponentData::Inline(vec![1, 2, 3]),
|
||||
vector_clock: clock1,
|
||||
};
|
||||
|
||||
let op2 = ComponentOp::Set {
|
||||
component_type: "Transform".to_string(),
|
||||
data: ComponentData::Inline(vec![4, 5, 6]),
|
||||
vector_clock: clock2,
|
||||
};
|
||||
|
||||
// op2 is newer, should apply
|
||||
assert!(should_apply_set(&op1, &op2));
|
||||
|
||||
// op1 is older, should not apply
|
||||
assert!(!should_apply_set(&op2, &op1));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user