Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
388
vendor/ruvector/crates/prime-radiant/src/distributed/adapter.rs
vendored
Normal file
388
vendor/ruvector/crates/prime-radiant/src/distributed/adapter.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
238
vendor/ruvector/crates/prime-radiant/src/distributed/config.rs
vendored
Normal file
238
vendor/ruvector/crates/prime-radiant/src/distributed/config.rs
vendored
Normal 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());
|
||||
}
|
||||
}
|
||||
435
vendor/ruvector/crates/prime-radiant/src/distributed/mod.rs
vendored
Normal file
435
vendor/ruvector/crates/prime-radiant/src/distributed/mod.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
502
vendor/ruvector/crates/prime-radiant/src/distributed/state.rs
vendored
Normal file
502
vendor/ruvector/crates/prime-radiant/src/distributed/state.rs
vendored
Normal 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(®ion_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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user