Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
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