use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use crate::engine::log::LogWriter;
use crate::engine::reader::{CommittedState, LogReader};
use crate::kernel::bank::{BankApp, BankEvent, BankQuery, BankQueryResponse};
use crate::kernel::executor::Executor;
use super::message::VsrMessage;
use super::network::MockNetwork;
use super::node::{NodeRole, VsrNode};
fn serialize_event(event: &BankEvent) -> Vec<u8> {
bincode::serialize(event).unwrap()
}
struct TestPaths {
log_paths: Vec<PathBuf>,
manifest_paths: Vec<PathBuf>,
}
impl TestPaths {
fn new(prefix: &str, count: usize) -> Self {
let log_paths: Vec<PathBuf> = (0..count)
.map(|i| PathBuf::from(format!("/tmp/chr_{}_{}.log", prefix, i)))
.collect();
let manifest_paths: Vec<PathBuf> = (0..count)
.map(|i| PathBuf::from(format!("/tmp/chr_{}_{}.manifest", prefix, i)))
.collect();
for path in &log_paths {
let _ = fs::remove_file(path);
}
for path in &manifest_paths {
let _ = fs::remove_file(path);
}
TestPaths { log_paths, manifest_paths }
}
fn log(&self, idx: usize) -> &Path {
&self.log_paths[idx]
}
fn manifest(&self, idx: usize) -> &Path {
&self.manifest_paths[idx]
}
}
impl Drop for TestPaths {
fn drop(&mut self) {
for path in &self.log_paths {
let _ = fs::remove_file(path);
}
for path in &self.manifest_paths {
let _ = fs::remove_file(path);
}
}
}
#[test]
fn test_vsr_quorum_commit() {
let log_path_0 = Path::new("/tmp/chr_vsr_node0.log");
let log_path_1 = Path::new("/tmp/chr_vsr_node1.log");
let log_path_2 = Path::new("/tmp/chr_vsr_node2.log");
let manifest_path_0 = Path::new("/tmp/chr_vsr_node0.manifest");
let manifest_path_1 = Path::new("/tmp/chr_vsr_node1.manifest");
let manifest_path_2 = Path::new("/tmp/chr_vsr_node2.manifest");
let _ = fs::remove_file(log_path_0);
let _ = fs::remove_file(log_path_1);
let _ = fs::remove_file(log_path_2);
let _ = fs::remove_file(manifest_path_0);
let _ = fs::remove_file(manifest_path_1);
let _ = fs::remove_file(manifest_path_2);
let mut network = MockNetwork::new(3);
let ep0 = network.create_endpoint(0).unwrap();
let ep1 = network.create_endpoint(1).unwrap();
let ep2 = network.create_endpoint(2).unwrap();
let committed_state_0 = Arc::new(CommittedState::new());
let committed_state_1 = Arc::new(CommittedState::new());
let committed_state_2 = Arc::new(CommittedState::new());
let writer_0 = LogWriter::create(log_path_0, 1).unwrap();
let writer_1 = LogWriter::create(log_path_1, 1).unwrap();
let writer_2 = LogWriter::create(log_path_2, 1).unwrap();
let reader_0_exec = LogReader::open(log_path_0, committed_state_0.clone()).unwrap();
let reader_1 = LogReader::open(log_path_1, committed_state_1.clone()).unwrap();
let reader_2 = LogReader::open(log_path_2, committed_state_2.clone()).unwrap();
let reader_0_catchup = LogReader::open(log_path_0, committed_state_0.clone()).unwrap();
let app_0 = BankApp::new();
let app_1 = BankApp::new();
let app_2 = BankApp::new();
let executor_0 = Executor::new(reader_0_exec, app_0, 0);
let executor_1 = Executor::new(reader_1, app_1, 0);
let executor_2 = Executor::new(reader_2, app_2, 0);
let mut node_0: VsrNode<BankApp> = VsrNode::new_primary(
0,
3,
1, writer_0,
Some(reader_0_catchup),
committed_state_0.clone(),
ep0,
Some(executor_0),
manifest_path_0,
).unwrap();
let mut node_1: VsrNode<BankApp> = VsrNode::new_backup(
1,
3,
1,
writer_1,
committed_state_1.clone(),
ep1,
Some(executor_1),
manifest_path_1,
).unwrap();
let mut node_2: VsrNode<BankApp> = VsrNode::new_backup(
2,
3,
1,
writer_2,
committed_state_2.clone(),
ep2,
Some(executor_2),
manifest_path_2,
).unwrap();
network.disconnect(2);
let deposit = BankEvent::Deposit {
user: "Alice".to_string(),
amount: 100,
};
let payload = serialize_event(&deposit);
let index = node_0.submit(&payload).unwrap();
assert_eq!(index, 0);
for _ in 0..20 {
thread::sleep(Duration::from_millis(5));
node_1.process_all();
node_2.process_all(); thread::sleep(Duration::from_millis(5));
node_0.process_all();
if node_0.committed_index() == Some(0) {
break;
}
}
assert_eq!(node_0.committed_index(), Some(0), "Node 0 should have committed index 0");
thread::sleep(Duration::from_millis(5));
node_1.process_all();
assert_eq!(node_1.committed_index(), Some(0), "Node 1 should have committed index 0");
if let Some(ref executor) = node_0.executor {
let response = executor.query(BankQuery::Balance {
user: "Alice".to_string(),
});
assert!(
matches!(response, BankQueryResponse::Balance(100)),
"Alice should have balance 100 on Node 0"
);
}
if let Some(ref executor) = node_1.executor {
let response = executor.query(BankQuery::Balance {
user: "Alice".to_string(),
});
assert!(
matches!(response, BankQueryResponse::Balance(100)),
"Alice should have balance 100 on Node 1"
);
}
assert_eq!(node_2.committed_index(), None, "Node 2 should not have committed anything");
network.reconnect(2);
let deposit2 = BankEvent::Deposit {
user: "Bob".to_string(),
amount: 50,
};
let payload2 = serialize_event(&deposit2);
let index2 = node_0.submit(&payload2).unwrap();
assert_eq!(index2, 1);
for _ in 0..20 {
thread::sleep(Duration::from_millis(5));
node_1.process_all();
node_2.process_all(); thread::sleep(Duration::from_millis(5));
node_0.process_all();
if node_0.committed_index() == Some(1) && node_1.committed_index() == Some(1) {
break;
}
}
node_1.process_all();
assert_eq!(node_0.committed_index(), Some(1), "Node 0 should have committed index 1");
assert_eq!(node_1.committed_index(), Some(1), "Node 1 should have committed index 1");
let _ = fs::remove_file(log_path_0);
let _ = fs::remove_file(log_path_1);
let _ = fs::remove_file(log_path_2);
}
#[test]
fn test_vsr_basic_replication() {
let paths = TestPaths::new("chr_basic", 2);
let mut network = MockNetwork::new(2);
let ep0 = network.create_endpoint(0).unwrap();
let ep1 = network.create_endpoint(1).unwrap();
let committed_state_0 = Arc::new(CommittedState::new());
let committed_state_1 = Arc::new(CommittedState::new());
let writer_0 = LogWriter::create(paths.log(0), 1).unwrap();
let writer_1 = LogWriter::create(paths.log(1), 1).unwrap();
let mut node_0: VsrNode<BankApp> = VsrNode::new_primary(
0,
2,
1,
writer_0,
None, committed_state_0.clone(),
ep0,
None,
paths.manifest(0),
).unwrap();
let mut node_1: VsrNode<BankApp> = VsrNode::new_backup(
1,
2,
1,
writer_1,
committed_state_1.clone(),
ep1,
None,
paths.manifest(1),
).unwrap();
let index = node_0.submit(b"test payload").unwrap();
assert_eq!(index, 0);
thread::sleep(Duration::from_millis(5));
node_1.process_all();
thread::sleep(Duration::from_millis(5));
node_0.process_all();
assert_eq!(node_0.committed_index(), Some(0));
thread::sleep(Duration::from_millis(5));
node_1.process_all();
assert_eq!(node_1.committed_index(), Some(0));
}
#[test]
fn test_vsr_multiple_entries() {
let paths = TestPaths::new("chr_multi", 2);
let mut network = MockNetwork::new(2);
let ep0 = network.create_endpoint(0).unwrap();
let ep1 = network.create_endpoint(1).unwrap();
let committed_state_0 = Arc::new(CommittedState::new());
let committed_state_1 = Arc::new(CommittedState::new());
let writer_0 = LogWriter::create(paths.log(0), 1).unwrap();
let writer_1 = LogWriter::create(paths.log(1), 1).unwrap();
let mut node_0: VsrNode<BankApp> = VsrNode::new_primary(
0,
2,
1,
writer_0,
None, committed_state_0.clone(),
ep0,
None,
paths.manifest(0),
).unwrap();
let mut node_1: VsrNode<BankApp> = VsrNode::new_backup(
1,
2,
1,
writer_1,
committed_state_1.clone(),
ep1,
None,
paths.manifest(1),
).unwrap();
for i in 0..5 {
let payload = format!("entry {}", i);
let index = node_0.submit(payload.as_bytes()).unwrap();
assert_eq!(index, i);
for _ in 0..20 {
thread::sleep(Duration::from_millis(5));
node_1.process_all();
thread::sleep(Duration::from_millis(5));
node_0.process_all();
thread::sleep(Duration::from_millis(5));
node_1.process_all();
if node_0.committed_index() == Some(i) && node_1.committed_index() == Some(i) {
break;
}
}
}
assert_eq!(node_0.committed_index(), Some(4));
assert_eq!(node_1.committed_index(), Some(4));
}
#[test]
fn test_vsr_failure_detection() {
use super::node::ELECTION_TIMEOUT;
let paths = TestPaths::new("chr_failure", 3);
let mut network = MockNetwork::new(3);
let ep0 = network.create_endpoint(0).unwrap();
let ep1 = network.create_endpoint(1).unwrap();
let ep2 = network.create_endpoint(2).unwrap();
let committed_state_0 = Arc::new(CommittedState::new());
let committed_state_1 = Arc::new(CommittedState::new());
let committed_state_2 = Arc::new(CommittedState::new());
let writer_0 = LogWriter::create(paths.log(0), 1).unwrap();
let writer_1 = LogWriter::create(paths.log(1), 1).unwrap();
let writer_2 = LogWriter::create(paths.log(2), 1).unwrap();
let mut node_0: VsrNode<BankApp> = VsrNode::new_primary(
0,
3,
1,
writer_0,
None, committed_state_0.clone(),
ep0,
None,
paths.manifest(0),
).unwrap();
let mut node_1: VsrNode<BankApp> = VsrNode::new_backup(
1,
3,
1,
writer_1,
committed_state_1.clone(),
ep1,
None,
paths.manifest(1),
).unwrap();
let mut node_2: VsrNode<BankApp> = VsrNode::new_backup(
2,
3,
1,
writer_2,
committed_state_2.clone(),
ep2,
None,
paths.manifest(2),
).unwrap();
assert_eq!(node_0.role, super::node::NodeRole::Primary);
assert_eq!(node_1.role, super::node::NodeRole::Backup);
assert_eq!(node_2.role, super::node::NodeRole::Backup);
node_0.tick(); thread::sleep(Duration::from_millis(10));
node_1.process_all();
node_2.process_all();
assert_eq!(node_1.role, super::node::NodeRole::Backup);
assert_eq!(node_2.role, super::node::NodeRole::Backup);
network.disconnect(0);
thread::sleep(ELECTION_TIMEOUT + Duration::from_millis(100));
let changed_1 = node_1.tick();
let changed_2 = node_2.tick();
assert!(changed_1, "Node 1 should have started view change");
assert!(changed_2, "Node 2 should have started view change");
assert!(
node_1.is_view_change_in_progress(),
"Node 1 should be in ViewChangeInProgress"
);
assert!(
node_2.is_view_change_in_progress(),
"Node 2 should be in ViewChangeInProgress"
);
assert_eq!(node_1.proposed_view(), 2, "Node 1 should propose view 2");
assert_eq!(node_2.proposed_view(), 2, "Node 2 should propose view 2");
thread::sleep(Duration::from_millis(10));
node_1.process_all();
node_2.process_all();
}
#[test]
fn test_vsr_heartbeat_prevents_election() {
use super::node::{HEARTBEAT_INTERVAL, ELECTION_TIMEOUT};
let paths = TestPaths::new("chr_heartbeat", 2);
let mut network = MockNetwork::new(2);
let ep0 = network.create_endpoint(0).unwrap();
let ep1 = network.create_endpoint(1).unwrap();
let committed_state_0 = Arc::new(CommittedState::new());
let committed_state_1 = Arc::new(CommittedState::new());
let writer_0 = LogWriter::create(paths.log(0), 1).unwrap();
let writer_1 = LogWriter::create(paths.log(1), 1).unwrap();
let mut node_0: VsrNode<BankApp> = VsrNode::new_primary(
0,
2,
1,
writer_0,
None, committed_state_0.clone(),
ep0,
None,
paths.manifest(0),
).unwrap();
let mut node_1: VsrNode<BankApp> = VsrNode::new_backup(
1,
2,
1,
writer_1,
committed_state_1.clone(),
ep1,
None,
paths.manifest(1),
).unwrap();
for _ in 0..5 {
thread::sleep(HEARTBEAT_INTERVAL + Duration::from_millis(10));
node_0.tick();
thread::sleep(Duration::from_millis(5));
node_1.process_all();
let changed = node_1.tick();
assert!(!changed, "Backup should not start view change while receiving heartbeats");
assert_eq!(node_1.role, super::node::NodeRole::Backup);
}
}
#[test]
fn test_vsr_full_view_change() {
use super::node::ELECTION_TIMEOUT;
let paths = TestPaths::new("chr_viewchange", 3);
let mut network = MockNetwork::new(3);
let ep0 = network.create_endpoint(0).unwrap();
let ep1 = network.create_endpoint(1).unwrap();
let ep2 = network.create_endpoint(2).unwrap();
let committed_state_0 = Arc::new(CommittedState::new());
let committed_state_1 = Arc::new(CommittedState::new());
let committed_state_2 = Arc::new(CommittedState::new());
let writer_0 = LogWriter::create(paths.log(0), 1).unwrap();
let writer_1 = LogWriter::create(paths.log(1), 1).unwrap();
let writer_2 = LogWriter::create(paths.log(2), 1).unwrap();
let mut node_0: VsrNode<BankApp> = VsrNode::new_primary(
0,
3,
0, writer_0,
None, committed_state_0.clone(),
ep0,
None,
paths.manifest(0),
).unwrap();
let mut node_1: VsrNode<BankApp> = VsrNode::new_backup(
1,
3,
0,
writer_1,
committed_state_1.clone(),
ep1,
None,
paths.manifest(1),
).unwrap();
let mut node_2: VsrNode<BankApp> = VsrNode::new_backup(
2,
3,
0,
writer_2,
committed_state_2.clone(),
ep2,
None,
paths.manifest(2),
).unwrap();
let deposit = BankEvent::Deposit {
user: "Alice".to_string(),
amount: 100,
};
let payload = serialize_event(&deposit);
let index = node_0.submit(&payload).unwrap();
assert_eq!(index, 0);
for _ in 0..10 {
thread::sleep(Duration::from_millis(5));
node_1.process_all();
node_2.process_all();
thread::sleep(Duration::from_millis(5));
node_0.process_all();
if node_0.committed_index() == Some(0) {
break;
}
}
assert_eq!(node_0.committed_index(), Some(0), "Node 0 should have committed");
thread::sleep(Duration::from_millis(10));
node_1.process_all();
node_2.process_all();
assert_eq!(node_1.committed_index(), Some(0), "Node 1 should have committed");
network.disconnect(0);
thread::sleep(ELECTION_TIMEOUT + Duration::from_millis(50));
node_1.tick();
node_2.tick();
assert!(node_1.is_view_change_in_progress(), "Node 1 should start view change");
assert!(node_2.is_view_change_in_progress(), "Node 2 should start view change");
for _ in 0..10 {
thread::sleep(Duration::from_millis(5));
node_1.process_all();
node_2.process_all();
if node_1.role == super::node::NodeRole::Primary && node_1.current_view() == 1 {
break;
}
}
assert_eq!(node_1.role, super::node::NodeRole::Primary, "Node 1 should be Primary");
assert_eq!(node_1.current_view(), 1, "Node 1 should be in view 1");
for _ in 0..10 {
thread::sleep(Duration::from_millis(5));
node_2.process_all();
if node_2.role == super::node::NodeRole::Backup && node_2.current_view() == 1 {
break;
}
}
assert_eq!(node_2.role, super::node::NodeRole::Backup, "Node 2 should be Backup");
assert_eq!(node_2.current_view(), 1, "Node 2 should be in view 1");
let deposit2 = BankEvent::Deposit {
user: "Bob".to_string(),
amount: 50,
};
let payload2 = serialize_event(&deposit2);
let index2 = node_1.submit(&payload2).unwrap();
assert_eq!(index2, 1);
for _ in 0..10 {
thread::sleep(Duration::from_millis(5));
node_2.process_all();
thread::sleep(Duration::from_millis(5));
node_1.process_all();
thread::sleep(Duration::from_millis(5));
node_2.process_all();
if node_1.committed_index() == Some(1) && node_2.committed_index() == Some(1) {
break;
}
}
assert_eq!(node_1.committed_index(), Some(1), "Node 1 should have committed index 1");
assert_eq!(node_2.committed_index(), Some(1), "Node 2 should have committed index 1");
}
#[test]
fn test_client_exactly_once_during_failover() {
use super::client::{chrClient, SessionMap};
use super::message::{ClientRequest, ClientResult};
use super::node::ELECTION_TIMEOUT;
let paths = TestPaths::new("chr_idempotent", 3);
let mut network = MockNetwork::new(3);
let ep0 = network.create_endpoint(0).unwrap();
let ep1 = network.create_endpoint(1).unwrap();
let ep2 = network.create_endpoint(2).unwrap();
let committed_state_0 = Arc::new(CommittedState::new());
let committed_state_1 = Arc::new(CommittedState::new());
let committed_state_2 = Arc::new(CommittedState::new());
let writer_0 = LogWriter::create(paths.log(0), 1).unwrap();
let writer_1 = LogWriter::create(paths.log(1), 1).unwrap();
let writer_2 = LogWriter::create(paths.log(2), 1).unwrap();
let mut node_0: VsrNode<BankApp> = VsrNode::new_primary(
0,
3,
0,
writer_0,
None, committed_state_0.clone(),
ep0,
None,
paths.manifest(0),
).unwrap();
let mut node_1: VsrNode<BankApp> = VsrNode::new_backup(
1,
3,
0,
writer_1,
committed_state_1.clone(),
ep1,
None,
paths.manifest(1),
).unwrap();
let mut node_2: VsrNode<BankApp> = VsrNode::new_backup(
2,
3,
0,
writer_2,
committed_state_2.clone(),
ep2,
None,
paths.manifest(2),
).unwrap();
let mut client = chrClient::new(42, vec![0, 1, 2]);
let deposit = BankEvent::Deposit {
user: "Alice".to_string(),
amount: 100,
};
let payload = serialize_event(&deposit);
let request = client.create_request(payload.clone());
let sequence_number = request.sequence_number;
assert_eq!(sequence_number, 1, "First request should have sequence 1");
let response = node_0.handle_client_request(&request);
assert!(
matches!(response.result, ClientResult::Pending),
"Request should be pending"
);
for _ in 0..20 {
thread::sleep(Duration::from_millis(5));
node_1.process_all();
node_2.process_all();
thread::sleep(Duration::from_millis(5));
node_0.process_all();
if node_0.committed_index() == Some(0) {
break;
}
}
let committed = node_0.check_committed_requests();
assert_eq!(committed.len(), 1, "Should have one committed request");
assert!(
matches!(committed[0].1.result, ClientResult::Success { log_index: 0 }),
"Request should be successful at index 0"
);
thread::sleep(Duration::from_millis(5));
node_1.process_all();
assert_eq!(node_1.committed_index(), Some(0));
let session_map_snapshot = node_0.session_map().clone();
node_1.restore_session_map(session_map_snapshot);
network.disconnect(0);
thread::sleep(ELECTION_TIMEOUT + Duration::from_millis(50));
node_1.tick();
node_2.tick();
for _ in 0..20 {
thread::sleep(Duration::from_millis(5));
node_1.process_all();
node_2.process_all();
if node_1.role == super::node::NodeRole::Primary && node_1.current_view() == 1 {
break;
}
}
assert_eq!(node_1.role, super::node::NodeRole::Primary);
assert_eq!(node_1.current_view(), 1);
let retry_request = client.create_request_with_seq(payload.clone(), sequence_number);
let retry_response = node_1.handle_client_request(&retry_request);
assert!(
matches!(retry_response.result, ClientResult::Success { log_index: 0 }),
"Retry should return cached success response, got {:?}",
retry_response.result
);
let deposit2 = BankEvent::Deposit {
user: "Bob".to_string(),
amount: 50,
};
let payload2 = serialize_event(&deposit2);
let request2 = client.create_request(payload2);
assert_eq!(request2.sequence_number, 2, "Second request should have sequence 2");
let response2 = node_1.handle_client_request(&request2);
assert!(
matches!(response2.result, ClientResult::Pending),
"New request should be pending"
);
for _ in 0..20 {
thread::sleep(Duration::from_millis(5));
node_2.process_all();
thread::sleep(Duration::from_millis(5));
node_1.process_all();
thread::sleep(Duration::from_millis(5));
node_2.process_all();
if node_1.committed_index() == Some(1) {
break;
}
}
let committed2 = node_1.check_committed_requests();
assert_eq!(committed2.len(), 1);
assert!(
matches!(committed2[0].1.result, ClientResult::Success { log_index: 1 }),
"New request should be at index 1"
);
}
#[test]
fn test_chr_jepsen_threaded() {
use super::message::{ClientRequest, ClientResult};
let paths = TestPaths::new("chr_redirect", 2);
let mut network = MockNetwork::new(2);
let ep0 = network.create_endpoint(0).unwrap();
let ep1 = network.create_endpoint(1).unwrap();
let committed_state_0 = Arc::new(CommittedState::new());
let committed_state_1 = Arc::new(CommittedState::new());
let writer_0 = LogWriter::create(paths.log(0), 1).unwrap();
let writer_1 = LogWriter::create(paths.log(1), 1).unwrap();
let mut node_0: VsrNode<BankApp> = VsrNode::new_primary(
0,
2,
0,
writer_0,
None, committed_state_0.clone(),
ep0,
None,
paths.manifest(0),
).unwrap();
let mut node_1: VsrNode<BankApp> = VsrNode::new_backup(
1,
2,
0,
writer_1,
committed_state_1.clone(),
ep1,
None,
paths.manifest(1),
).unwrap();
let request = ClientRequest {
client_id: 1,
sequence_number: 1,
payload: b"test".to_vec(),
};
let response = node_1.handle_client_request(&request);
assert!(
matches!(response.result, ClientResult::NotThePrimary { leader_hint: Some(0) }),
"Backup should redirect to Primary (node 0)"
);
let response2 = node_0.handle_client_request(&request);
assert!(
matches!(response2.result, ClientResult::Pending),
"Primary should accept request"
);
}
#[test]
fn test_outbox_exactly_once_with_failure() {
use super::node::ELECTION_TIMEOUT;
use crate::kernel::traits::{EffectId, SideEffectStatus};
use crate::kernel::side_effect_manager::{
MockEffectExecutor, MockAcknowledgeSubmitter, SideEffectManager, SideEffectManagerConfig,
};
let paths = TestPaths::new("outbox", 3);
let mut network = MockNetwork::new(3);
let ep0 = network.create_endpoint(0).unwrap();
let ep1 = network.create_endpoint(1).unwrap();
let ep2 = network.create_endpoint(2).unwrap();
let committed_state_0 = Arc::new(CommittedState::new());
let committed_state_1 = Arc::new(CommittedState::new());
let committed_state_2 = Arc::new(CommittedState::new());
let writer_0 = LogWriter::create(paths.log(0), 1).unwrap();
let writer_1 = LogWriter::create(paths.log(1), 1).unwrap();
let writer_2 = LogWriter::create(paths.log(2), 1).unwrap();
let reader_0 = LogReader::open(paths.log(0), committed_state_0.clone()).unwrap();
let reader_1 = LogReader::open(paths.log(1), committed_state_1.clone()).unwrap();
let reader_2 = LogReader::open(paths.log(2), committed_state_2.clone()).unwrap();
let app_0 = BankApp::new();
let app_1 = BankApp::new();
let app_2 = BankApp::new();
let executor_0 = Executor::new(reader_0, app_0, 0);
let executor_1 = Executor::new(reader_1, app_1, 0);
let executor_2 = Executor::new(reader_2, app_2, 0);
let reader_0_catchup = LogReader::open(paths.log(0), committed_state_0.clone()).unwrap();
let mut node_0: VsrNode<BankApp> = VsrNode::new_primary(
0,
3,
0,
writer_0,
Some(reader_0_catchup),
committed_state_0.clone(),
ep0,
Some(executor_0),
paths.manifest(0),
).unwrap();
let mut node_1: VsrNode<BankApp> = VsrNode::new_backup(
1,
3,
0,
writer_1,
committed_state_1.clone(),
ep1,
Some(executor_1),
paths.manifest(1),
).unwrap();
let mut node_2: VsrNode<BankApp> = VsrNode::new_backup(
2,
3,
0,
writer_2,
committed_state_2.clone(),
ep2,
Some(executor_2),
paths.manifest(2),
).unwrap();
let mock_executor_0 = MockEffectExecutor::new();
let mock_submitter_0 = MockAcknowledgeSubmitter::new();
let mock_executor_1 = MockEffectExecutor::new();
let mock_submitter_1 = MockAcknowledgeSubmitter::new();
let client_id = 42u64;
let sequence_number = 1u64;
let send_email = BankEvent::SendEmail {
to: "alice@example.com".to_string(),
subject: "Welcome!".to_string(),
client_id,
sequence_number,
};
let payload = serialize_event(&send_email);
let index = node_0.submit(&payload).unwrap();
assert_eq!(index, 0);
for _ in 0..20 {
thread::sleep(Duration::from_millis(5));
node_1.process_all();
node_2.process_all();
thread::sleep(Duration::from_millis(5));
node_0.process_all();
if node_0.committed_index() == Some(0)
&& node_1.committed_index() == Some(0)
&& node_2.committed_index() == Some(0) {
break;
}
}
assert_eq!(node_0.committed_index(), Some(0), "Node 0 should have committed");
assert_eq!(node_1.committed_index(), Some(0), "Node 1 should have committed");
assert_eq!(node_2.committed_index(), Some(0), "Node 2 should have committed");
let effect_id = EffectId::new(client_id, sequence_number, 0);
if let Some(ref executor) = node_0.executor {
let outbox = executor.state().outbox();
assert_eq!(outbox.pending_count(), 1, "Node 0 should have 1 pending effect");
let entry = outbox.get(&effect_id).expect("Effect should exist");
assert_eq!(entry.status, SideEffectStatus::Pending);
}
if let Some(ref executor) = node_1.executor {
let outbox = executor.state().outbox();
assert_eq!(outbox.pending_count(), 1, "Node 1 should have 1 pending effect");
}
let manager_0 = SideEffectManager::new(
SideEffectManagerConfig::default(),
mock_executor_0.as_executor(),
mock_submitter_0.as_submitter(),
);
manager_0.set_primary_with_token(true, node_0.current_view());
if let Some(ref executor) = node_0.executor {
let processed = manager_0.process_pending(executor.state().outbox());
assert_eq!(processed, 1, "Should have processed 1 effect");
}
assert_eq!(mock_executor_0.get_executed().len(), 1, "Effect should have been executed once");
assert_eq!(mock_submitter_0.get_submitted().len(), 1, "Acknowledge should have been submitted");
network.disconnect(0);
thread::sleep(ELECTION_TIMEOUT + Duration::from_millis(100));
node_1.tick();
node_2.tick();
for _ in 0..30 {
thread::sleep(Duration::from_millis(10));
node_1.process_all();
node_2.process_all();
node_1.tick();
node_2.tick();
if node_1.role == NodeRole::Primary || node_2.role == NodeRole::Primary {
break;
}
}
let new_primary = if node_1.role == NodeRole::Primary {
&mut node_1
} else if node_2.role == NodeRole::Primary {
&mut node_2
} else {
panic!("No new primary was elected!");
};
assert!(new_primary.current_view() > 0, "View should have changed");
if let Some(ref executor) = new_primary.executor {
let outbox = executor.state().outbox();
assert_eq!(outbox.pending_count(), 1, "New primary should still have 1 pending effect");
let entry = outbox.get(&effect_id).expect("Effect should still exist");
assert_eq!(entry.status, SideEffectStatus::Pending, "Effect should still be Pending");
}
let manager_1 = SideEffectManager::new(
SideEffectManagerConfig::default(),
mock_executor_1.as_executor(),
mock_submitter_1.as_submitter(),
);
manager_1.set_primary_with_token(true, new_primary.current_view());
if let Some(ref executor) = new_primary.executor {
let processed = manager_1.process_pending(executor.state().outbox());
assert_eq!(processed, 1, "New primary should re-execute the effect");
}
assert_eq!(mock_executor_1.get_executed().len(), 1, "Effect should have been executed on new primary");
let ack_event = BankEvent::SystemAcknowledgeEffect { effect_id };
let ack_payload = serialize_event(&ack_event);
let ack_index = new_primary.submit(&ack_payload).unwrap();
for _ in 0..20 {
thread::sleep(Duration::from_millis(5));
node_1.process_all();
node_2.process_all();
thread::sleep(Duration::from_millis(5));
node_1.tick();
node_2.tick();
let committed_1 = node_1.committed_index();
let committed_2 = node_2.committed_index();
if committed_1 >= Some(ack_index) && committed_2 >= Some(ack_index) {
break;
}
}
if let Some(ref executor) = node_1.executor {
let outbox = executor.state().outbox();
assert_eq!(outbox.pending_count(), 0, "Node 1 should have 0 pending effects after ack");
let entry = outbox.get(&effect_id).expect("Effect should still exist");
assert_eq!(entry.status, SideEffectStatus::Acknowledged, "Effect should be Acknowledged");
}
if let Some(ref executor) = node_2.executor {
let outbox = executor.state().outbox();
assert_eq!(outbox.pending_count(), 0, "Node 2 should have 0 pending effects after ack");
let entry = outbox.get(&effect_id).expect("Effect should still exist");
assert_eq!(entry.status, SideEffectStatus::Acknowledged, "Effect should be Acknowledged");
}
}
#[test]
fn test_chr_survivability() {
use super::message::{ClientRequest, ClientResult};
let paths = TestPaths::new("survivability", 3);
let mut network = MockNetwork::new(3);
let ep0 = network.create_endpoint(0).unwrap();
let ep1 = network.create_endpoint(1).unwrap();
let ep2 = network.create_endpoint(2).unwrap();
let committed_state_0 = Arc::new(CommittedState::new());
let committed_state_1 = Arc::new(CommittedState::new());
let committed_state_2 = Arc::new(CommittedState::new());
let writer_0 = LogWriter::create(paths.log(0), 1).unwrap();
let writer_1 = LogWriter::create(paths.log(1), 1).unwrap();
let writer_2 = LogWriter::create(paths.log(2), 1).unwrap();
let reader_0 = LogReader::open(paths.log(0), committed_state_0.clone()).unwrap();
let reader_1 = LogReader::open(paths.log(1), committed_state_1.clone()).unwrap();
let reader_2 = LogReader::open(paths.log(2), committed_state_2.clone()).unwrap();
let app_0 = BankApp::new();
let app_1 = BankApp::new();
let app_2 = BankApp::new();
let executor_0 = Executor::new(reader_0, app_0, 0);
let executor_1 = Executor::new(reader_1, app_1, 0);
let executor_2 = Executor::new(reader_2, app_2, 0);
let reader_0_catchup = LogReader::open(paths.log(0), committed_state_0.clone()).unwrap();
let mut node_0: VsrNode<BankApp> = VsrNode::new_primary(
0,
3,
0,
writer_0,
Some(reader_0_catchup),
committed_state_0.clone(),
ep0,
Some(executor_0),
paths.manifest(0),
).unwrap();
let mut node_1: VsrNode<BankApp> = VsrNode::new_backup(
1,
3,
0,
writer_1,
committed_state_1.clone(),
ep1,
Some(executor_1),
paths.manifest(1),
).unwrap();
let mut node_2: VsrNode<BankApp> = VsrNode::new_backup(
2,
3,
0,
writer_2,
committed_state_2.clone(),
ep2,
Some(executor_2),
paths.manifest(2),
).unwrap();
const MAX_INFLIGHT: usize = 50;
node_0.set_max_inflight_requests(MAX_INFLIGHT);
const TOTAL_REQUESTS: usize = 500;
let mut accepted_count = 0;
let mut rejected_count = 0;
let mut accepted_sequences: Vec<u64> = Vec::new();
for i in 0..TOTAL_REQUESTS {
let deposit = BankEvent::Deposit {
user: format!("User{}", i),
amount: 100,
};
let payload = serialize_event(&deposit);
let request = ClientRequest {
client_id: 1000 + i as u64,
sequence_number: 1,
payload,
};
let response = node_0.handle_client_request(&request);
match &response.result {
ClientResult::Pending => {
accepted_count += 1;
accepted_sequences.push(request.client_id);
}
ClientResult::Error { message } => {
assert!(
message.contains("System Overloaded"),
"Expected 'System Overloaded' error, got: {}",
message
);
rejected_count += 1;
}
other => {
panic!("Unexpected response: {:?}", other);
}
}
}
assert_eq!(
accepted_count, MAX_INFLIGHT,
"Expected {} accepted requests, got {}",
MAX_INFLIGHT, accepted_count
);
assert_eq!(
rejected_count, TOTAL_REQUESTS - MAX_INFLIGHT,
"Expected {} rejected requests, got {}",
TOTAL_REQUESTS - MAX_INFLIGHT, rejected_count
);
assert_eq!(
node_0.inflight_count(), MAX_INFLIGHT,
"In-flight count should be {}",
MAX_INFLIGHT
);
assert_eq!(
node_0.rejected_count() as usize, rejected_count,
"Rejected count metric should match"
);
for _ in 0..200 {
thread::sleep(Duration::from_millis(2));
node_1.process_all();
node_2.process_all();
thread::sleep(Duration::from_millis(2));
node_0.process_all();
let _ = node_0.check_committed_requests();
let node_0_committed = node_0.committed_index();
let node_1_committed = node_1.committed_index();
let node_2_committed = node_2.committed_index();
if node_0_committed == Some(MAX_INFLIGHT as u64 - 1)
&& node_1_committed == Some(MAX_INFLIGHT as u64 - 1)
&& node_2_committed == Some(MAX_INFLIGHT as u64 - 1) {
break;
}
}
assert_eq!(
node_0.committed_index(),
Some(MAX_INFLIGHT as u64 - 1),
"All accepted requests should be committed on primary"
);
assert_eq!(
node_0.inflight_count(), 0,
"In-flight count should be 0 after all commits"
);
assert_eq!(
node_1.committed_index(),
Some(MAX_INFLIGHT as u64 - 1),
"Node 1 should have same committed index"
);
assert_eq!(
node_2.committed_index(),
Some(MAX_INFLIGHT as u64 - 1),
"Node 2 should have same committed index"
);
let new_deposit = BankEvent::Deposit {
user: "NewUser".to_string(),
amount: 500,
};
let new_payload = serialize_event(&new_deposit);
let new_request = ClientRequest {
client_id: 9999,
sequence_number: 1,
payload: new_payload,
};
let new_response = node_0.handle_client_request(&new_request);
assert!(
matches!(new_response.result, ClientResult::Pending),
"New request should be accepted after in-flight drops: {:?}",
new_response.result
);
for _ in 0..20 {
thread::sleep(Duration::from_millis(5));
node_1.process_all();
node_2.process_all();
thread::sleep(Duration::from_millis(5));
node_0.process_all();
let _ = node_0.check_committed_requests();
if node_0.committed_index() == Some(MAX_INFLIGHT as u64) {
break;
}
}
assert_eq!(
node_0.committed_index(),
Some(MAX_INFLIGHT as u64),
"New request should be committed"
);
}
#[test]
fn test_scheduler_fairness() {
use super::node::RequestBatcher;
let mut batcher = RequestBatcher::new();
let client_a_id = 1000;
for i in 0..100 {
let payload = format!("spammer_request_{}", i).into_bytes();
batcher.add(payload, client_a_id, i as u64);
}
let client_b_id = 2000;
for i in 0..10 {
let payload = format!("admin_request_{}", i).into_bytes();
batcher.add(payload, client_b_id, i as u64);
}
assert_eq!(batcher.client_count(), 2, "Should have 2 unique clients");
let (payloads, clients) = batcher.take_batch();
assert_eq!(payloads.len(), 110, "Should have 110 total requests");
assert_eq!(clients.len(), 110, "Should have 110 client entries");
let mut client_a_in_first_20 = 0;
let mut client_b_in_first_20 = 0;
for (client_id, _seq) in clients.iter().take(20) {
if *client_id == client_a_id {
client_a_in_first_20 += 1;
} else if *client_id == client_b_id {
client_b_in_first_20 += 1;
}
}
assert_eq!(
client_a_in_first_20, 10,
"First 20 entries should have 10 from Client A (got {})",
client_a_in_first_20
);
assert_eq!(
client_b_in_first_20, 10,
"First 20 entries should have 10 from Client B (got {})",
client_b_in_first_20
);
let mut max_consecutive_a = 0;
let mut current_consecutive_a = 0;
for (i, (client_id, _)) in clients.iter().enumerate() {
if *client_id == client_a_id {
current_consecutive_a += 1;
if current_consecutive_a > max_consecutive_a {
max_consecutive_a = current_consecutive_a;
}
} else {
current_consecutive_a = 0;
}
if i < 20 {
assert!(
current_consecutive_a <= 1,
"Before B exhausted, should not have consecutive A requests (index {})",
i
);
}
}
let remaining_a: usize = clients.iter().skip(20).filter(|(id, _)| *id == client_a_id).count();
assert_eq!(
remaining_a, 90,
"After B exhausted, remaining 90 should be from A"
);
assert!(!batcher.has_pending(), "Batcher should be empty after take_batch");
assert_eq!(batcher.pending_count(), 0, "Pending count should be 0");
assert_eq!(batcher.client_count(), 0, "Client count should be 0");
}
#[test]
fn test_deterministic_drift_protection() {
use super::message::{ClientRequest, ClientResult, PreparedEntry, VsrMessage};
let timestamp_ns: u64 = 1_700_000_000_000_000_000;
let prepare_batch = VsrMessage::PrepareBatch {
view: 1,
start_index: 0,
entries: vec![
PreparedEntry {
index: 0,
payload: b"test_payload".to_vec(),
},
],
commit_index: None,
timestamp_ns,
};
if let VsrMessage::PrepareBatch { timestamp_ns: ts, .. } = prepare_batch {
assert_eq!(ts, timestamp_ns, "PrepareBatch should carry the exact timestamp");
} else {
panic!("Expected PrepareBatch message");
}
let prepare = VsrMessage::Prepare {
view: 1,
index: 0,
payload: b"test".to_vec(),
commit_index: None,
timestamp_ns,
};
if let VsrMessage::Prepare { timestamp_ns: ts, .. } = prepare {
assert_eq!(ts, timestamp_ns, "Prepare should carry the exact timestamp");
} else {
panic!("Expected Prepare message");
}
let prev_hash: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
let index: u64 = 42;
let derive_seed = |prev_hash: &[u8; 16], index: u64| -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(prev_hash);
hasher.update(&index.to_le_bytes());
*hasher.finalize().as_bytes()
};
let seed1 = derive_seed(&prev_hash, index);
let seed2 = derive_seed(&prev_hash, index);
assert_eq!(seed1, seed2, "Same prev_hash and index should produce same seed");
let seed3 = derive_seed(&prev_hash, index + 1);
assert_ne!(seed1, seed3, "Different index should produce different seed");
let different_prev_hash: [u8; 16] = [16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
let seed4 = derive_seed(&different_prev_hash, index);
assert_ne!(seed1, seed4, "Different prev_hash should produce different seed");
use crate::kernel::traits::BlockTime;
let block_time1 = BlockTime::from_nanos(timestamp_ns);
let block_time2 = BlockTime::from_nanos(timestamp_ns);
assert_eq!(block_time1.as_nanos(), block_time2.as_nanos(), "BlockTime should be deterministic");
assert_eq!(block_time1.as_secs(), 1_700_000_000, "BlockTime seconds should match");
}
#[test]
fn test_async_durability_mode() {
use crate::engine::durability::DurabilityWorker;
use super::node::HEARTBEAT_INTERVAL;
let paths = TestPaths::new("async_durability", 2);
let worker = DurabilityWorker::create(paths.log(0), 1).unwrap();
let handle = worker.handle();
let writer_1 = LogWriter::create(paths.log(1), 1).unwrap();
let committed_state_0 = Arc::new(CommittedState::new());
let committed_state_1 = Arc::new(CommittedState::new());
let mut network = MockNetwork::new(2);
let ep_0 = network.create_endpoint(0).unwrap();
let ep_1 = network.create_endpoint(1).unwrap();
let dummy_writer_0 = LogWriter::create(
&PathBuf::from("/tmp/chr_async_durability_dummy.log"),
1
).unwrap();
let mut node_0: VsrNode<BankApp> = VsrNode::new_primary(
0,
2,
1,
dummy_writer_0,
None,
committed_state_0.clone(),
ep_0,
None, paths.manifest(0),
).unwrap();
node_0.enable_async_durability(handle.clone());
assert!(node_0.is_async_durability_enabled());
let mut node_1: VsrNode<BankApp> = VsrNode::new_backup(
1,
2,
1,
writer_1,
committed_state_1.clone(),
ep_1,
None,
paths.manifest(1),
).unwrap();
let event1 = BankEvent::Deposit { user: "alice".to_string(), amount: 100 };
let event2 = BankEvent::Deposit { user: "bob".to_string(), amount: 200 };
node_0.batcher.add(serialize_event(&event1), 1, 1);
node_0.batcher.add(serialize_event(&event2), 2, 1);
let tracking = node_0.flush_batch().unwrap();
assert!(tracking.is_empty(), "Async mode should return empty tracking");
assert_eq!(node_0.pending_durability_count(), 1);
std::thread::sleep(HEARTBEAT_INTERVAL + Duration::from_millis(10));
node_0.tick();
let msg = node_1.network.try_recv();
assert!(msg.is_some(), "Backup should receive heartbeat");
let completions = worker.drain_completions();
assert!(!completions.is_empty(), "Should have durability completions");
let processed = node_0.process_durability_completions(&completions);
assert_eq!(processed, 1, "Should process one completion");
assert_eq!(node_0.pending_durability_count(), 0);
let msg = node_1.network.try_recv();
assert!(msg.is_some(), "Backup should receive PrepareBatch");
if let Some((from, VsrMessage::PrepareBatch { view, start_index, entries, .. })) = msg {
assert_eq!(from, 0);
assert_eq!(view, 1);
assert_eq!(start_index, 0);
assert_eq!(entries.len(), 2);
} else {
panic!("Expected PrepareBatch message");
}
worker.shutdown_and_join().unwrap();
let _ = fs::remove_file("/tmp/chr_async_durability_dummy.log");
}
#[test]
fn test_heartbeat_decoupled_from_flush() {
use super::node::HEARTBEAT_INTERVAL;
let paths = TestPaths::new("heartbeat_decoupled", 2);
let writer_0 = LogWriter::create(paths.log(0), 1).unwrap();
let writer_1 = LogWriter::create(paths.log(1), 1).unwrap();
let committed_state_0 = Arc::new(CommittedState::new());
let committed_state_1 = Arc::new(CommittedState::new());
let mut network = MockNetwork::new(2);
let ep_0 = network.create_endpoint(0).unwrap();
let ep_1 = network.create_endpoint(1).unwrap();
let mut node_0: VsrNode<BankApp> = VsrNode::new_primary(
0,
2,
1,
writer_0,
None,
committed_state_0.clone(),
ep_0,
None,
paths.manifest(0),
).unwrap();
let mut node_1: VsrNode<BankApp> = VsrNode::new_backup(
1,
2,
1,
writer_1,
committed_state_1.clone(),
ep_1,
None,
paths.manifest(1),
).unwrap();
std::thread::sleep(HEARTBEAT_INTERVAL + Duration::from_millis(10));
node_0.tick();
let msg = node_1.network.try_recv();
assert!(msg.is_some(), "Backup should receive heartbeat");
if let Some((from, VsrMessage::Commit { view, commit_index })) = msg {
assert_eq!(from, 0);
assert_eq!(view, 1);
assert_eq!(commit_index, 0); } else {
panic!("Expected Commit (heartbeat) message");
}
assert_eq!(HEARTBEAT_INTERVAL, Duration::from_millis(100));
assert_eq!(super::node::ELECTION_TIMEOUT, Duration::from_millis(1000));
}
#[test]
fn test_chronon_io_isolation() {
use crate::engine::durability::DurabilityWorker;
let paths = TestPaths::new("chronon_io_isolation", 2);
let worker = DurabilityWorker::create(paths.log(0), 1).unwrap();
worker.set_stalled(true);
let writer_1 = LogWriter::create(paths.log(1), 1).unwrap();
let committed_state_0 = Arc::new(CommittedState::new());
let committed_state_1 = Arc::new(CommittedState::new());
let mut network = MockNetwork::new(2);
let ep_0 = network.create_endpoint(0).unwrap();
let ep_1 = network.create_endpoint(1).unwrap();
let dummy_writer_0 = LogWriter::create(
&PathBuf::from("/tmp/chr_chronon_io_isolation_dummy.log"),
1,
)
.unwrap();
let mut node_0: VsrNode<BankApp> = VsrNode::new_primary(
0,
2,
1,
dummy_writer_0,
None,
committed_state_0.clone(),
ep_0,
None,
paths.manifest(0),
)
.unwrap();
node_0.enable_async_durability(worker.handle());
let mut node_1: VsrNode<BankApp> = VsrNode::new_backup(
1,
2,
1,
writer_1,
committed_state_1.clone(),
ep_1,
None,
paths.manifest(1),
)
.unwrap();
node_0
.batcher
.add(serialize_event(&BankEvent::Deposit { user: "a".to_string(), amount: 1 }), 1, 1);
node_0
.batcher
.add(serialize_event(&BankEvent::Deposit { user: "b".to_string(), amount: 1 }), 2, 1);
std::thread::sleep(super::node::MAX_BATCH_DELAY + Duration::from_millis(2));
node_0.tick();
assert_eq!(node_0.pending_durability_count(), 1);
assert!(worker.drain_completions().is_empty());
let start = std::time::Instant::now();
let mut processed_msgs = 0usize;
while start.elapsed() < super::node::ELECTION_TIMEOUT + Duration::from_millis(250) {
node_0.tick();
while node_1.process_one() {
processed_msgs += 1;
}
assert_eq!(node_1.role, NodeRole::Backup);
assert!(!node_1.tick());
std::thread::sleep(Duration::from_millis(10));
}
assert!(processed_msgs > 0);
assert!(node_1.last_primary_contact.elapsed() < super::node::ELECTION_TIMEOUT);
assert_eq!(node_0.pending_durability_count(), 1);
assert!(worker.drain_completions().is_empty());
worker.set_stalled(false);
let unstall_start = std::time::Instant::now();
let mut completions = Vec::new();
while unstall_start.elapsed() < Duration::from_millis(500) {
completions = worker.drain_completions();
if !completions.is_empty() {
break;
}
std::thread::sleep(Duration::from_millis(5));
}
assert!(!completions.is_empty());
assert_eq!(node_0.process_durability_completions(&completions), 1);
assert_eq!(node_0.pending_durability_count(), 0);
while node_1.process_one() {}
assert_eq!(node_1.next_expected_index, 2);
worker.shutdown_and_join().unwrap();
let _ = fs::remove_file("/tmp/chr_chronon_io_isolation_dummy.log");
}