Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
319
vendor/ruvector/crates/ruvector-delta-consensus/src/causal.rs
vendored
Normal file
319
vendor/ruvector/crates/ruvector-delta-consensus/src/causal.rs
vendored
Normal file
@@ -0,0 +1,319 @@
|
||||
//! Causal ordering with vector clocks and hybrid logical clocks
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ReplicaId;
|
||||
|
||||
/// Vector clock for causal ordering
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct VectorClock {
|
||||
/// Map of replica ID to logical timestamp
|
||||
clock: HashMap<ReplicaId, u64>,
|
||||
}
|
||||
|
||||
impl VectorClock {
|
||||
/// Create a new vector clock
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
clock: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment the clock for a replica
|
||||
pub fn increment(&mut self, replica_id: &str) {
|
||||
let counter = self.clock.entry(replica_id.to_string()).or_insert(0);
|
||||
*counter += 1;
|
||||
}
|
||||
|
||||
/// Get the timestamp for a replica
|
||||
pub fn get(&self, replica_id: &str) -> u64 {
|
||||
self.clock.get(replica_id).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Merge with another vector clock (take max of each component)
|
||||
pub fn merge(&mut self, other: &VectorClock) {
|
||||
for (replica_id, ×tamp) in &other.clock {
|
||||
let current = self.clock.entry(replica_id.clone()).or_insert(0);
|
||||
*current = (*current).max(timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this clock happens-before another
|
||||
pub fn happens_before(&self, other: &VectorClock) -> bool {
|
||||
let mut at_least_one_less = false;
|
||||
|
||||
// Check all replicas in self
|
||||
for (replica_id, &self_ts) in &self.clock {
|
||||
let other_ts = other.get(replica_id);
|
||||
if self_ts > other_ts {
|
||||
return false;
|
||||
}
|
||||
if self_ts < other_ts {
|
||||
at_least_one_less = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check replicas only in other
|
||||
for (replica_id, &other_ts) in &other.clock {
|
||||
if !self.clock.contains_key(replica_id) && other_ts > 0 {
|
||||
at_least_one_less = true;
|
||||
}
|
||||
}
|
||||
|
||||
at_least_one_less
|
||||
}
|
||||
|
||||
/// Check if two clocks are concurrent
|
||||
pub fn is_concurrent(&self, other: &VectorClock) -> bool {
|
||||
!self.happens_before(other) && !other.happens_before(self) && self != other
|
||||
}
|
||||
|
||||
/// Compare two vector clocks
|
||||
pub fn compare(&self, other: &VectorClock) -> CausalOrder {
|
||||
if self == other {
|
||||
CausalOrder::Equal
|
||||
} else if self.happens_before(other) {
|
||||
CausalOrder::Before
|
||||
} else if other.happens_before(self) {
|
||||
CausalOrder::After
|
||||
} else {
|
||||
CausalOrder::Concurrent
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all replica IDs
|
||||
pub fn replicas(&self) -> Vec<&ReplicaId> {
|
||||
self.clock.keys().collect()
|
||||
}
|
||||
|
||||
/// Get total count (sum of all timestamps)
|
||||
pub fn total(&self) -> u64 {
|
||||
self.clock.values().sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VectorClock {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Causal ordering relationship
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CausalOrder {
|
||||
/// Equal clocks
|
||||
Equal,
|
||||
/// First happens before second
|
||||
Before,
|
||||
/// First happens after second
|
||||
After,
|
||||
/// Concurrent (conflicting)
|
||||
Concurrent,
|
||||
}
|
||||
|
||||
/// Hybrid Logical Clock for combining physical and logical time
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct HybridLogicalClock {
|
||||
/// Physical time component (milliseconds)
|
||||
pub physical: u64,
|
||||
/// Logical counter
|
||||
pub logical: u32,
|
||||
/// Replica ID
|
||||
pub replica: ReplicaId,
|
||||
}
|
||||
|
||||
impl HybridLogicalClock {
|
||||
/// Create a new HLC
|
||||
pub fn new(replica: ReplicaId) -> Self {
|
||||
Self {
|
||||
physical: current_time_millis(),
|
||||
logical: 0,
|
||||
replica,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current timestamp
|
||||
pub fn now(&mut self) -> HlcTimestamp {
|
||||
let pt = current_time_millis();
|
||||
|
||||
if pt > self.physical {
|
||||
self.physical = pt;
|
||||
self.logical = 0;
|
||||
} else {
|
||||
self.logical += 1;
|
||||
}
|
||||
|
||||
HlcTimestamp {
|
||||
physical: self.physical,
|
||||
logical: self.logical,
|
||||
replica: self.replica.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update clock on message receive
|
||||
pub fn receive(&mut self, remote: &HlcTimestamp) -> HlcTimestamp {
|
||||
let pt = current_time_millis();
|
||||
|
||||
if pt > self.physical && pt > remote.physical {
|
||||
self.physical = pt;
|
||||
self.logical = 0;
|
||||
} else if self.physical > remote.physical {
|
||||
self.logical += 1;
|
||||
} else if remote.physical > self.physical {
|
||||
self.physical = remote.physical;
|
||||
self.logical = remote.logical + 1;
|
||||
} else {
|
||||
self.logical = self.logical.max(remote.logical) + 1;
|
||||
}
|
||||
|
||||
HlcTimestamp {
|
||||
physical: self.physical,
|
||||
logical: self.logical,
|
||||
replica: self.replica.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A timestamp from an HLC
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct HlcTimestamp {
|
||||
/// Physical time
|
||||
pub physical: u64,
|
||||
/// Logical counter
|
||||
pub logical: u32,
|
||||
/// Origin replica
|
||||
pub replica: ReplicaId,
|
||||
}
|
||||
|
||||
impl HlcTimestamp {
|
||||
/// Compare timestamps
|
||||
pub fn compare(&self, other: &HlcTimestamp) -> Ordering {
|
||||
match self.physical.cmp(&other.physical) {
|
||||
Ordering::Equal => match self.logical.cmp(&other.logical) {
|
||||
Ordering::Equal => self.replica.cmp(&other.replica),
|
||||
other => other,
|
||||
},
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for HlcTimestamp {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.compare(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for HlcTimestamp {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.compare(other)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current time in milliseconds
|
||||
fn current_time_millis() -> u64 {
|
||||
chrono::Utc::now().timestamp_millis() as u64
|
||||
}
|
||||
|
||||
/// Lamport timestamp (simpler alternative to vector clocks)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct LamportClock {
|
||||
/// Logical timestamp
|
||||
pub timestamp: u64,
|
||||
/// Replica ID for tie-breaking
|
||||
pub replica: ReplicaId,
|
||||
}
|
||||
|
||||
impl LamportClock {
|
||||
/// Create a new Lamport clock
|
||||
pub fn new(replica: ReplicaId) -> Self {
|
||||
Self {
|
||||
timestamp: 0,
|
||||
replica,
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment on local event
|
||||
pub fn tick(&mut self) -> u64 {
|
||||
self.timestamp += 1;
|
||||
self.timestamp
|
||||
}
|
||||
|
||||
/// Update on message receive
|
||||
pub fn receive(&mut self, remote_timestamp: u64) -> u64 {
|
||||
self.timestamp = self.timestamp.max(remote_timestamp) + 1;
|
||||
self.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_vector_clock_happens_before() {
|
||||
let mut clock1 = VectorClock::new();
|
||||
clock1.increment("r1");
|
||||
|
||||
let mut clock2 = clock1.clone();
|
||||
clock2.increment("r1");
|
||||
|
||||
assert!(clock1.happens_before(&clock2));
|
||||
assert!(!clock2.happens_before(&clock1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vector_clock_concurrent() {
|
||||
let mut clock1 = VectorClock::new();
|
||||
clock1.increment("r1");
|
||||
|
||||
let mut clock2 = VectorClock::new();
|
||||
clock2.increment("r2");
|
||||
|
||||
assert!(clock1.is_concurrent(&clock2));
|
||||
assert!(clock2.is_concurrent(&clock1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vector_clock_merge() {
|
||||
let mut clock1 = VectorClock::new();
|
||||
clock1.increment("r1");
|
||||
clock1.increment("r1");
|
||||
|
||||
let mut clock2 = VectorClock::new();
|
||||
clock2.increment("r2");
|
||||
clock2.increment("r2");
|
||||
clock2.increment("r2");
|
||||
|
||||
clock1.merge(&clock2);
|
||||
|
||||
assert_eq!(clock1.get("r1"), 2);
|
||||
assert_eq!(clock1.get("r2"), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hlc_ordering() {
|
||||
let mut hlc1 = HybridLogicalClock::new("r1".to_string());
|
||||
let mut hlc2 = HybridLogicalClock::new("r2".to_string());
|
||||
|
||||
let ts1 = hlc1.now();
|
||||
let ts2 = hlc2.now();
|
||||
|
||||
// Timestamps should be comparable
|
||||
let cmp = ts1.compare(&ts2);
|
||||
assert!(cmp != Ordering::Equal || ts1.replica != ts2.replica);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lamport_clock() {
|
||||
let mut clock = LamportClock::new("r1".to_string());
|
||||
|
||||
assert_eq!(clock.tick(), 1);
|
||||
assert_eq!(clock.tick(), 2);
|
||||
assert_eq!(clock.receive(10), 11);
|
||||
assert_eq!(clock.tick(), 12);
|
||||
}
|
||||
}
|
||||
285
vendor/ruvector/crates/ruvector-delta-consensus/src/conflict.rs
vendored
Normal file
285
vendor/ruvector/crates/ruvector-delta-consensus/src/conflict.rs
vendored
Normal file
@@ -0,0 +1,285 @@
|
||||
//! Conflict resolution strategies for concurrent deltas
|
||||
|
||||
use ruvector_delta_core::{Delta, VectorDelta};
|
||||
|
||||
use crate::{ConsensusError, Result};
|
||||
|
||||
/// Conflict resolution strategy
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConflictStrategy {
|
||||
/// Last write wins (by timestamp)
|
||||
LastWriteWins,
|
||||
/// First write wins
|
||||
FirstWriteWins,
|
||||
/// Merge all deltas
|
||||
Merge,
|
||||
/// Custom resolution function
|
||||
Custom,
|
||||
}
|
||||
|
||||
/// Result of conflict resolution
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MergeResult {
|
||||
/// The resolved delta
|
||||
pub delta: VectorDelta,
|
||||
/// Number of deltas merged
|
||||
pub merged_count: usize,
|
||||
/// Whether conflicts were detected
|
||||
pub had_conflicts: bool,
|
||||
}
|
||||
|
||||
/// Trait for conflict resolution
|
||||
pub trait ConflictResolver<T> {
|
||||
/// Resolve conflicts between multiple deltas
|
||||
fn resolve(&self, deltas: &[&T]) -> Result<T>;
|
||||
}
|
||||
|
||||
/// Last-write-wins resolver
|
||||
pub struct LastWriteWinsResolver;
|
||||
|
||||
impl ConflictResolver<VectorDelta> for LastWriteWinsResolver {
|
||||
fn resolve(&self, deltas: &[&VectorDelta]) -> Result<VectorDelta> {
|
||||
if deltas.is_empty() {
|
||||
return Err(ConsensusError::InvalidOperation(
|
||||
"No deltas to resolve".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Take the last delta (assumed to be sorted by timestamp)
|
||||
Ok(deltas.last().unwrap().clone().clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// First-write-wins resolver
|
||||
pub struct FirstWriteWinsResolver;
|
||||
|
||||
impl ConflictResolver<VectorDelta> for FirstWriteWinsResolver {
|
||||
fn resolve(&self, deltas: &[&VectorDelta]) -> Result<VectorDelta> {
|
||||
if deltas.is_empty() {
|
||||
return Err(ConsensusError::InvalidOperation(
|
||||
"No deltas to resolve".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Take the first delta
|
||||
Ok(deltas.first().unwrap().clone().clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge resolver - composes all deltas
|
||||
#[derive(Default)]
|
||||
pub struct MergeResolver {
|
||||
/// Weight for averaging (if applicable)
|
||||
pub averaging: bool,
|
||||
}
|
||||
|
||||
impl MergeResolver {
|
||||
/// Create new merge resolver
|
||||
pub fn new() -> Self {
|
||||
Self { averaging: false }
|
||||
}
|
||||
|
||||
/// Create with averaging enabled
|
||||
pub fn with_averaging() -> Self {
|
||||
Self { averaging: true }
|
||||
}
|
||||
}
|
||||
|
||||
impl ConflictResolver<VectorDelta> for MergeResolver {
|
||||
fn resolve(&self, deltas: &[&VectorDelta]) -> Result<VectorDelta> {
|
||||
if deltas.is_empty() {
|
||||
return Err(ConsensusError::InvalidOperation(
|
||||
"No deltas to resolve".into(),
|
||||
));
|
||||
}
|
||||
|
||||
if deltas.len() == 1 {
|
||||
return Ok(deltas[0].clone().clone());
|
||||
}
|
||||
|
||||
// Compose all deltas
|
||||
let mut result = deltas[0].clone().clone();
|
||||
for delta in deltas.iter().skip(1) {
|
||||
result = result.compose((*delta).clone());
|
||||
}
|
||||
|
||||
// Optionally average the result
|
||||
if self.averaging {
|
||||
let scale = 1.0 / deltas.len() as f32;
|
||||
result = result.scale(scale);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Weighted merge resolver
|
||||
pub struct WeightedMergeResolver {
|
||||
/// Default weight for deltas without explicit weight
|
||||
pub default_weight: f32,
|
||||
}
|
||||
|
||||
impl WeightedMergeResolver {
|
||||
/// Create new weighted resolver
|
||||
pub fn new(default_weight: f32) -> Self {
|
||||
Self { default_weight }
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum magnitude resolver - takes delta with largest L2 norm
|
||||
pub struct MaxMagnitudeResolver;
|
||||
|
||||
impl ConflictResolver<VectorDelta> for MaxMagnitudeResolver {
|
||||
fn resolve(&self, deltas: &[&VectorDelta]) -> Result<VectorDelta> {
|
||||
if deltas.is_empty() {
|
||||
return Err(ConsensusError::InvalidOperation(
|
||||
"No deltas to resolve".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let max_delta = deltas
|
||||
.iter()
|
||||
.max_by(|a, b| {
|
||||
a.l2_norm()
|
||||
.partial_cmp(&b.l2_norm())
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Ok((*max_delta).clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimum magnitude resolver - takes delta with smallest L2 norm
|
||||
pub struct MinMagnitudeResolver;
|
||||
|
||||
impl ConflictResolver<VectorDelta> for MinMagnitudeResolver {
|
||||
fn resolve(&self, deltas: &[&VectorDelta]) -> Result<VectorDelta> {
|
||||
if deltas.is_empty() {
|
||||
return Err(ConsensusError::InvalidOperation(
|
||||
"No deltas to resolve".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let min_delta = deltas
|
||||
.iter()
|
||||
.min_by(|a, b| {
|
||||
a.l2_norm()
|
||||
.partial_cmp(&b.l2_norm())
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Ok((*min_delta).clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Clipped merge resolver - merges and clips to range
|
||||
pub struct ClippedMergeResolver {
|
||||
/// Minimum value
|
||||
pub min: f32,
|
||||
/// Maximum value
|
||||
pub max: f32,
|
||||
}
|
||||
|
||||
impl ClippedMergeResolver {
|
||||
/// Create new clipped resolver
|
||||
pub fn new(min: f32, max: f32) -> Self {
|
||||
Self { min, max }
|
||||
}
|
||||
}
|
||||
|
||||
impl ConflictResolver<VectorDelta> for ClippedMergeResolver {
|
||||
fn resolve(&self, deltas: &[&VectorDelta]) -> Result<VectorDelta> {
|
||||
let merge = MergeResolver::new();
|
||||
let merged = merge.resolve(deltas)?;
|
||||
Ok(merged.clip(self.min, self.max))
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve by sparsity - prefer sparser deltas
|
||||
pub struct SparsityResolver;
|
||||
|
||||
impl ConflictResolver<VectorDelta> for SparsityResolver {
|
||||
fn resolve(&self, deltas: &[&VectorDelta]) -> Result<VectorDelta> {
|
||||
if deltas.is_empty() {
|
||||
return Err(ConsensusError::InvalidOperation(
|
||||
"No deltas to resolve".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Take the sparsest delta
|
||||
let sparsest = deltas.iter().min_by_key(|d| d.value.nnz()).unwrap();
|
||||
|
||||
Ok((*sparsest).clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_last_write_wins() {
|
||||
let d1 = VectorDelta::from_dense(vec![1.0, 0.0, 0.0]);
|
||||
let d2 = VectorDelta::from_dense(vec![0.0, 1.0, 0.0]);
|
||||
let d3 = VectorDelta::from_dense(vec![0.0, 0.0, 1.0]);
|
||||
|
||||
let resolver = LastWriteWinsResolver;
|
||||
let result = resolver.resolve(&[&d1, &d2, &d3]).unwrap();
|
||||
|
||||
// Should return d3 (last)
|
||||
assert_eq!(result.dimensions, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first_write_wins() {
|
||||
let d1 = VectorDelta::from_dense(vec![1.0, 0.0, 0.0]);
|
||||
let d2 = VectorDelta::from_dense(vec![0.0, 1.0, 0.0]);
|
||||
|
||||
let resolver = FirstWriteWinsResolver;
|
||||
let result = resolver.resolve(&[&d1, &d2]).unwrap();
|
||||
|
||||
// Should return d1 (first)
|
||||
assert_eq!(result.dimensions, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_resolver() {
|
||||
let d1 = VectorDelta::from_dense(vec![1.0, 0.0, 0.0]);
|
||||
let d2 = VectorDelta::from_dense(vec![0.0, 1.0, 0.0]);
|
||||
|
||||
let resolver = MergeResolver::new();
|
||||
let result = resolver.resolve(&[&d1, &d2]).unwrap();
|
||||
|
||||
// Should compose both deltas
|
||||
assert!(!result.is_identity());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_magnitude() {
|
||||
let small = VectorDelta::from_dense(vec![0.1, 0.1, 0.1]);
|
||||
let large = VectorDelta::from_dense(vec![1.0, 1.0, 1.0]);
|
||||
|
||||
let resolver = MaxMagnitudeResolver;
|
||||
let result = resolver.resolve(&[&small, &large]).unwrap();
|
||||
|
||||
// Should return the larger delta
|
||||
assert!(result.l2_norm() > 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipped_merge() {
|
||||
let d1 = VectorDelta::from_dense(vec![10.0, -10.0]);
|
||||
let d2 = VectorDelta::from_dense(vec![5.0, -5.0]);
|
||||
|
||||
let resolver = ClippedMergeResolver::new(-1.0, 1.0);
|
||||
let result = resolver.resolve(&[&d1, &d2]).unwrap();
|
||||
|
||||
// Values should be clipped to [-1, 1]
|
||||
if let ruvector_delta_core::DeltaValue::Dense(values) = &result.value {
|
||||
assert!(values[0] <= 1.0);
|
||||
assert!(values[1] >= -1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
485
vendor/ruvector/crates/ruvector-delta-consensus/src/crdt.rs
vendored
Normal file
485
vendor/ruvector/crates/ruvector-delta-consensus/src/crdt.rs
vendored
Normal file
@@ -0,0 +1,485 @@
|
||||
//! CRDT implementations for delta-based replication
|
||||
//!
|
||||
//! Conflict-free Replicated Data Types that can be used with delta propagation.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::hash::Hash;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ReplicaId;
|
||||
|
||||
/// Trait for delta-based CRDTs
|
||||
pub trait DeltaCrdt: Clone + Send + Sync {
|
||||
/// The delta type for this CRDT
|
||||
type Delta: Clone + Send + Sync;
|
||||
|
||||
/// Get the current delta (changes since last sync)
|
||||
fn delta(&self) -> Self::Delta;
|
||||
|
||||
/// Apply a delta from another replica
|
||||
fn apply_delta(&mut self, delta: &Self::Delta);
|
||||
|
||||
/// Merge with another CRDT state
|
||||
fn merge(&mut self, other: &Self);
|
||||
|
||||
/// Clear the delta accumulator
|
||||
fn clear_delta(&mut self);
|
||||
}
|
||||
|
||||
/// G-Counter (Grow-only counter)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GCounter {
|
||||
/// Per-replica counts
|
||||
counts: HashMap<ReplicaId, u64>,
|
||||
/// Delta since last sync
|
||||
delta: HashMap<ReplicaId, u64>,
|
||||
}
|
||||
|
||||
impl GCounter {
|
||||
/// Create a new G-Counter
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
counts: HashMap::new(),
|
||||
delta: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment the counter for a replica
|
||||
pub fn increment(&mut self, replica: &ReplicaId) {
|
||||
let count = self.counts.entry(replica.clone()).or_insert(0);
|
||||
*count += 1;
|
||||
|
||||
// Track delta
|
||||
let delta_count = self.delta.entry(replica.clone()).or_insert(0);
|
||||
*delta_count += 1;
|
||||
}
|
||||
|
||||
/// Get the current value
|
||||
pub fn value(&self) -> u64 {
|
||||
self.counts.values().sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GCounter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DeltaCrdt for GCounter {
|
||||
type Delta = HashMap<ReplicaId, u64>;
|
||||
|
||||
fn delta(&self) -> Self::Delta {
|
||||
self.delta.clone()
|
||||
}
|
||||
|
||||
fn apply_delta(&mut self, delta: &Self::Delta) {
|
||||
for (replica, &count) in delta {
|
||||
let current = self.counts.entry(replica.clone()).or_insert(0);
|
||||
*current = (*current).max(count);
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &Self) {
|
||||
for (replica, &count) in &other.counts {
|
||||
let current = self.counts.entry(replica.clone()).or_insert(0);
|
||||
*current = (*current).max(count);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_delta(&mut self) {
|
||||
self.delta.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// PN-Counter (Positive-Negative counter)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PNCounter {
|
||||
/// Positive counts
|
||||
positive: GCounter,
|
||||
/// Negative counts
|
||||
negative: GCounter,
|
||||
}
|
||||
|
||||
impl PNCounter {
|
||||
/// Create a new PN-Counter
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
positive: GCounter::new(),
|
||||
negative: GCounter::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment the counter
|
||||
pub fn increment(&mut self, replica: &ReplicaId) {
|
||||
self.positive.increment(replica);
|
||||
}
|
||||
|
||||
/// Decrement the counter
|
||||
pub fn decrement(&mut self, replica: &ReplicaId) {
|
||||
self.negative.increment(replica);
|
||||
}
|
||||
|
||||
/// Get the current value
|
||||
pub fn value(&self) -> i64 {
|
||||
self.positive.value() as i64 - self.negative.value() as i64
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PNCounter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Delta for PN-Counter
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PNCounterDelta {
|
||||
positive: HashMap<ReplicaId, u64>,
|
||||
negative: HashMap<ReplicaId, u64>,
|
||||
}
|
||||
|
||||
impl DeltaCrdt for PNCounter {
|
||||
type Delta = PNCounterDelta;
|
||||
|
||||
fn delta(&self) -> Self::Delta {
|
||||
PNCounterDelta {
|
||||
positive: self.positive.delta(),
|
||||
negative: self.negative.delta(),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_delta(&mut self, delta: &Self::Delta) {
|
||||
self.positive.apply_delta(&delta.positive);
|
||||
self.negative.apply_delta(&delta.negative);
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &Self) {
|
||||
self.positive.merge(&other.positive);
|
||||
self.negative.merge(&other.negative);
|
||||
}
|
||||
|
||||
fn clear_delta(&mut self) {
|
||||
self.positive.clear_delta();
|
||||
self.negative.clear_delta();
|
||||
}
|
||||
}
|
||||
|
||||
/// LWW-Register (Last-Writer-Wins Register)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LWWRegister<T: Clone> {
|
||||
/// Current value
|
||||
value: Option<T>,
|
||||
/// Timestamp of last write
|
||||
timestamp: u64,
|
||||
/// Replica that made last write
|
||||
replica: ReplicaId,
|
||||
/// Delta (new write)
|
||||
delta: Option<(T, u64, ReplicaId)>,
|
||||
}
|
||||
|
||||
impl<T: Clone> LWWRegister<T> {
|
||||
/// Create a new register
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
value: None,
|
||||
timestamp: 0,
|
||||
replica: String::new(),
|
||||
delta: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the value
|
||||
pub fn set(&mut self, value: T, timestamp: u64, replica: ReplicaId) {
|
||||
if timestamp > self.timestamp || (timestamp == self.timestamp && replica > self.replica) {
|
||||
self.delta = Some((value.clone(), timestamp, replica.clone()));
|
||||
self.value = Some(value);
|
||||
self.timestamp = timestamp;
|
||||
self.replica = replica;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current value
|
||||
pub fn get(&self) -> Option<&T> {
|
||||
self.value.as_ref()
|
||||
}
|
||||
|
||||
/// Get the timestamp
|
||||
pub fn timestamp(&self) -> u64 {
|
||||
self.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Default for LWWRegister<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Delta for LWW-Register
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LWWRegisterDelta<T: Clone> {
|
||||
pub value: Option<T>,
|
||||
pub timestamp: u64,
|
||||
pub replica: ReplicaId,
|
||||
}
|
||||
|
||||
impl<T: Clone + Send + Sync> DeltaCrdt for LWWRegister<T> {
|
||||
type Delta = LWWRegisterDelta<T>;
|
||||
|
||||
fn delta(&self) -> Self::Delta {
|
||||
if let Some((value, timestamp, replica)) = &self.delta {
|
||||
LWWRegisterDelta {
|
||||
value: Some(value.clone()),
|
||||
timestamp: *timestamp,
|
||||
replica: replica.clone(),
|
||||
}
|
||||
} else {
|
||||
LWWRegisterDelta {
|
||||
value: None,
|
||||
timestamp: 0,
|
||||
replica: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_delta(&mut self, delta: &Self::Delta) {
|
||||
if let Some(value) = &delta.value {
|
||||
self.set(value.clone(), delta.timestamp, delta.replica.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &Self) {
|
||||
if let Some(value) = &other.value {
|
||||
self.set(value.clone(), other.timestamp, other.replica.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_delta(&mut self) {
|
||||
self.delta = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// OR-Set (Observed-Remove Set)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ORSet<T: Clone + Eq + Hash> {
|
||||
/// Elements with their unique tags
|
||||
elements: HashMap<T, HashSet<String>>,
|
||||
/// Tombstones (removed tags)
|
||||
tombstones: HashSet<String>,
|
||||
/// Delta additions
|
||||
delta_adds: Vec<(T, String)>,
|
||||
/// Delta removes
|
||||
delta_removes: Vec<String>,
|
||||
}
|
||||
|
||||
impl<T: Clone + Eq + Hash> ORSet<T> {
|
||||
/// Create a new OR-Set
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
elements: HashMap::new(),
|
||||
tombstones: HashSet::new(),
|
||||
delta_adds: Vec::new(),
|
||||
delta_removes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an element
|
||||
pub fn add(&mut self, element: T, replica: &ReplicaId) {
|
||||
let tag = format!("{}:{}", replica, uuid::Uuid::new_v4());
|
||||
self.elements
|
||||
.entry(element.clone())
|
||||
.or_insert_with(HashSet::new)
|
||||
.insert(tag.clone());
|
||||
self.delta_adds.push((element, tag));
|
||||
}
|
||||
|
||||
/// Remove an element (all its tags)
|
||||
pub fn remove(&mut self, element: &T) {
|
||||
if let Some(tags) = self.elements.remove(element) {
|
||||
for tag in tags {
|
||||
self.tombstones.insert(tag.clone());
|
||||
self.delta_removes.push(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if element is in set
|
||||
pub fn contains(&self, element: &T) -> bool {
|
||||
if let Some(tags) = self.elements.get(element) {
|
||||
!tags.is_empty()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all elements
|
||||
pub fn elements(&self) -> Vec<&T> {
|
||||
self.elements
|
||||
.iter()
|
||||
.filter(|(_, tags)| !tags.is_empty())
|
||||
.map(|(e, _)| e)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get size
|
||||
pub fn len(&self) -> usize {
|
||||
self.elements
|
||||
.iter()
|
||||
.filter(|(_, tags)| !tags.is_empty())
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Check if empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Eq + Hash> Default for ORSet<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Delta for OR-Set
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ORSetDelta<T: Clone> {
|
||||
pub adds: Vec<(T, String)>,
|
||||
pub removes: Vec<String>,
|
||||
}
|
||||
|
||||
impl<T: Clone + Eq + Hash + Send + Sync> DeltaCrdt for ORSet<T> {
|
||||
type Delta = ORSetDelta<T>;
|
||||
|
||||
fn delta(&self) -> Self::Delta {
|
||||
ORSetDelta {
|
||||
adds: self.delta_adds.clone(),
|
||||
removes: self.delta_removes.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_delta(&mut self, delta: &Self::Delta) {
|
||||
// Apply removes first
|
||||
for tag in &delta.removes {
|
||||
self.tombstones.insert(tag.clone());
|
||||
for tags in self.elements.values_mut() {
|
||||
tags.remove(tag);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply adds (if not tombstoned)
|
||||
for (element, tag) in &delta.adds {
|
||||
if !self.tombstones.contains(tag) {
|
||||
self.elements
|
||||
.entry(element.clone())
|
||||
.or_insert_with(HashSet::new)
|
||||
.insert(tag.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &Self) {
|
||||
// Merge tombstones
|
||||
for tag in &other.tombstones {
|
||||
self.tombstones.insert(tag.clone());
|
||||
for tags in self.elements.values_mut() {
|
||||
tags.remove(tag);
|
||||
}
|
||||
}
|
||||
|
||||
// Merge elements
|
||||
for (element, other_tags) in &other.elements {
|
||||
let tags = self
|
||||
.elements
|
||||
.entry(element.clone())
|
||||
.or_insert_with(HashSet::new);
|
||||
for tag in other_tags {
|
||||
if !self.tombstones.contains(tag) {
|
||||
tags.insert(tag.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_delta(&mut self) {
|
||||
self.delta_adds.clear();
|
||||
self.delta_removes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_gcounter() {
|
||||
let mut c1 = GCounter::new();
|
||||
let mut c2 = GCounter::new();
|
||||
|
||||
c1.increment(&"r1".to_string());
|
||||
c1.increment(&"r1".to_string());
|
||||
c2.increment(&"r2".to_string());
|
||||
|
||||
c1.merge(&c2);
|
||||
|
||||
assert_eq!(c1.value(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pncounter() {
|
||||
let mut c = PNCounter::new();
|
||||
|
||||
c.increment(&"r1".to_string());
|
||||
c.increment(&"r1".to_string());
|
||||
c.decrement(&"r1".to_string());
|
||||
|
||||
assert_eq!(c.value(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lww_register() {
|
||||
let mut r1 = LWWRegister::new();
|
||||
let mut r2 = LWWRegister::new();
|
||||
|
||||
r1.set("value1".to_string(), 1, "r1".to_string());
|
||||
r2.set("value2".to_string(), 2, "r2".to_string());
|
||||
|
||||
r1.merge(&r2);
|
||||
|
||||
assert_eq!(r1.get(), Some(&"value2".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_orset() {
|
||||
let mut s1 = ORSet::new();
|
||||
let mut s2 = ORSet::new();
|
||||
|
||||
s1.add("a", &"r1".to_string());
|
||||
s1.add("b", &"r1".to_string());
|
||||
s2.add("c", &"r2".to_string());
|
||||
|
||||
s1.merge(&s2);
|
||||
|
||||
assert!(s1.contains(&"a"));
|
||||
assert!(s1.contains(&"b"));
|
||||
assert!(s1.contains(&"c"));
|
||||
assert_eq!(s1.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_orset_remove() {
|
||||
let mut s1 = ORSet::new();
|
||||
let mut s2 = ORSet::new();
|
||||
|
||||
s1.add("a", &"r1".to_string());
|
||||
s2.merge(&s1);
|
||||
|
||||
s1.remove(&"a");
|
||||
|
||||
s2.merge(&s1);
|
||||
|
||||
assert!(!s2.contains(&"a"));
|
||||
}
|
||||
}
|
||||
43
vendor/ruvector/crates/ruvector-delta-consensus/src/error.rs
vendored
Normal file
43
vendor/ruvector/crates/ruvector-delta-consensus/src/error.rs
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
//! Error types for consensus operations
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Result type for consensus operations
|
||||
pub type Result<T> = std::result::Result<T, ConsensusError>;
|
||||
|
||||
/// Errors that can occur during consensus
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ConsensusError {
|
||||
/// Delta error
|
||||
DeltaError(String),
|
||||
/// Causal ordering violation
|
||||
CausalViolation(String),
|
||||
/// Conflict resolution failed
|
||||
ConflictResolutionFailed(String),
|
||||
/// Invalid operation
|
||||
InvalidOperation(String),
|
||||
/// Network error
|
||||
NetworkError(String),
|
||||
/// Timeout
|
||||
Timeout(String),
|
||||
/// Replica not found
|
||||
ReplicaNotFound(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for ConsensusError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::DeltaError(msg) => write!(f, "Delta error: {}", msg),
|
||||
Self::CausalViolation(msg) => write!(f, "Causal violation: {}", msg),
|
||||
Self::ConflictResolutionFailed(msg) => {
|
||||
write!(f, "Conflict resolution failed: {}", msg)
|
||||
}
|
||||
Self::InvalidOperation(msg) => write!(f, "Invalid operation: {}", msg),
|
||||
Self::NetworkError(msg) => write!(f, "Network error: {}", msg),
|
||||
Self::Timeout(msg) => write!(f, "Timeout: {}", msg),
|
||||
Self::ReplicaNotFound(id) => write!(f, "Replica not found: {}", id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ConsensusError {}
|
||||
488
vendor/ruvector/crates/ruvector-delta-consensus/src/lib.rs
vendored
Normal file
488
vendor/ruvector/crates/ruvector-delta-consensus/src/lib.rs
vendored
Normal file
@@ -0,0 +1,488 @@
|
||||
//! # RuVector Delta Consensus
|
||||
//!
|
||||
//! Distributed delta consensus using CRDTs and causal ordering.
|
||||
//! Enables consistent delta application across distributed nodes.
|
||||
//!
|
||||
//! ## Key Features
|
||||
//!
|
||||
//! - CRDT-based delta merging
|
||||
//! - Causal ordering with vector clocks
|
||||
//! - Conflict resolution strategies
|
||||
//! - Delta compression for network transfer
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![warn(clippy::all)]
|
||||
|
||||
pub mod causal;
|
||||
pub mod conflict;
|
||||
pub mod crdt;
|
||||
pub mod error;
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use ruvector_delta_core::{Delta, VectorDelta};
|
||||
|
||||
pub use causal::{CausalOrder, HybridLogicalClock, VectorClock};
|
||||
pub use conflict::{ConflictResolver, ConflictStrategy, MergeResult};
|
||||
pub use crdt::{DeltaCrdt, GCounter, LWWRegister, ORSet, PNCounter};
|
||||
pub use error::{ConsensusError, Result};
|
||||
|
||||
/// A replica identifier
|
||||
pub type ReplicaId = String;
|
||||
|
||||
/// A delta with causal metadata
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CausalDelta {
|
||||
/// Unique delta ID
|
||||
pub id: Uuid,
|
||||
/// The delta data
|
||||
pub delta: VectorDelta,
|
||||
/// Vector clock for causal ordering
|
||||
pub vector_clock: VectorClock,
|
||||
/// Origin replica
|
||||
pub origin: ReplicaId,
|
||||
/// Timestamp (for HLC)
|
||||
pub timestamp: u64,
|
||||
/// Dependencies (delta IDs this depends on)
|
||||
pub dependencies: Vec<Uuid>,
|
||||
}
|
||||
|
||||
impl CausalDelta {
|
||||
/// Create a new causal delta
|
||||
pub fn new(delta: VectorDelta, origin: ReplicaId, clock: VectorClock) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
delta,
|
||||
vector_clock: clock,
|
||||
origin,
|
||||
timestamp: chrono::Utc::now().timestamp_millis() as u64,
|
||||
dependencies: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a dependency
|
||||
pub fn with_dependency(mut self, dep: Uuid) -> Self {
|
||||
self.dependencies.push(dep);
|
||||
self
|
||||
}
|
||||
|
||||
/// Check if this delta is causally before another
|
||||
pub fn is_before(&self, other: &CausalDelta) -> bool {
|
||||
self.vector_clock.happens_before(&other.vector_clock)
|
||||
}
|
||||
|
||||
/// Check if deltas are concurrent
|
||||
pub fn is_concurrent(&self, other: &CausalDelta) -> bool {
|
||||
self.vector_clock.is_concurrent(&other.vector_clock)
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for consensus
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConsensusConfig {
|
||||
/// This replica's ID
|
||||
pub replica_id: ReplicaId,
|
||||
/// Conflict resolution strategy
|
||||
pub conflict_strategy: ConflictStrategy,
|
||||
/// Maximum pending deltas before compaction
|
||||
pub max_pending: usize,
|
||||
/// Whether to enable causal delivery
|
||||
pub causal_delivery: bool,
|
||||
}
|
||||
|
||||
impl Default for ConsensusConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
replica_id: Uuid::new_v4().to_string(),
|
||||
conflict_strategy: ConflictStrategy::LastWriteWins,
|
||||
max_pending: 1000,
|
||||
causal_delivery: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delta consensus coordinator
|
||||
pub struct DeltaConsensus {
|
||||
config: ConsensusConfig,
|
||||
/// Current vector clock
|
||||
clock: RwLock<VectorClock>,
|
||||
/// Pending deltas awaiting delivery
|
||||
pending: RwLock<HashMap<Uuid, CausalDelta>>,
|
||||
/// Applied delta IDs
|
||||
applied: RwLock<HashSet<Uuid>>,
|
||||
/// Conflict resolver
|
||||
resolver: Box<dyn ConflictResolver<VectorDelta> + Send + Sync>,
|
||||
}
|
||||
|
||||
impl DeltaConsensus {
|
||||
/// Create a new consensus coordinator
|
||||
pub fn new(config: ConsensusConfig) -> Self {
|
||||
let resolver: Box<dyn ConflictResolver<VectorDelta> + Send + Sync> =
|
||||
match config.conflict_strategy {
|
||||
ConflictStrategy::LastWriteWins => Box::new(conflict::LastWriteWinsResolver),
|
||||
ConflictStrategy::FirstWriteWins => Box::new(conflict::FirstWriteWinsResolver),
|
||||
ConflictStrategy::Merge => Box::new(conflict::MergeResolver::default()),
|
||||
ConflictStrategy::Custom => Box::new(conflict::MergeResolver::default()),
|
||||
};
|
||||
|
||||
Self {
|
||||
config,
|
||||
clock: RwLock::new(VectorClock::new()),
|
||||
pending: RwLock::new(HashMap::new()),
|
||||
applied: RwLock::new(HashSet::new()),
|
||||
resolver,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current replica ID
|
||||
pub fn replica_id(&self) -> &ReplicaId {
|
||||
&self.config.replica_id
|
||||
}
|
||||
|
||||
/// Create a new local delta with causal metadata
|
||||
pub fn create_delta(&self, delta: VectorDelta) -> CausalDelta {
|
||||
let mut clock = self.clock.write();
|
||||
clock.increment(&self.config.replica_id);
|
||||
|
||||
CausalDelta::new(delta, self.config.replica_id.clone(), clock.clone())
|
||||
}
|
||||
|
||||
/// Receive a delta from another replica
|
||||
pub fn receive(&self, delta: CausalDelta) -> Result<DeliveryStatus> {
|
||||
// Check if already applied
|
||||
if self.applied.read().contains(&delta.id) {
|
||||
return Ok(DeliveryStatus::AlreadyApplied);
|
||||
}
|
||||
|
||||
// Check causal dependencies
|
||||
if self.config.causal_delivery {
|
||||
if !self.dependencies_satisfied(&delta) {
|
||||
// Queue for later delivery
|
||||
self.pending.write().insert(delta.id, delta);
|
||||
return Ok(DeliveryStatus::Pending);
|
||||
}
|
||||
}
|
||||
|
||||
// Update vector clock
|
||||
{
|
||||
let mut clock = self.clock.write();
|
||||
clock.merge(&delta.vector_clock);
|
||||
clock.increment(&self.config.replica_id);
|
||||
}
|
||||
|
||||
// Mark as applied
|
||||
self.applied.write().insert(delta.id);
|
||||
|
||||
// Try to deliver pending deltas
|
||||
self.try_deliver_pending()?;
|
||||
|
||||
Ok(DeliveryStatus::Delivered)
|
||||
}
|
||||
|
||||
/// Apply delta to a base vector, handling conflicts
|
||||
pub fn apply_with_consensus(
|
||||
&self,
|
||||
delta: &CausalDelta,
|
||||
base: &mut Vec<f32>,
|
||||
concurrent_deltas: &[CausalDelta],
|
||||
) -> Result<()> {
|
||||
if concurrent_deltas.is_empty() {
|
||||
// No conflicts, apply directly
|
||||
delta
|
||||
.delta
|
||||
.apply(base)
|
||||
.map_err(|e| ConsensusError::DeltaError(format!("{:?}", e)))?;
|
||||
} else {
|
||||
// Resolve conflicts
|
||||
let mut all_deltas: Vec<&CausalDelta> = vec![delta];
|
||||
all_deltas.extend(concurrent_deltas);
|
||||
|
||||
let resolved = self.resolve_conflicts(&all_deltas)?;
|
||||
resolved
|
||||
.apply(base)
|
||||
.map_err(|e| ConsensusError::DeltaError(format!("{:?}", e)))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all pending deltas
|
||||
pub fn pending_deltas(&self) -> Vec<CausalDelta> {
|
||||
self.pending.read().values().cloned().collect()
|
||||
}
|
||||
|
||||
/// Get number of pending deltas
|
||||
pub fn pending_count(&self) -> usize {
|
||||
self.pending.read().len()
|
||||
}
|
||||
|
||||
/// Get current vector clock
|
||||
pub fn current_clock(&self) -> VectorClock {
|
||||
self.clock.read().clone()
|
||||
}
|
||||
|
||||
/// Clear applied history (for memory management)
|
||||
pub fn clear_history(&self) {
|
||||
self.applied.write().clear();
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
fn dependencies_satisfied(&self, delta: &CausalDelta) -> bool {
|
||||
let applied = self.applied.read();
|
||||
|
||||
for dep in &delta.dependencies {
|
||||
if !applied.contains(dep) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn try_deliver_pending(&self) -> Result<usize> {
|
||||
let mut delivered = 0;
|
||||
|
||||
loop {
|
||||
let pending = self.pending.read();
|
||||
let ready: Vec<Uuid> = pending
|
||||
.iter()
|
||||
.filter(|(_, d)| self.dependencies_satisfied(d))
|
||||
.map(|(id, _)| *id)
|
||||
.collect();
|
||||
drop(pending);
|
||||
|
||||
if ready.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for id in ready {
|
||||
if let Some(delta) = self.pending.write().remove(&id) {
|
||||
// Update clock
|
||||
{
|
||||
let mut clock = self.clock.write();
|
||||
clock.merge(&delta.vector_clock);
|
||||
clock.increment(&self.config.replica_id);
|
||||
}
|
||||
|
||||
self.applied.write().insert(id);
|
||||
delivered += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(delivered)
|
||||
}
|
||||
|
||||
fn resolve_conflicts(&self, deltas: &[&CausalDelta]) -> Result<VectorDelta> {
|
||||
if deltas.is_empty() {
|
||||
return Err(ConsensusError::InvalidOperation(
|
||||
"No deltas to resolve".into(),
|
||||
));
|
||||
}
|
||||
|
||||
if deltas.len() == 1 {
|
||||
return Ok(deltas[0].delta.clone());
|
||||
}
|
||||
|
||||
// Sort by timestamp for deterministic resolution
|
||||
let mut sorted: Vec<_> = deltas.iter().collect();
|
||||
sorted.sort_by_key(|d| d.timestamp);
|
||||
|
||||
// Use resolver
|
||||
let delta_refs: Vec<_> = sorted.iter().map(|d| &d.delta).collect();
|
||||
self.resolver.resolve(&delta_refs)
|
||||
}
|
||||
}
|
||||
|
||||
/// Status of delta delivery
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DeliveryStatus {
|
||||
/// Delta was delivered successfully
|
||||
Delivered,
|
||||
/// Delta is pending (waiting for dependencies)
|
||||
Pending,
|
||||
/// Delta was already applied
|
||||
AlreadyApplied,
|
||||
/// Delta was rejected
|
||||
Rejected,
|
||||
}
|
||||
|
||||
/// Gossip protocol for delta dissemination
|
||||
pub struct DeltaGossip {
|
||||
consensus: Arc<DeltaConsensus>,
|
||||
/// Known peers
|
||||
peers: RwLock<HashSet<ReplicaId>>,
|
||||
/// Deltas to send
|
||||
outbox: RwLock<Vec<CausalDelta>>,
|
||||
}
|
||||
|
||||
impl DeltaGossip {
|
||||
/// Create new gossip protocol
|
||||
pub fn new(consensus: Arc<DeltaConsensus>) -> Self {
|
||||
Self {
|
||||
consensus,
|
||||
peers: RwLock::new(HashSet::new()),
|
||||
outbox: RwLock::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a peer
|
||||
pub fn add_peer(&self, peer: ReplicaId) {
|
||||
self.peers.write().insert(peer);
|
||||
}
|
||||
|
||||
/// Remove a peer
|
||||
pub fn remove_peer(&self, peer: &ReplicaId) {
|
||||
self.peers.write().remove(peer);
|
||||
}
|
||||
|
||||
/// Queue delta for gossip
|
||||
pub fn broadcast(&self, delta: CausalDelta) {
|
||||
self.outbox.write().push(delta);
|
||||
}
|
||||
|
||||
/// Get deltas to send
|
||||
pub fn get_outbox(&self) -> Vec<CausalDelta> {
|
||||
let mut outbox = self.outbox.write();
|
||||
std::mem::take(&mut *outbox)
|
||||
}
|
||||
|
||||
/// Receive gossip from peer
|
||||
pub fn receive_gossip(&self, deltas: Vec<CausalDelta>) -> Result<GossipResult> {
|
||||
let mut delivered = 0;
|
||||
let mut pending = 0;
|
||||
let mut already_applied = 0;
|
||||
|
||||
for delta in deltas {
|
||||
match self.consensus.receive(delta)? {
|
||||
DeliveryStatus::Delivered => delivered += 1,
|
||||
DeliveryStatus::Pending => pending += 1,
|
||||
DeliveryStatus::AlreadyApplied => already_applied += 1,
|
||||
DeliveryStatus::Rejected => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(GossipResult {
|
||||
delivered,
|
||||
pending,
|
||||
already_applied,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get anti-entropy summary (for sync)
|
||||
pub fn get_summary(&self) -> GossipSummary {
|
||||
GossipSummary {
|
||||
replica_id: self.consensus.replica_id().clone(),
|
||||
clock: self.consensus.current_clock(),
|
||||
pending_count: self.consensus.pending_count(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of gossip receive
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GossipResult {
|
||||
/// Deltas delivered
|
||||
pub delivered: usize,
|
||||
/// Deltas pending
|
||||
pub pending: usize,
|
||||
/// Deltas already applied
|
||||
pub already_applied: usize,
|
||||
}
|
||||
|
||||
/// Summary for anti-entropy
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GossipSummary {
|
||||
/// Replica ID
|
||||
pub replica_id: ReplicaId,
|
||||
/// Current vector clock
|
||||
pub clock: VectorClock,
|
||||
/// Number of pending deltas
|
||||
pub pending_count: usize,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_delta() {
|
||||
let config = ConsensusConfig {
|
||||
replica_id: "replica1".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let consensus = DeltaConsensus::new(config);
|
||||
let delta = VectorDelta::from_dense(vec![1.0, 2.0, 3.0]);
|
||||
let causal = consensus.create_delta(delta);
|
||||
|
||||
assert_eq!(causal.origin, "replica1");
|
||||
assert!(!causal.id.is_nil());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_receive_delta() {
|
||||
let config = ConsensusConfig {
|
||||
replica_id: "replica1".to_string(),
|
||||
causal_delivery: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let consensus = DeltaConsensus::new(config);
|
||||
|
||||
let delta = VectorDelta::from_dense(vec![1.0, 2.0, 3.0]);
|
||||
let causal = CausalDelta::new(delta, "replica2".to_string(), VectorClock::new());
|
||||
|
||||
let status = consensus.receive(causal).unwrap();
|
||||
assert_eq!(status, DeliveryStatus::Delivered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_causal_ordering() {
|
||||
let clock1 = {
|
||||
let mut c = VectorClock::new();
|
||||
c.increment("r1");
|
||||
c
|
||||
};
|
||||
|
||||
let clock2 = {
|
||||
let mut c = clock1.clone();
|
||||
c.increment("r1");
|
||||
c
|
||||
};
|
||||
|
||||
let d1 = CausalDelta::new(VectorDelta::from_dense(vec![1.0]), "r1".to_string(), clock1);
|
||||
|
||||
let d2 = CausalDelta::new(VectorDelta::from_dense(vec![2.0]), "r1".to_string(), clock2);
|
||||
|
||||
assert!(d1.is_before(&d2));
|
||||
assert!(!d2.is_before(&d1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_deltas() {
|
||||
let clock1 = {
|
||||
let mut c = VectorClock::new();
|
||||
c.increment("r1");
|
||||
c
|
||||
};
|
||||
|
||||
let clock2 = {
|
||||
let mut c = VectorClock::new();
|
||||
c.increment("r2");
|
||||
c
|
||||
};
|
||||
|
||||
let d1 = CausalDelta::new(VectorDelta::from_dense(vec![1.0]), "r1".to_string(), clock1);
|
||||
|
||||
let d2 = CausalDelta::new(VectorDelta::from_dense(vec![2.0]), "r2".to_string(), clock2);
|
||||
|
||||
assert!(d1.is_concurrent(&d2));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user