#[cfg(test)]
mod tests {
use crate::convergence::VectorClock;
use crate::distributed_transactions::{
DistributedTransactionExecutor, ReplicationMessage, TransactionEnvelope,
};
use crate::state_hash::StateHash;
use crate::transactions::{ConsistencyContract, ConsistencyLevel};
use std::collections::HashMap;
use tempfile::TempDir;
#[derive(Clone, Debug, PartialEq)]
enum RecoveryState {
Recovering,
Ready,
}
struct RecoveryOrchestrator {
executor: DistributedTransactionExecutor,
state: RecoveryState,
missing_operations: Vec<TransactionEnvelope>,
peer_node_id: String,
}
impl RecoveryOrchestrator {
fn new(executor: DistributedTransactionExecutor, peer_node_id: String) -> Self {
Self {
executor,
state: RecoveryState::Recovering,
missing_operations: Vec::new(),
peer_node_id,
}
}
fn reconcile_with_peer(
&mut self,
peer_state: &DistributedTransactionExecutor,
) -> Result<(), String> {
let local_replica = self
.executor
.get_replica_state(&self.executor.get_local_node_id())
.ok_or("Local replica not found")?;
let peer_replica = peer_state
.get_replica_state(&self.peer_node_id)
.ok_or("Peer replica not found")?;
if peer_replica.operations_applied < local_replica.operations_applied {
return Err("Peer has fewer operations than local".to_string());
}
Ok(())
}
fn apply_missing_operations(
&mut self,
missing: Vec<TransactionEnvelope>,
) -> Result<(), String> {
for envelope in missing {
if self.executor.envelopes_seen.contains(&envelope.envelope_id) {
continue; }
let message = ReplicationMessage::new(
envelope.clone(),
self.peer_node_id.clone(),
self.executor.get_local_node_id(),
envelope.envelope_id.sequence,
);
self.executor
.envelopes_seen
.insert(envelope.envelope_id.clone());
}
Ok(())
}
fn verify_convergence(&mut self) -> Result<bool, String> {
Ok(self.executor.check_convergence())
}
fn finalize_recovery(&mut self) -> Result<(), String> {
if self.state != RecoveryState::Recovering {
return Err("Already recovered".to_string());
}
if !self.verify_convergence()? {
return Err("State not converged after recovery".to_string());
}
self.state = RecoveryState::Ready;
Ok(())
}
fn get_state(&self) -> RecoveryState {
self.state.clone()
}
}
fn create_test_envelope(node: &str, sequence: u64) -> TransactionEnvelope {
TransactionEnvelope::new(
node.to_string(),
sequence,
format!("tx{}", sequence),
Vec::new(),
ConsistencyContract {
level: ConsistencyLevel::Causal,
timeout_ms: Some(1000),
required_acks: None,
},
)
}
#[test]
fn test_recovery_zero_missing_operations() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
let log_path = temp_dir.path().join("operations.log");
let mut node_a = DistributedTransactionExecutor::with_log(
"nodeA".to_string(),
StateHash::from_hex("hash0".to_string()),
Some(log_path.clone()),
)?;
let node_b = DistributedTransactionExecutor::new(
"nodeB".to_string(),
StateHash::from_hex("hash0".to_string()),
);
node_a.register_replica(
"nodeB".to_string(),
StateHash::from_hex("hash0".to_string()),
);
assert!(node_a.check_convergence());
let mut recovery = RecoveryOrchestrator::new(node_a, "nodeB".to_string());
recovery.reconcile_with_peer(&node_b)?;
recovery.apply_missing_operations(Vec::new())?; recovery.finalize_recovery()?;
assert_eq!(recovery.get_state(), RecoveryState::Ready);
Ok(())
}
#[test]
fn test_recovery_one_missing_operation() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
let log_path = temp_dir.path().join("operations.log");
let mut node_a = DistributedTransactionExecutor::with_log(
"nodeA".to_string(),
StateHash::from_hex("hash0".to_string()),
Some(log_path.clone()),
)?;
let mut node_b = DistributedTransactionExecutor::new(
"nodeB".to_string(),
StateHash::from_hex("hash0".to_string()),
);
node_a.register_replica(
"nodeB".to_string(),
StateHash::from_hex("hash0".to_string()),
);
node_b.register_replica(
"nodeA".to_string(),
StateHash::from_hex("hash0".to_string()),
);
let missing_op = create_test_envelope("nodeB", 1);
let mut recovery = RecoveryOrchestrator::new(node_a, "nodeB".to_string());
recovery.reconcile_with_peer(&node_b)?;
recovery.apply_missing_operations(vec![missing_op])?;
recovery.finalize_recovery()?;
assert_eq!(recovery.get_state(), RecoveryState::Ready);
Ok(())
}
#[test]
fn test_recovery_multiple_missing_operations() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
let log_path = temp_dir.path().join("operations.log");
let mut node_a = DistributedTransactionExecutor::with_log(
"nodeA".to_string(),
StateHash::from_hex("hash0".to_string()),
Some(log_path.clone()),
)?;
let mut node_b = DistributedTransactionExecutor::new(
"nodeB".to_string(),
StateHash::from_hex("hash0".to_string()),
);
node_a.register_replica(
"nodeB".to_string(),
StateHash::from_hex("hash0".to_string()),
);
node_b.register_replica(
"nodeA".to_string(),
StateHash::from_hex("hash0".to_string()),
);
let missing_ops: Vec<_> = (1..=5)
.map(|seq| create_test_envelope("nodeB", seq))
.collect();
let mut recovery = RecoveryOrchestrator::new(node_a, "nodeB".to_string());
recovery.reconcile_with_peer(&node_b)?;
recovery.apply_missing_operations(missing_ops)?;
recovery.finalize_recovery()?;
assert_eq!(recovery.get_state(), RecoveryState::Ready);
Ok(())
}
#[test]
fn test_recovery_duplicate_operation_prevention() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
let log_path = temp_dir.path().join("operations.log");
let mut node_a = DistributedTransactionExecutor::with_log(
"nodeA".to_string(),
StateHash::from_hex("hash0".to_string()),
Some(log_path.clone()),
)?;
let node_b = DistributedTransactionExecutor::new(
"nodeB".to_string(),
StateHash::from_hex("hash0".to_string()),
);
let envelope = create_test_envelope("nodeB", 1);
node_a.envelopes_seen.insert(envelope.envelope_id.clone());
let mut recovery = RecoveryOrchestrator::new(node_a, "nodeB".to_string());
recovery.reconcile_with_peer(&node_b)?;
recovery.apply_missing_operations(vec![envelope.clone()])?;
assert!(recovery
.executor
.envelopes_seen
.contains(&envelope.envelope_id));
recovery.finalize_recovery()?;
assert_eq!(recovery.get_state(), RecoveryState::Ready);
Ok(())
}
#[test]
fn test_recovery_interrupted_and_resumed() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
let log_path = temp_dir.path().join("operations.log");
let node_a_attempt1 = DistributedTransactionExecutor::with_log(
"nodeA".to_string(),
StateHash::from_hex("hash0".to_string()),
Some(log_path.clone()),
)?;
let node_b = DistributedTransactionExecutor::new(
"nodeB".to_string(),
StateHash::from_hex("hash0".to_string()),
);
let ops: Vec<_> = (1..=3)
.map(|seq| create_test_envelope("nodeB", seq))
.collect();
let mut recovery1 = RecoveryOrchestrator::new(node_a_attempt1, "nodeB".to_string());
recovery1.reconcile_with_peer(&node_b)?;
recovery1.apply_missing_operations(ops[0..2].to_vec())?;
let mut node_a_attempt2 = DistributedTransactionExecutor::with_log(
"nodeA".to_string(),
StateHash::from_hex("hash0".to_string()),
Some(log_path.clone()),
)?;
node_a_attempt2
.envelopes_seen
.extend(recovery1.executor.envelopes_seen.clone());
let mut recovery2 = RecoveryOrchestrator::new(node_a_attempt2, "nodeB".to_string());
recovery2.reconcile_with_peer(&node_b)?;
recovery2.apply_missing_operations(ops)?;
recovery2.finalize_recovery()?;
assert_eq!(recovery2.get_state(), RecoveryState::Ready);
Ok(())
}
#[test]
fn test_recovery_peer_unavailable() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
let log_path = temp_dir.path().join("operations.log");
let node_a = DistributedTransactionExecutor::with_log(
"nodeA".to_string(),
StateHash::from_hex("hash0".to_string()),
Some(log_path.clone()),
)?;
let mut recovery = RecoveryOrchestrator::new(node_a, "unavailable_peer".to_string());
let stub_peer = DistributedTransactionExecutor::new(
"different_node".to_string(),
StateHash::from_hex("hash0".to_string()),
);
let result = recovery.reconcile_with_peer(&stub_peer);
assert!(result.is_err());
assert_eq!(recovery.get_state(), RecoveryState::Recovering);
Ok(())
}
#[test]
fn test_recovery_complete_state_machine_progression() -> Result<(), Box<dyn std::error::Error>>
{
let temp_dir = TempDir::new()?;
let log_path = temp_dir.path().join("operations.log");
let mut peer = DistributedTransactionExecutor::new(
"peer".to_string(),
StateHash::from_hex("hash0".to_string()),
);
let mut local = DistributedTransactionExecutor::with_log(
"local".to_string(),
StateHash::from_hex("hash0".to_string()),
Some(log_path.clone()),
)?;
local.register_replica("peer".to_string(), StateHash::from_hex("hash0".to_string()));
peer.register_replica(
"local".to_string(),
StateHash::from_hex("hash0".to_string()),
);
let mut recovery = RecoveryOrchestrator::new(local, "peer".to_string());
assert_eq!(recovery.get_state(), RecoveryState::Recovering);
recovery.reconcile_with_peer(&peer)?;
recovery.apply_missing_operations(Vec::new())?;
let converged = recovery.verify_convergence()?;
assert!(converged);
recovery.finalize_recovery()?;
assert_eq!(recovery.get_state(), RecoveryState::Ready);
Ok(())
}
#[test]
fn test_executor_restart_and_recovery_integration() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
let log_path = temp_dir.path().join("operations.log");
{
let executor = DistributedTransactionExecutor::with_log(
"nodeA".to_string(),
StateHash::from_hex("hash0".to_string()),
Some(log_path.clone()),
)?;
let _node_id = executor.get_local_node_id();
assert_eq!(_node_id, "nodeA");
}
let mut recovered_executor = DistributedTransactionExecutor::with_log(
"nodeA".to_string(),
StateHash::from_hex("hash0".to_string()),
Some(log_path.clone()),
)?;
let count = recovered_executor.load_from_disk(log_path)?;
assert_eq!(count, 0);
let peer = DistributedTransactionExecutor::new(
"nodeB".to_string(),
StateHash::from_hex("hash0".to_string()),
);
let recovered_hash = recovered_executor
.get_replica_state("nodeA")
.expect("the recovered node has a local replica")
.state_hash;
recovered_executor.register_replica("nodeB".to_string(), recovered_hash);
let mut recovery = RecoveryOrchestrator::new(recovered_executor, "nodeB".to_string());
recovery.reconcile_with_peer(&peer)?;
recovery.apply_missing_operations(Vec::new())?;
recovery.finalize_recovery()?;
assert_eq!(recovery.get_state(), RecoveryState::Ready);
Ok(())
}
}