Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,388 @@
//! Adapter to ruvector-raft
//!
//! Wraps Raft consensus for coherence state replication.
use super::config::NodeRole;
use super::{DistributedCoherenceConfig, DistributedError, Result};
/// Command types for coherence state machine
#[derive(Debug, Clone)]
pub enum CoherenceCommand {
/// Update energy for an edge
UpdateEnergy { edge_id: (u64, u64), energy: f32 },
/// Set node state vector
SetNodeState { node_id: u64, state: Vec<f32> },
/// Record coherence checkpoint
Checkpoint { total_energy: f32, timestamp: u64 },
/// Mark region as incoherent
MarkIncoherent { region_id: u64, nodes: Vec<u64> },
/// Clear incoherence flag
ClearIncoherent { region_id: u64 },
}
impl CoherenceCommand {
/// Serialize command to bytes
pub fn to_bytes(&self) -> Vec<u8> {
// Simple serialization format
let mut bytes = Vec::new();
match self {
Self::UpdateEnergy { edge_id, energy } => {
bytes.push(0);
bytes.extend(edge_id.0.to_le_bytes());
bytes.extend(edge_id.1.to_le_bytes());
bytes.extend(energy.to_le_bytes());
}
Self::SetNodeState { node_id, state } => {
bytes.push(1);
bytes.extend(node_id.to_le_bytes());
bytes.extend((state.len() as u32).to_le_bytes());
for &v in state {
bytes.extend(v.to_le_bytes());
}
}
Self::Checkpoint {
total_energy,
timestamp,
} => {
bytes.push(2);
bytes.extend(total_energy.to_le_bytes());
bytes.extend(timestamp.to_le_bytes());
}
Self::MarkIncoherent { region_id, nodes } => {
bytes.push(3);
bytes.extend(region_id.to_le_bytes());
bytes.extend((nodes.len() as u32).to_le_bytes());
for &n in nodes {
bytes.extend(n.to_le_bytes());
}
}
Self::ClearIncoherent { region_id } => {
bytes.push(4);
bytes.extend(region_id.to_le_bytes());
}
}
bytes
}
/// Deserialize command from bytes
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
if bytes.is_empty() {
return None;
}
let cmd_type = bytes[0];
let data = &bytes[1..];
match cmd_type {
0 if data.len() >= 20 => {
let src = u64::from_le_bytes(data[0..8].try_into().ok()?);
let dst = u64::from_le_bytes(data[8..16].try_into().ok()?);
let energy = f32::from_le_bytes(data[16..20].try_into().ok()?);
Some(Self::UpdateEnergy {
edge_id: (src, dst),
energy,
})
}
1 if data.len() >= 12 => {
let node_id = u64::from_le_bytes(data[0..8].try_into().ok()?);
let len = u32::from_le_bytes(data[8..12].try_into().ok()?) as usize;
if data.len() < 12 + len * 4 {
return None;
}
let state: Vec<f32> = (0..len)
.map(|i| {
let offset = 12 + i * 4;
f32::from_le_bytes(data[offset..offset + 4].try_into().unwrap())
})
.collect();
Some(Self::SetNodeState { node_id, state })
}
2 if data.len() >= 12 => {
let total_energy = f32::from_le_bytes(data[0..4].try_into().ok()?);
let timestamp = u64::from_le_bytes(data[4..12].try_into().ok()?);
Some(Self::Checkpoint {
total_energy,
timestamp,
})
}
3 if data.len() >= 12 => {
let region_id = u64::from_le_bytes(data[0..8].try_into().ok()?);
let len = u32::from_le_bytes(data[8..12].try_into().ok()?) as usize;
if data.len() < 12 + len * 8 {
return None;
}
let nodes: Vec<u64> = (0..len)
.map(|i| {
let offset = 12 + i * 8;
u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap())
})
.collect();
Some(Self::MarkIncoherent { region_id, nodes })
}
4 if data.len() >= 8 => {
let region_id = u64::from_le_bytes(data[0..8].try_into().ok()?);
Some(Self::ClearIncoherent { region_id })
}
_ => None,
}
}
}
/// Result of applying a command
#[derive(Debug, Clone)]
pub struct CommandResult {
/// Log index where command was applied
pub index: u64,
/// Term when command was applied
pub term: u64,
/// Whether command was successful
pub success: bool,
}
/// Adapter wrapping ruvector-raft for coherence coordination
#[derive(Debug)]
pub struct RaftAdapter {
/// Configuration
config: DistributedCoherenceConfig,
/// Current role (simulated without actual Raft)
role: NodeRole,
/// Current term
current_term: u64,
/// Current leader ID
current_leader: Option<String>,
/// Log index
log_index: u64,
/// Pending commands (for simulation)
pending_commands: Vec<CoherenceCommand>,
}
impl RaftAdapter {
/// Create a new Raft adapter
pub fn new(config: DistributedCoherenceConfig) -> Self {
let is_leader = config.is_single_node();
Self {
role: if is_leader {
NodeRole::Leader
} else {
NodeRole::Follower
},
current_term: 1,
current_leader: if is_leader {
Some(config.node_id.clone())
} else {
None
},
log_index: 0,
pending_commands: Vec::new(),
config,
}
}
/// Get current role
pub fn role(&self) -> NodeRole {
self.role
}
/// Get current term
pub fn current_term(&self) -> u64 {
self.current_term
}
/// Get current leader
pub fn current_leader(&self) -> Option<&str> {
self.current_leader.as_deref()
}
/// Check if this node is the leader
pub fn is_leader(&self) -> bool {
self.role.is_leader()
}
/// Submit a command for replication
pub fn submit_command(&mut self, command: CoherenceCommand) -> Result<CommandResult> {
if !self.is_leader() {
return Err(DistributedError::NotLeader {
leader: self.current_leader.clone(),
});
}
// In a real implementation, this would go through Raft
self.log_index += 1;
self.pending_commands.push(command);
Ok(CommandResult {
index: self.log_index,
term: self.current_term,
success: true,
})
}
/// Update energy for an edge
pub fn update_energy(&mut self, edge_id: (u64, u64), energy: f32) -> Result<CommandResult> {
let command = CoherenceCommand::UpdateEnergy { edge_id, energy };
self.submit_command(command)
}
/// Set node state
pub fn set_node_state(&mut self, node_id: u64, state: Vec<f32>) -> Result<CommandResult> {
let command = CoherenceCommand::SetNodeState { node_id, state };
self.submit_command(command)
}
/// Record checkpoint
pub fn checkpoint(&mut self, total_energy: f32, timestamp: u64) -> Result<CommandResult> {
let command = CoherenceCommand::Checkpoint {
total_energy,
timestamp,
};
self.submit_command(command)
}
/// Mark region as incoherent
pub fn mark_incoherent(&mut self, region_id: u64, nodes: Vec<u64>) -> Result<CommandResult> {
let command = CoherenceCommand::MarkIncoherent { region_id, nodes };
self.submit_command(command)
}
/// Clear incoherence flag
pub fn clear_incoherent(&mut self, region_id: u64) -> Result<CommandResult> {
let command = CoherenceCommand::ClearIncoherent { region_id };
self.submit_command(command)
}
/// Get pending commands (for state machine application)
pub fn take_pending_commands(&mut self) -> Vec<CoherenceCommand> {
std::mem::take(&mut self.pending_commands)
}
/// Simulate leader election (for testing)
pub fn become_leader(&mut self) {
self.role = NodeRole::Leader;
self.current_term += 1;
self.current_leader = Some(self.config.node_id.clone());
}
/// Simulate stepping down
pub fn step_down(&mut self) {
self.role = NodeRole::Follower;
self.current_leader = None;
}
/// Get cluster status
pub fn cluster_status(&self) -> ClusterStatus {
ClusterStatus {
node_id: self.config.node_id.clone(),
role: self.role,
term: self.current_term,
leader: self.current_leader.clone(),
cluster_size: self.config.cluster_members.len(),
quorum_size: self.config.quorum_size(),
log_index: self.log_index,
}
}
}
/// Status of the Raft cluster
#[derive(Debug, Clone)]
pub struct ClusterStatus {
/// This node's ID
pub node_id: String,
/// Current role
pub role: NodeRole,
/// Current term
pub term: u64,
/// Current leader (if known)
pub leader: Option<String>,
/// Total cluster size
pub cluster_size: usize,
/// Quorum size
pub quorum_size: usize,
/// Current log index
pub log_index: u64,
}
impl ClusterStatus {
/// Check if cluster is healthy (has leader)
pub fn is_healthy(&self) -> bool {
self.leader.is_some()
}
/// Check if this node can accept writes
pub fn can_write(&self) -> bool {
self.role.is_leader()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_adapter_creation() {
let config = DistributedCoherenceConfig::single_node("node1");
let adapter = RaftAdapter::new(config);
assert!(adapter.is_leader());
assert_eq!(adapter.current_term(), 1);
}
#[test]
fn test_command_serialization() {
let cmd = CoherenceCommand::UpdateEnergy {
edge_id: (1, 2),
energy: 0.5,
};
let bytes = cmd.to_bytes();
let recovered = CoherenceCommand::from_bytes(&bytes).unwrap();
if let CoherenceCommand::UpdateEnergy { edge_id, energy } = recovered {
assert_eq!(edge_id, (1, 2));
assert!((energy - 0.5).abs() < 1e-6);
} else {
panic!("Wrong command type");
}
}
#[test]
fn test_submit_command() {
let config = DistributedCoherenceConfig::single_node("node1");
let mut adapter = RaftAdapter::new(config);
let result = adapter.update_energy((1, 2), 0.5).unwrap();
assert!(result.success);
assert_eq!(result.index, 1);
let pending = adapter.take_pending_commands();
assert_eq!(pending.len(), 1);
}
#[test]
fn test_not_leader_error() {
let config = DistributedCoherenceConfig {
node_id: "node1".to_string(),
cluster_members: vec![
"node1".to_string(),
"node2".to_string(),
"node3".to_string(),
],
..Default::default()
};
let mut adapter = RaftAdapter::new(config);
adapter.step_down();
let result = adapter.update_energy((1, 2), 0.5);
assert!(result.is_err());
}
#[test]
fn test_cluster_status() {
let config = DistributedCoherenceConfig::single_node("node1");
let adapter = RaftAdapter::new(config);
let status = adapter.cluster_status();
assert!(status.is_healthy());
assert!(status.can_write());
assert_eq!(status.cluster_size, 1);
}
}

View File

@@ -0,0 +1,238 @@
//! Distributed Coherence Configuration
//!
//! Configuration for Raft-based multi-node coherence coordination.
use serde::{Deserialize, Serialize};
/// Configuration for distributed coherence
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DistributedCoherenceConfig {
/// This node's identifier
pub node_id: String,
/// All cluster member node IDs
pub cluster_members: Vec<String>,
/// Coherence state dimension
pub dimension: usize,
/// Minimum election timeout (milliseconds)
pub election_timeout_min: u64,
/// Maximum election timeout (milliseconds)
pub election_timeout_max: u64,
/// Heartbeat interval (milliseconds)
pub heartbeat_interval: u64,
/// Maximum entries per AppendEntries RPC
pub max_entries_per_message: usize,
/// Snapshot chunk size (bytes)
pub snapshot_chunk_size: usize,
/// Energy threshold for coherence
pub coherence_threshold: f32,
/// Synchronization interval (milliseconds)
pub sync_interval: u64,
/// Enable energy checkpointing
pub enable_checkpoints: bool,
/// Checkpoint interval (number of updates)
pub checkpoint_interval: usize,
/// Replication factor for energy states
pub replication_factor: usize,
}
impl Default for DistributedCoherenceConfig {
fn default() -> Self {
Self {
node_id: "node0".to_string(),
cluster_members: vec!["node0".to_string()],
dimension: 64,
election_timeout_min: 150,
election_timeout_max: 300,
heartbeat_interval: 50,
max_entries_per_message: 100,
snapshot_chunk_size: 64 * 1024,
coherence_threshold: 0.01,
sync_interval: 100,
enable_checkpoints: true,
checkpoint_interval: 1000,
replication_factor: 3,
}
}
}
impl DistributedCoherenceConfig {
/// Create configuration for a single node (development)
pub fn single_node(node_id: &str) -> Self {
Self {
node_id: node_id.to_string(),
cluster_members: vec![node_id.to_string()],
replication_factor: 1,
..Default::default()
}
}
/// Create configuration for a 3-node cluster
pub fn three_node_cluster(node_id: &str, members: Vec<String>) -> Self {
assert!(
members.len() >= 3,
"Need at least 3 members for 3-node cluster"
);
Self {
node_id: node_id.to_string(),
cluster_members: members,
replication_factor: 3,
..Default::default()
}
}
/// Create configuration for a 5-node cluster
pub fn five_node_cluster(node_id: &str, members: Vec<String>) -> Self {
assert!(
members.len() >= 5,
"Need at least 5 members for 5-node cluster"
);
Self {
node_id: node_id.to_string(),
cluster_members: members,
replication_factor: 5,
..Default::default()
}
}
/// Validate configuration
pub fn validate(&self) -> Result<(), String> {
if self.node_id.is_empty() {
return Err("node_id cannot be empty".to_string());
}
if self.cluster_members.is_empty() {
return Err("cluster_members cannot be empty".to_string());
}
if !self.cluster_members.contains(&self.node_id) {
return Err("node_id must be in cluster_members".to_string());
}
if self.election_timeout_min >= self.election_timeout_max {
return Err("election_timeout_min must be less than election_timeout_max".to_string());
}
if self.heartbeat_interval >= self.election_timeout_min {
return Err("heartbeat_interval must be less than election_timeout_min".to_string());
}
if self.replication_factor > self.cluster_members.len() {
return Err("replication_factor cannot exceed cluster size".to_string());
}
Ok(())
}
/// Get quorum size for the cluster
pub fn quorum_size(&self) -> usize {
self.cluster_members.len() / 2 + 1
}
/// Check if this is a single-node cluster
pub fn is_single_node(&self) -> bool {
self.cluster_members.len() == 1
}
/// Get number of tolerable failures
pub fn max_failures(&self) -> usize {
self.cluster_members
.len()
.saturating_sub(self.quorum_size())
}
}
/// Node role in the distributed system
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeRole {
/// Following the leader
Follower,
/// Candidate for leadership
Candidate,
/// Current leader
Leader,
}
impl NodeRole {
/// Check if this node is the leader
pub fn is_leader(&self) -> bool {
matches!(self, Self::Leader)
}
/// Check if this node can accept writes
pub fn can_write(&self) -> bool {
matches!(self, Self::Leader)
}
/// Get role name
pub fn name(&self) -> &'static str {
match self {
Self::Follower => "follower",
Self::Candidate => "candidate",
Self::Leader => "leader",
}
}
}
impl std::fmt::Display for NodeRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = DistributedCoherenceConfig::default();
assert!(config.validate().is_ok());
}
#[test]
fn test_single_node_config() {
let config = DistributedCoherenceConfig::single_node("node1");
assert!(config.validate().is_ok());
assert!(config.is_single_node());
assert_eq!(config.quorum_size(), 1);
}
#[test]
fn test_three_node_config() {
let members = vec!["n1".to_string(), "n2".to_string(), "n3".to_string()];
let config = DistributedCoherenceConfig::three_node_cluster("n1", members);
assert!(config.validate().is_ok());
assert_eq!(config.quorum_size(), 2);
assert_eq!(config.max_failures(), 1);
}
#[test]
fn test_invalid_config() {
let config = DistributedCoherenceConfig {
node_id: "node1".to_string(),
cluster_members: vec!["node2".to_string()], // node1 not in members
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_node_role() {
assert!(NodeRole::Leader.is_leader());
assert!(NodeRole::Leader.can_write());
assert!(!NodeRole::Follower.is_leader());
assert!(!NodeRole::Follower.can_write());
}
}

View File

@@ -0,0 +1,435 @@
//! Distributed Coherence Module
//!
//! Provides Raft-based multi-node coherence coordination using `ruvector-raft`.
//!
//! # Features
//!
//! - Raft consensus for coherence state replication
//! - Replicated state machine for energy values
//! - Checkpoint and snapshot support
//! - Incoherent region tracking across cluster
//! - Leader-based write coordination
//!
//! # Architecture
//!
//! The distributed coherence system uses Raft consensus to ensure that all
//! nodes in the cluster have a consistent view of the coherence state:
//!
//! ```text
//! +-------------+ +-------------+ +-------------+
//! | Node 1 |<--->| Node 2 |<--->| Node 3 |
//! | (Leader) | | (Follower) | | (Follower) |
//! +-------------+ +-------------+ +-------------+
//! | | |
//! v v v
//! +-------------+ +-------------+ +-------------+
//! | State Mach | | State Mach | | State Mach |
//! +-------------+ +-------------+ +-------------+
//! ```
//!
//! - **Leader**: Accepts write operations (energy updates, state changes)
//! - **Followers**: Replicate state from leader, serve read operations
//! - **State Machine**: Applies committed commands to local state
//!
//! # Example
//!
//! ```ignore
//! use prime_radiant::distributed::{DistributedCoherence, DistributedCoherenceConfig};
//!
//! let config = DistributedCoherenceConfig::single_node("node1");
//! let mut coherence = DistributedCoherence::new(config);
//!
//! // Update energy (leader only)
//! coherence.update_energy(1, 2, 0.5)?;
//!
//! // Get total energy
//! let total = coherence.total_energy();
//! ```
mod adapter;
mod config;
mod state;
pub use adapter::{ClusterStatus, CoherenceCommand, CommandResult, RaftAdapter};
pub use config::{DistributedCoherenceConfig, NodeRole};
pub use state::{
ApplyResult, Checkpoint, CoherenceStateMachine, EdgeEnergy, IncoherentRegion, NodeState,
StateSnapshot, StateSummary,
};
/// Result type for distributed operations
pub type Result<T> = std::result::Result<T, DistributedError>;
/// Errors in distributed coherence operations
#[derive(Debug, Clone, thiserror::Error)]
pub enum DistributedError {
/// Not the leader
#[error("Not the leader, current leader: {leader:?}")]
NotLeader { leader: Option<String> },
/// No leader available
#[error("No leader available in the cluster")]
NoLeader,
/// Command failed
#[error("Command failed: {0}")]
CommandFailed(String),
/// Invalid state
#[error("Invalid state: {0}")]
InvalidState(String),
/// Replication failed
#[error("Replication failed: {0}")]
ReplicationFailed(String),
/// Timeout
#[error("Operation timed out")]
Timeout,
/// Node not found
#[error("Node not found: {0}")]
NodeNotFound(u64),
/// Configuration error
#[error("Configuration error: {0}")]
ConfigError(String),
}
/// Main distributed coherence engine
///
/// Combines Raft consensus with coherence state machine to provide
/// replicated coherence tracking across a cluster of nodes.
#[derive(Debug)]
pub struct DistributedCoherence {
/// Configuration
config: DistributedCoherenceConfig,
/// Raft adapter
raft: RaftAdapter,
/// State machine
state_machine: CoherenceStateMachine,
/// Update counter for checkpoint scheduling
update_counter: usize,
}
impl DistributedCoherence {
/// Create a new distributed coherence engine
pub fn new(config: DistributedCoherenceConfig) -> Self {
let raft = RaftAdapter::new(config.clone());
let state_machine = CoherenceStateMachine::new(config.dimension);
Self {
config,
raft,
state_machine,
update_counter: 0,
}
}
/// Create with default configuration (single node)
pub fn single_node(node_id: &str) -> Self {
Self::new(DistributedCoherenceConfig::single_node(node_id))
}
/// Update energy for an edge
///
/// This operation goes through Raft consensus and is replicated to all nodes.
pub fn update_energy(
&mut self,
source: u64,
target: u64,
energy: f32,
) -> Result<CommandResult> {
let result = self.raft.update_energy((source, target), energy)?;
// Apply to local state machine
self.apply_pending_commands();
// Check if we need a checkpoint
self.maybe_checkpoint()?;
Ok(result)
}
/// Set node state vector
pub fn set_node_state(&mut self, node_id: u64, state: Vec<f32>) -> Result<CommandResult> {
let result = self.raft.set_node_state(node_id, state)?;
self.apply_pending_commands();
self.maybe_checkpoint()?;
Ok(result)
}
/// Mark a region as incoherent
pub fn mark_incoherent(&mut self, region_id: u64, nodes: Vec<u64>) -> Result<CommandResult> {
let result = self.raft.mark_incoherent(region_id, nodes)?;
self.apply_pending_commands();
Ok(result)
}
/// Clear incoherence flag for a region
pub fn clear_incoherent(&mut self, region_id: u64) -> Result<CommandResult> {
let result = self.raft.clear_incoherent(region_id)?;
self.apply_pending_commands();
Ok(result)
}
/// Apply pending commands from Raft to state machine
fn apply_pending_commands(&mut self) {
let commands = self.raft.take_pending_commands();
let mut index = self.state_machine.summary().applied_index;
for cmd in commands {
index += 1;
self.state_machine.apply(&cmd, index);
self.update_counter += 1;
}
}
/// Create checkpoint if needed
fn maybe_checkpoint(&mut self) -> Result<()> {
if !self.config.enable_checkpoints {
return Ok(());
}
if self.update_counter >= self.config.checkpoint_interval {
self.checkpoint()?;
self.update_counter = 0;
}
Ok(())
}
/// Force a checkpoint
pub fn checkpoint(&mut self) -> Result<CommandResult> {
let total_energy = self.state_machine.total_energy();
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let result = self.raft.checkpoint(total_energy, timestamp)?;
self.apply_pending_commands();
Ok(result)
}
/// Get total energy
pub fn total_energy(&self) -> f32 {
self.state_machine.total_energy()
}
/// Get energy for a specific edge
pub fn get_edge_energy(&self, source: u64, target: u64) -> Option<f32> {
self.state_machine.get_edge_energy((source, target))
}
/// Get node state
pub fn get_node_state(&self, node_id: u64) -> Option<&NodeState> {
self.state_machine.get_node_state(node_id)
}
/// Check if a node is in an incoherent region
pub fn is_node_incoherent(&self, node_id: u64) -> bool {
self.state_machine.is_node_incoherent(node_id)
}
/// Get number of active incoherent regions
pub fn num_incoherent_regions(&self) -> usize {
self.state_machine.num_incoherent_regions()
}
/// Get state machine summary
pub fn summary(&self) -> StateSummary {
self.state_machine.summary()
}
/// Get cluster status
pub fn cluster_status(&self) -> ClusterStatus {
self.raft.cluster_status()
}
/// Check if this node is the leader
pub fn is_leader(&self) -> bool {
self.raft.is_leader()
}
/// Get current role
pub fn role(&self) -> NodeRole {
self.raft.role()
}
/// Get configuration
pub fn config(&self) -> &DistributedCoherenceConfig {
&self.config
}
/// Get latest checkpoint
pub fn latest_checkpoint(&self) -> Option<&Checkpoint> {
self.state_machine.latest_checkpoint()
}
/// Create snapshot of current state
pub fn snapshot(&self) -> StateSnapshot {
self.state_machine.snapshot()
}
/// Restore from snapshot
pub fn restore(&mut self, snapshot: StateSnapshot) {
self.state_machine.restore(snapshot);
}
/// Compute coherence status
pub fn coherence_status(&self) -> CoherenceStatus {
let summary = self.state_machine.summary();
let cluster = self.raft.cluster_status();
let is_coherent = summary.total_energy < self.config.coherence_threshold
&& summary.num_incoherent_regions == 0;
CoherenceStatus {
is_coherent,
total_energy: summary.total_energy,
threshold: self.config.coherence_threshold,
num_incoherent_regions: summary.num_incoherent_regions,
cluster_healthy: cluster.is_healthy(),
is_leader: cluster.can_write(),
}
}
}
/// Overall coherence status
#[derive(Debug, Clone)]
pub struct CoherenceStatus {
/// Whether the system is coherent
pub is_coherent: bool,
/// Total energy
pub total_energy: f32,
/// Coherence threshold
pub threshold: f32,
/// Number of incoherent regions
pub num_incoherent_regions: usize,
/// Whether cluster is healthy
pub cluster_healthy: bool,
/// Whether this node can write
pub is_leader: bool,
}
impl CoherenceStatus {
/// Get coherence ratio (lower is better)
pub fn coherence_ratio(&self) -> f32 {
if self.threshold > 0.0 {
self.total_energy / self.threshold
} else {
if self.total_energy > 0.0 {
f32::INFINITY
} else {
0.0
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_distributed_coherence_creation() {
let coherence = DistributedCoherence::single_node("node1");
assert!(coherence.is_leader());
assert_eq!(coherence.total_energy(), 0.0);
}
#[test]
fn test_update_energy() {
let mut coherence = DistributedCoherence::single_node("node1");
let result = coherence.update_energy(1, 2, 0.5).unwrap();
assert!(result.success);
assert!((coherence.total_energy() - 0.5).abs() < 1e-6);
assert!((coherence.get_edge_energy(1, 2).unwrap() - 0.5).abs() < 1e-6);
}
#[test]
fn test_set_node_state() {
let mut coherence = DistributedCoherence::single_node("node1");
let state = vec![1.0, 2.0, 3.0, 4.0];
coherence.set_node_state(1, state.clone()).unwrap();
let retrieved = coherence.get_node_state(1).unwrap();
assert_eq!(retrieved.state.len(), 4);
}
#[test]
fn test_incoherent_regions() {
let mut coherence = DistributedCoherence::single_node("node1");
coherence.mark_incoherent(1, vec![10, 20]).unwrap();
assert_eq!(coherence.num_incoherent_regions(), 1);
assert!(coherence.is_node_incoherent(10));
coherence.clear_incoherent(1).unwrap();
assert_eq!(coherence.num_incoherent_regions(), 0);
}
#[test]
fn test_coherence_status() {
let mut coherence = DistributedCoherence::single_node("node1");
// Initially coherent
let status = coherence.coherence_status();
assert!(status.is_coherent);
// Add high energy
for i in 0..100 {
coherence.update_energy(i, i + 1, 0.001).unwrap();
}
let status = coherence.coherence_status();
// May or may not be coherent depending on threshold
assert!(status.cluster_healthy);
assert!(status.is_leader);
}
#[test]
fn test_checkpoint() {
let config = DistributedCoherenceConfig {
enable_checkpoints: true,
checkpoint_interval: 1,
..DistributedCoherenceConfig::single_node("node1")
};
let mut coherence = DistributedCoherence::new(config);
coherence.update_energy(1, 2, 0.5).unwrap();
coherence.checkpoint().unwrap();
let cp = coherence.latest_checkpoint().unwrap();
assert!((cp.total_energy - 0.5).abs() < 1e-6);
}
#[test]
fn test_snapshot_restore() {
let mut coherence1 = DistributedCoherence::single_node("node1");
coherence1.update_energy(1, 2, 0.5).unwrap();
coherence1.set_node_state(1, vec![1.0; 64]).unwrap();
let snapshot = coherence1.snapshot();
let mut coherence2 = DistributedCoherence::single_node("node2");
coherence2.restore(snapshot);
assert!((coherence2.get_edge_energy(1, 2).unwrap() - 0.5).abs() < 1e-6);
assert!(coherence2.get_node_state(1).is_some());
}
#[test]
fn test_cluster_status() {
let coherence = DistributedCoherence::single_node("node1");
let status = coherence.cluster_status();
assert!(status.is_healthy());
assert!(status.can_write());
assert_eq!(status.cluster_size, 1);
}
}

View File

@@ -0,0 +1,502 @@
//! Distributed Coherence State Machine
//!
//! State machine for replicated coherence state across the cluster.
use super::adapter::CoherenceCommand;
use std::collections::{HashMap, HashSet};
/// Node state in the distributed system
#[derive(Debug, Clone)]
pub struct NodeState {
/// Node identifier
pub node_id: u64,
/// State vector
pub state: Vec<f32>,
/// Last update timestamp
pub last_update: u64,
}
/// Edge energy state
#[derive(Debug, Clone)]
pub struct EdgeEnergy {
/// Source node
pub source: u64,
/// Target node
pub target: u64,
/// Current energy value
pub energy: f32,
/// History of recent energies (for trend analysis)
pub history: Vec<f32>,
}
impl EdgeEnergy {
/// Create new edge energy
pub fn new(source: u64, target: u64, energy: f32) -> Self {
Self {
source,
target,
energy,
history: vec![energy],
}
}
/// Update energy value
pub fn update(&mut self, energy: f32) {
self.energy = energy;
self.history.push(energy);
// Keep only last 10 values
if self.history.len() > 10 {
self.history.remove(0);
}
}
/// Get energy trend (positive = increasing, negative = decreasing)
pub fn trend(&self) -> f32 {
if self.history.len() < 2 {
return 0.0;
}
let n = self.history.len();
let first_half: f32 = self.history[..n / 2].iter().sum::<f32>() / (n / 2) as f32;
let second_half: f32 = self.history[n / 2..].iter().sum::<f32>() / (n - n / 2) as f32;
second_half - first_half
}
/// Check if energy is stable
pub fn is_stable(&self, threshold: f32) -> bool {
if self.history.len() < 2 {
return true;
}
let mean: f32 = self.history.iter().sum::<f32>() / self.history.len() as f32;
let variance: f32 = self.history.iter().map(|e| (e - mean).powi(2)).sum::<f32>()
/ self.history.len() as f32;
variance.sqrt() < threshold
}
}
/// Incoherent region tracking
#[derive(Debug, Clone)]
pub struct IncoherentRegion {
/// Region identifier
pub region_id: u64,
/// Nodes in this region
pub nodes: HashSet<u64>,
/// When the region was marked incoherent
pub marked_at: u64,
/// Whether region is currently flagged
pub active: bool,
}
/// Checkpoint of coherence state
#[derive(Debug, Clone)]
pub struct Checkpoint {
/// Checkpoint index
pub index: u64,
/// Total energy at checkpoint
pub total_energy: f32,
/// Timestamp
pub timestamp: u64,
/// Number of edges
pub num_edges: usize,
/// Number of incoherent regions
pub num_incoherent: usize,
}
/// Replicated coherence state machine
#[derive(Debug)]
pub struct CoherenceStateMachine {
/// Node states (node_id -> state)
node_states: HashMap<u64, NodeState>,
/// Edge energies ((src, dst) -> energy)
edge_energies: HashMap<(u64, u64), EdgeEnergy>,
/// Incoherent regions
incoherent_regions: HashMap<u64, IncoherentRegion>,
/// Checkpoints
checkpoints: Vec<Checkpoint>,
/// Current applied index
applied_index: u64,
/// Configuration dimension
dimension: usize,
}
impl CoherenceStateMachine {
/// Create a new state machine
pub fn new(dimension: usize) -> Self {
Self {
node_states: HashMap::new(),
edge_energies: HashMap::new(),
incoherent_regions: HashMap::new(),
checkpoints: Vec::new(),
applied_index: 0,
dimension,
}
}
/// Apply a command to the state machine
pub fn apply(&mut self, command: &CoherenceCommand, index: u64) -> ApplyResult {
self.applied_index = index;
match command {
CoherenceCommand::UpdateEnergy { edge_id, energy } => {
self.apply_update_energy(*edge_id, *energy)
}
CoherenceCommand::SetNodeState { node_id, state } => {
self.apply_set_node_state(*node_id, state.clone())
}
CoherenceCommand::Checkpoint {
total_energy,
timestamp,
} => self.apply_checkpoint(*total_energy, *timestamp),
CoherenceCommand::MarkIncoherent { region_id, nodes } => {
self.apply_mark_incoherent(*region_id, nodes.clone())
}
CoherenceCommand::ClearIncoherent { region_id } => {
self.apply_clear_incoherent(*region_id)
}
}
}
fn apply_update_energy(&mut self, edge_id: (u64, u64), energy: f32) -> ApplyResult {
let edge = self
.edge_energies
.entry(edge_id)
.or_insert_with(|| EdgeEnergy::new(edge_id.0, edge_id.1, 0.0));
let old_energy = edge.energy;
edge.update(energy);
ApplyResult::EnergyUpdated {
edge_id,
old_energy,
new_energy: energy,
}
}
fn apply_set_node_state(&mut self, node_id: u64, state: Vec<f32>) -> ApplyResult {
let truncated_state: Vec<f32> = state.into_iter().take(self.dimension).collect();
let node = self
.node_states
.entry(node_id)
.or_insert_with(|| NodeState {
node_id,
state: vec![0.0; self.dimension],
last_update: 0,
});
node.state = truncated_state;
node.last_update = self.applied_index;
ApplyResult::NodeStateSet { node_id }
}
fn apply_checkpoint(&mut self, total_energy: f32, timestamp: u64) -> ApplyResult {
let checkpoint = Checkpoint {
index: self.applied_index,
total_energy,
timestamp,
num_edges: self.edge_energies.len(),
num_incoherent: self
.incoherent_regions
.values()
.filter(|r| r.active)
.count(),
};
self.checkpoints.push(checkpoint.clone());
// Keep only last 100 checkpoints
if self.checkpoints.len() > 100 {
self.checkpoints.remove(0);
}
ApplyResult::CheckpointCreated { checkpoint }
}
fn apply_mark_incoherent(&mut self, region_id: u64, nodes: Vec<u64>) -> ApplyResult {
let region = self
.incoherent_regions
.entry(region_id)
.or_insert_with(|| IncoherentRegion {
region_id,
nodes: HashSet::new(),
marked_at: self.applied_index,
active: false,
});
region.nodes = nodes.into_iter().collect();
region.marked_at = self.applied_index;
region.active = true;
ApplyResult::RegionMarkedIncoherent {
region_id,
node_count: region.nodes.len(),
}
}
fn apply_clear_incoherent(&mut self, region_id: u64) -> ApplyResult {
if let Some(region) = self.incoherent_regions.get_mut(&region_id) {
region.active = false;
ApplyResult::RegionCleared { region_id }
} else {
ApplyResult::RegionNotFound { region_id }
}
}
/// Get node state
pub fn get_node_state(&self, node_id: u64) -> Option<&NodeState> {
self.node_states.get(&node_id)
}
/// Get edge energy
pub fn get_edge_energy(&self, edge_id: (u64, u64)) -> Option<f32> {
self.edge_energies.get(&edge_id).map(|e| e.energy)
}
/// Get total energy
pub fn total_energy(&self) -> f32 {
self.edge_energies.values().map(|e| e.energy).sum()
}
/// Get number of incoherent regions
pub fn num_incoherent_regions(&self) -> usize {
self.incoherent_regions
.values()
.filter(|r| r.active)
.count()
}
/// Get all incoherent node IDs
pub fn incoherent_nodes(&self) -> HashSet<u64> {
self.incoherent_regions
.values()
.filter(|r| r.active)
.flat_map(|r| r.nodes.iter().copied())
.collect()
}
/// Check if a node is in an incoherent region
pub fn is_node_incoherent(&self, node_id: u64) -> bool {
self.incoherent_regions
.values()
.any(|r| r.active && r.nodes.contains(&node_id))
}
/// Get latest checkpoint
pub fn latest_checkpoint(&self) -> Option<&Checkpoint> {
self.checkpoints.last()
}
/// Get state summary
pub fn summary(&self) -> StateSummary {
StateSummary {
applied_index: self.applied_index,
num_nodes: self.node_states.len(),
num_edges: self.edge_energies.len(),
total_energy: self.total_energy(),
num_incoherent_regions: self.num_incoherent_regions(),
num_checkpoints: self.checkpoints.len(),
}
}
/// Create snapshot data
pub fn snapshot(&self) -> StateSnapshot {
StateSnapshot {
applied_index: self.applied_index,
node_states: self.node_states.clone(),
edge_energies: self.edge_energies.clone(),
incoherent_regions: self.incoherent_regions.clone(),
}
}
/// Restore from snapshot
pub fn restore(&mut self, snapshot: StateSnapshot) {
self.applied_index = snapshot.applied_index;
self.node_states = snapshot.node_states;
self.edge_energies = snapshot.edge_energies;
self.incoherent_regions = snapshot.incoherent_regions;
}
}
/// Result of applying a command
#[derive(Debug, Clone)]
pub enum ApplyResult {
/// Energy was updated
EnergyUpdated {
edge_id: (u64, u64),
old_energy: f32,
new_energy: f32,
},
/// Node state was set
NodeStateSet { node_id: u64 },
/// Checkpoint was created
CheckpointCreated { checkpoint: Checkpoint },
/// Region was marked incoherent
RegionMarkedIncoherent { region_id: u64, node_count: usize },
/// Region was cleared
RegionCleared { region_id: u64 },
/// Region was not found
RegionNotFound { region_id: u64 },
}
/// Summary of state machine state
#[derive(Debug, Clone)]
pub struct StateSummary {
/// Last applied log index
pub applied_index: u64,
/// Number of nodes
pub num_nodes: usize,
/// Number of edges
pub num_edges: usize,
/// Total energy
pub total_energy: f32,
/// Number of active incoherent regions
pub num_incoherent_regions: usize,
/// Number of checkpoints
pub num_checkpoints: usize,
}
/// Snapshot of state machine
#[derive(Debug, Clone)]
pub struct StateSnapshot {
/// Applied index at snapshot time
pub applied_index: u64,
/// Node states
pub node_states: HashMap<u64, NodeState>,
/// Edge energies
pub edge_energies: HashMap<(u64, u64), EdgeEnergy>,
/// Incoherent regions
pub incoherent_regions: HashMap<u64, IncoherentRegion>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_state_machine_creation() {
let sm = CoherenceStateMachine::new(64);
assert_eq!(sm.total_energy(), 0.0);
assert_eq!(sm.num_incoherent_regions(), 0);
}
#[test]
fn test_update_energy() {
let mut sm = CoherenceStateMachine::new(64);
let cmd = CoherenceCommand::UpdateEnergy {
edge_id: (1, 2),
energy: 0.5,
};
sm.apply(&cmd, 1);
assert!((sm.get_edge_energy((1, 2)).unwrap() - 0.5).abs() < 1e-6);
assert!((sm.total_energy() - 0.5).abs() < 1e-6);
}
#[test]
fn test_set_node_state() {
let mut sm = CoherenceStateMachine::new(4);
let cmd = CoherenceCommand::SetNodeState {
node_id: 1,
state: vec![1.0, 2.0, 3.0, 4.0],
};
sm.apply(&cmd, 1);
let state = sm.get_node_state(1).unwrap();
assert_eq!(state.state, vec![1.0, 2.0, 3.0, 4.0]);
}
#[test]
fn test_mark_incoherent() {
let mut sm = CoherenceStateMachine::new(64);
let cmd = CoherenceCommand::MarkIncoherent {
region_id: 1,
nodes: vec![10, 20, 30],
};
sm.apply(&cmd, 1);
assert_eq!(sm.num_incoherent_regions(), 1);
assert!(sm.is_node_incoherent(10));
assert!(sm.is_node_incoherent(20));
assert!(!sm.is_node_incoherent(40));
}
#[test]
fn test_clear_incoherent() {
let mut sm = CoherenceStateMachine::new(64);
sm.apply(
&CoherenceCommand::MarkIncoherent {
region_id: 1,
nodes: vec![10],
},
1,
);
assert_eq!(sm.num_incoherent_regions(), 1);
sm.apply(&CoherenceCommand::ClearIncoherent { region_id: 1 }, 2);
assert_eq!(sm.num_incoherent_regions(), 0);
}
#[test]
fn test_checkpoint() {
let mut sm = CoherenceStateMachine::new(64);
sm.apply(
&CoherenceCommand::Checkpoint {
total_energy: 1.5,
timestamp: 1000,
},
1,
);
let cp = sm.latest_checkpoint().unwrap();
assert!((cp.total_energy - 1.5).abs() < 1e-6);
assert_eq!(cp.timestamp, 1000);
}
#[test]
fn test_edge_energy_trend() {
let mut edge = EdgeEnergy::new(1, 2, 1.0);
edge.update(1.1);
edge.update(1.2);
edge.update(1.3);
edge.update(1.4);
let trend = edge.trend();
assert!(
trend > 0.0,
"Trend should be positive for increasing energy"
);
}
#[test]
fn test_snapshot_restore() {
let mut sm = CoherenceStateMachine::new(64);
sm.apply(
&CoherenceCommand::UpdateEnergy {
edge_id: (1, 2),
energy: 0.5,
},
1,
);
sm.apply(
&CoherenceCommand::SetNodeState {
node_id: 1,
state: vec![1.0; 64],
},
2,
);
let snapshot = sm.snapshot();
let mut sm2 = CoherenceStateMachine::new(64);
sm2.restore(snapshot);
assert!((sm2.get_edge_energy((1, 2)).unwrap() - 0.5).abs() < 1e-6);
assert!(sm2.get_node_state(1).is_some());
}
}