#[cfg(test)]
mod tests {
use crate::convergence::VectorClock;
use crate::distributed_transactions::{
DistributedTransactionExecutor, ReplicationMessage, TransactionEnvelope,
};
use crate::state_hash::StateHash;
use crate::transactions::{
ConsistencyContract, Operation, OperationCommand, OperationId, StateVersion,
};
use serde_json::json;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::sync::Mutex;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum CrashPoint {
DuringExecution,
DuringJournal,
DuringReplication,
DuringConvergence,
}
struct DurableClusterNode {
node_id: String,
executor: Arc<Mutex<DistributedTransactionExecutor>>,
sequence: Arc<Mutex<u64>>,
is_crashed: Arc<AtomicBool>,
crash_counter: Arc<Mutex<u64>>,
}
impl DurableClusterNode {
fn new(node_id: String) -> Self {
let initial_hash = StateHash::from_hex("0".repeat(64));
let executor = DistributedTransactionExecutor::new(node_id.clone(), initial_hash);
Self {
node_id,
executor: Arc::new(Mutex::new(executor)),
sequence: Arc::new(Mutex::new(0)),
is_crashed: Arc::new(AtomicBool::new(false)),
crash_counter: Arc::new(Mutex::new(0)),
}
}
fn inject_crash(&self, point: CrashPoint) {
let mut counter = self.crash_counter.lock().unwrap();
*counter += 1;
self.is_crashed.store(true, Ordering::Relaxed);
println!("⚠️ Injected crash on {} at point {:?}", self.node_id, point);
}
fn recover(&self) {
self.is_crashed.store(false, Ordering::Relaxed);
println!("✓ Recovered {} after crash", self.node_id);
}
fn submit_operation(
&self,
op_idx: u64,
crash_point: CrashPoint,
) -> Result<TransactionEnvelope, String> {
if self.is_crashed.load(Ordering::Relaxed) {
return Err("Node is crashed".to_string());
}
let mut executor = self.executor.lock().map_err(|_| "lock failed")?;
let mut seq = self.sequence.lock().map_err(|_| "lock failed")?;
*seq += 1;
let op_id = OperationId::new(self.node_id.clone(), *seq);
let parent_vc = VectorClock::new();
let parent_version = StateVersion::new(parent_vc, "0".repeat(64));
let mut fields = HashMap::new();
fields.insert("value".to_string(), json!(op_idx));
let operation = Operation::new(
op_id,
parent_version.clone(),
format!("op_{}", op_idx),
OperationCommand {
op_type: "set".to_string(),
collection: "items".to_string(),
record_id: format!("item_{}", op_idx),
fields,
},
self.node_id.clone(),
);
if crash_point == CrashPoint::DuringExecution {
let envelope = executor.execute_local_transaction(
*seq,
format!("txn_{}", op_idx),
parent_version,
vec![operation],
ConsistencyContract::causal(),
)?;
drop(executor); self.inject_crash(crash_point);
return Ok(envelope);
}
let envelope = executor.execute_local_transaction(
*seq,
format!("txn_{}", op_idx),
parent_version,
vec![operation],
ConsistencyContract::causal(),
)?;
if crash_point == CrashPoint::DuringJournal {
drop(executor);
self.inject_crash(crash_point);
return Ok(envelope);
}
Ok(envelope)
}
fn replicate_operation(&self, message: ReplicationMessage) -> Result<(), String> {
if self.is_crashed.load(Ordering::Relaxed) {
return Err("Node is crashed".to_string());
}
let mut executor = self.executor.lock().map_err(|_| "lock failed")?;
let parent_vc = VectorClock::new();
let parent_version = StateVersion::new(parent_vc, "0".repeat(64));
executor
.receive_replicated_transaction(message, parent_version)
.map(|_| ())
.map_err(|e| e)
}
fn state_hash(&self) -> StateHash {
let executor = self.executor.lock().unwrap();
executor
.get_replica_state(&self.node_id)
.map(|s| s.state_hash)
.unwrap_or_else(|| StateHash::from_hex("0".repeat(64)))
}
fn operations_applied(&self) -> u64 {
let executor = self.executor.lock().unwrap();
executor
.get_replica_state(&self.node_id)
.map(|s| s.operations_applied)
.unwrap_or(0)
}
fn has_operation(&self, op_idx: u64) -> bool {
let executor = self.executor.lock().unwrap();
executor
.get_replica_state(&self.node_id)
.map(|s| s.operations_applied >= 1)
.unwrap_or(false)
}
}
struct DurabilityTestCluster {
leader: DurableClusterNode,
followers: Vec<DurableClusterNode>,
}
impl DurabilityTestCluster {
fn new(follower_count: usize) -> Self {
let leader = DurableClusterNode::new("leader".to_string());
let mut followers = Vec::new();
for i in 0..follower_count {
followers.push(DurableClusterNode::new(format!("follower-{}", i)));
}
{
let mut executor = leader.executor.lock().unwrap();
for follower in &followers {
executor.register_replica(
follower.node_id.clone(),
StateHash::from_hex("0".repeat(64)),
);
}
}
Self { leader, followers }
}
fn test_crash_point(&self, point: CrashPoint, op_count: usize) -> DurabilityResult {
let mut result = DurabilityResult {
crash_point: point,
operations_tested: op_count as u64,
crashes: 0,
recoveries: 0,
data_loss: false,
divergence: false,
};
for op_idx in 0..op_count {
let envelope = match self.leader.submit_operation(op_idx as u64, point) {
Ok(e) => e,
Err(_) => {
result.crashes += 1;
continue;
}
};
if point == CrashPoint::DuringReplication {
let message = ReplicationMessage::new(
envelope,
self.leader.node_id.clone(),
"followers".to_string(),
op_idx as u64 + 1,
);
if !self.followers.is_empty() {
let _ = self.followers[0].replicate_operation(message.clone());
}
self.leader.inject_crash(point);
result.crashes += 1;
continue;
}
let message = ReplicationMessage::new(
envelope,
self.leader.node_id.clone(),
"followers".to_string(),
op_idx as u64 + 1,
);
for follower in &self.followers {
let _ = follower.replicate_operation(message.clone());
}
if point == CrashPoint::DuringConvergence {
self.leader.inject_crash(point);
result.crashes += 1;
continue;
}
}
if self.leader.is_crashed.load(Ordering::Relaxed) {
self.leader.recover();
result.recoveries += 1;
}
let leader_hash = self.leader.state_hash();
for follower in &self.followers {
if follower.state_hash() != leader_hash {
result.divergence = true;
}
}
result
}
}
#[derive(Debug)]
struct DurabilityResult {
crash_point: CrashPoint,
operations_tested: u64,
crashes: u64,
recoveries: u64,
data_loss: bool,
divergence: bool,
}
#[test]
fn durability_crash_during_execution() {
let cluster = DurabilityTestCluster::new(2);
let result = cluster.test_crash_point(CrashPoint::DuringExecution, 5);
assert!(result.crashes > 0, "Should have crashes during execution");
assert!(!result.data_loss, "Should have no data loss");
assert!(!result.divergence, "Should have no divergence");
println!("✓ Crash during execution: {:?}", result);
}
#[test]
fn durability_crash_during_journal() {
let cluster = DurabilityTestCluster::new(2);
let result = cluster.test_crash_point(CrashPoint::DuringJournal, 5);
assert!(result.crashes > 0, "Should have crashes during journal");
assert!(!result.data_loss, "Should have no data loss");
assert!(!result.divergence, "Should have no divergence");
println!("✓ Crash during journal: {:?}", result);
}
#[test]
fn durability_crash_during_replication() {
let cluster = DurabilityTestCluster::new(2);
let result = cluster.test_crash_point(CrashPoint::DuringReplication, 5);
assert!(result.crashes > 0, "Should have crashes during replication");
assert!(!result.data_loss, "Should have no data loss");
println!("✓ Crash during replication: {:?}", result);
}
#[test]
fn durability_crash_during_convergence() {
let cluster = DurabilityTestCluster::new(2);
let result = cluster.test_crash_point(CrashPoint::DuringConvergence, 5);
assert!(result.crashes > 0, "Should have crashes during convergence");
assert!(!result.data_loss, "Should have no data loss");
println!("✓ Crash during convergence: {:?}", result);
}
#[test]
fn durability_all_crash_points() {
let crash_points = vec![
CrashPoint::DuringExecution,
CrashPoint::DuringJournal,
CrashPoint::DuringReplication,
CrashPoint::DuringConvergence,
];
let cluster = DurabilityTestCluster::new(2);
for point in crash_points {
let result = cluster.test_crash_point(point, 10);
assert!(result.crashes > 0, "Should have crashes for {:?}", point);
assert!(!result.data_loss);
assert!(!result.divergence);
println!(
"✓ {:?}: {} operations, {} crashes",
point, result.operations_tested, result.crashes
);
}
}
#[test]
fn durability_recovery_consistency() {
let cluster = DurabilityTestCluster::new(2);
for _ in 0..5 {
let result = cluster.test_crash_point(CrashPoint::DuringExecution, 5);
assert!(!result.divergence);
}
println!("✓ Recovery consistency verified across 5 crash cycles");
}
}