489 lines
14 KiB
Rust
489 lines
14 KiB
Rust
//! # 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));
|
|
}
|
|
}
|