#![cfg(all(feature = "replication", feature = "var-collections"))]
use std::net::{SocketAddr, TcpListener};
use std::sync::Arc;
use std::time::{Duration, Instant};
use armdb::Config;
use armdb::ShutdownSignal;
use armdb::VarTree;
use armdb::replication::{ReplicationClientOptions, ReplicationRegistry, ReplicationServerOptions};
use armdb::serialize_entry;
fn next_bind_addr() -> SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
drop(listener);
addr
}
fn wait_until(timeout: Duration, mut f: impl FnMut() -> bool) -> bool {
let start = Instant::now();
loop {
if f() {
return true;
}
if start.elapsed() >= timeout {
return f(); }
std::thread::sleep(Duration::from_millis(20));
}
}
fn test_cfg() -> Config {
Config::test()
}
#[test]
fn catch_up_smoke() {
let leader_dir = tempfile::tempdir().unwrap();
let follower_dir = tempfile::tempdir().unwrap();
let addr = next_bind_addr();
let leader = VarTree::<[u8; 8]>::open(leader_dir.path(), test_cfg()).unwrap();
for i in 0u64..512 {
leader
.put(&i.to_be_bytes(), format!("v{i}").as_bytes())
.unwrap();
}
let leader_signal = ShutdownSignal::new();
let _server = leader
.start_replication_server(addr, leader_signal.clone())
.unwrap();
let follower = Arc::new(VarTree::<[u8; 8]>::open(follower_dir.path(), test_cfg()).unwrap());
let registry = Arc::new(ReplicationRegistry::new(follower.as_replication_target()));
let follower_signal = ShutdownSignal::new();
let _client = follower
.start_replication_client(addr, registry, follower_signal.clone())
.unwrap();
assert!(
wait_until(Duration::from_secs(15), || {
(0u64..512).all(|i| follower.contains(&i.to_be_bytes()))
}),
"follower did not catch up within 15s (len={})",
follower.len()
);
follower_signal.shutdown();
leader_signal.shutdown();
}
#[test]
fn streaming_after_catchup() {
let leader_dir = tempfile::tempdir().unwrap();
let follower_dir = tempfile::tempdir().unwrap();
let addr = next_bind_addr();
let leader = VarTree::<[u8; 8]>::open(leader_dir.path(), test_cfg()).unwrap();
for i in 0u64..64 {
leader.put(&i.to_be_bytes(), b"initial").unwrap();
}
let leader_signal = ShutdownSignal::new();
let _server = leader
.start_replication_server(addr, leader_signal.clone())
.unwrap();
let follower = Arc::new(VarTree::<[u8; 8]>::open(follower_dir.path(), test_cfg()).unwrap());
let registry = Arc::new(ReplicationRegistry::new(follower.as_replication_target()));
let follower_signal = ShutdownSignal::new();
let _client = follower
.start_replication_client(addr, registry, follower_signal.clone())
.unwrap();
assert!(
wait_until(Duration::from_secs(10), || { follower.len() >= 64 }),
"catch-up did not complete within 10s"
);
for i in 512u64..768 {
leader.put(&i.to_be_bytes(), b"streamed").unwrap();
}
assert!(
wait_until(Duration::from_secs(5), || {
(512u64..768).all(|i| follower.contains(&i.to_be_bytes()))
}),
"follower did not receive streamed entries within 5s (len={})",
follower.len()
);
follower_signal.shutdown();
leader_signal.shutdown();
}
#[test]
fn reconnect_no_loss_no_duplicates() {
let leader_dir = tempfile::tempdir().unwrap();
let follower_dir = tempfile::tempdir().unwrap();
let addr = next_bind_addr();
let leader = VarTree::<[u8; 8]>::open(leader_dir.path(), test_cfg()).unwrap();
let leader_signal = ShutdownSignal::new();
let _server = leader
.start_replication_server_with_options(
addr,
leader_signal.clone(),
ReplicationServerOptions {
heartbeat_interval_secs: 1,
..Default::default()
},
)
.unwrap();
let follower = Arc::new(VarTree::<[u8; 8]>::open(follower_dir.path(), test_cfg()).unwrap());
let registry = Arc::new(ReplicationRegistry::new(follower.as_replication_target()));
let signal1 = ShutdownSignal::new();
let client1 = follower
.start_replication_client(addr, registry.clone(), signal1.clone())
.unwrap();
for i in 0u64..100 {
leader.put(&i.to_be_bytes(), b"batch1").unwrap();
}
assert!(
wait_until(Duration::from_secs(10), || follower.len() >= 100),
"follower did not receive first 100 entries"
);
drop(client1);
signal1.shutdown();
std::thread::sleep(Duration::from_millis(50));
for i in 100u64..200 {
leader.put(&i.to_be_bytes(), b"batch2").unwrap();
}
let signal2 = ShutdownSignal::new();
let _client2 = follower
.start_replication_client_with_options(
addr,
registry,
signal2.clone(),
ReplicationClientOptions {
reconnect_base_ms: 50,
reconnect_max_ms: 200,
..Default::default()
},
)
.unwrap();
assert!(
wait_until(Duration::from_secs(15), || follower.len() >= 200),
"follower did not receive all 200 entries after reconnect (len={})",
follower.len()
);
assert_eq!(
follower.len(),
200,
"expected 200 unique entries, got {}",
follower.len()
);
signal2.shutdown();
leader_signal.shutdown();
}
#[test]
fn reconnect_uses_in_memory_last_applied() {
use armdb::replication::protocol::{
EntryBatch, ShardInfo, SyncRequest, VAR_PROTOCOL_VERSION, WireEntry, read_frame,
write_frame,
};
use std::io::BufReader;
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let mock = std::thread::spawn(move || {
for (round, expected_from) in [(0, 1), (1, 11)] {
let (stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut writer = stream;
let request = read_frame(&mut reader).unwrap();
let request = SyncRequest::decode(&request.payload).unwrap();
assert_eq!(request.from_gsn, expected_from);
write_frame(
&mut writer,
&ShardInfo {
protocol_version: VAR_PROTOCOL_VERSION,
shard_count: 1,
max_file_size: 256 * 1024 * 1024,
}
.encode(),
)
.unwrap();
let entries = ((round * 10 + 1)..=(round * 10 + 10))
.map(|gsn| {
let key = (gsn as u64).to_be_bytes();
let data = serialize_entry(gsn as u64, &key, b"v", false);
WireEntry {
entry_len: data.len() as u32,
key_len: 8,
gsn: gsn as u64,
data,
}
})
.collect();
write_frame(
&mut writer,
&EntryBatch {
shard_id: 0,
entries,
}
.encode(),
)
.unwrap();
}
});
let dir = tempfile::tempdir().unwrap();
let follower = Arc::new(
VarTree::<[u8; 8]>::open(
dir.path(),
Config::balanced().shard_count(1).hints(true).build(),
)
.unwrap(),
);
let registry = Arc::new(ReplicationRegistry::new(follower.as_replication_target()));
let signal = ShutdownSignal::new();
let _client = follower
.start_replication_client_with_options(
addr,
registry,
signal.clone(),
ReplicationClientOptions {
reconnect_base_ms: 20,
reconnect_max_ms: 20,
heartbeat_interval_secs: 1,
},
)
.unwrap();
assert!(wait_until(Duration::from_secs(5), || follower.len() == 20));
signal.shutdown();
mock.join().unwrap();
}
#[test]
fn rotation_during_streaming() {
let leader_dir = tempfile::tempdir().unwrap();
let follower_dir = tempfile::tempdir().unwrap();
let addr = next_bind_addr();
let cfg = Config {
shard_count: 2,
max_file_size: 32 * 1024,
write_buffer_size: 16 * 1024,
..Config::balanced().build()
};
let leader = VarTree::<[u8; 8]>::open(leader_dir.path(), cfg.clone()).unwrap();
let leader_signal = ShutdownSignal::new();
let _server = leader
.start_replication_server(addr, leader_signal.clone())
.unwrap();
let follower = Arc::new(VarTree::<[u8; 8]>::open(follower_dir.path(), cfg).unwrap());
let registry = Arc::new(ReplicationRegistry::new(follower.as_replication_target()));
let follower_signal = ShutdownSignal::new();
let _client = follower
.start_replication_client(addr, registry, follower_signal.clone())
.unwrap();
let value = b"rotation_test_value_32bytes_xxxxx";
for i in 0u64..1000 {
leader.put(&i.to_be_bytes(), value).unwrap();
}
assert!(
wait_until(Duration::from_secs(15), || { follower.len() >= 1000 }),
"follower did not catch up after rotations (len={})",
follower.len()
);
for i in [0u64, 499, 999] {
assert!(
follower.get(&i.to_be_bytes()).is_some(),
"key {i} missing from follower after rotation"
);
}
follower_signal.shutdown();
leader_signal.shutdown();
}
#[test]
fn multi_shard() {
let leader_dir = tempfile::tempdir().unwrap();
let follower_dir = tempfile::tempdir().unwrap();
let addr = next_bind_addr();
let cfg = Config {
shard_count: 4,
..Config::balanced().build()
};
let leader = VarTree::<[u8; 8]>::open(leader_dir.path(), cfg.clone()).unwrap();
for i in 0u64..200 {
leader.put(&i.to_be_bytes(), b"multi").unwrap();
}
let leader_signal = ShutdownSignal::new();
let _server = leader
.start_replication_server(addr, leader_signal.clone())
.unwrap();
let follower = Arc::new(VarTree::<[u8; 8]>::open(follower_dir.path(), cfg).unwrap());
let registry = Arc::new(ReplicationRegistry::new(follower.as_replication_target()));
let follower_signal = ShutdownSignal::new();
let _client = follower
.start_replication_client(addr, registry, follower_signal.clone())
.unwrap();
assert!(
wait_until(Duration::from_secs(15), || {
(0u64..200).all(|i| follower.contains(&i.to_be_bytes()))
}),
"multi-shard follower did not receive all entries (len={})",
follower.len()
);
follower_signal.shutdown();
leader_signal.shutdown();
}
#[cfg(feature = "encryption")]
#[test]
fn encrypted_catchup() {
let leader_dir = tempfile::tempdir().unwrap();
let follower_dir = tempfile::tempdir().unwrap();
let addr = next_bind_addr();
let cfg = Config {
shard_count: 2,
write_buffer_size: 8192,
#[cfg(feature = "encryption")]
encryption_key: Some([0x42u8; 32]),
..Config::balanced().build()
};
let leader = VarTree::<[u8; 8]>::open(leader_dir.path(), cfg.clone()).unwrap();
for i in 0u64..100 {
leader.put(&i.to_be_bytes(), b"encrypted").unwrap();
}
let leader_signal = ShutdownSignal::new();
let _server = leader
.start_replication_server(addr, leader_signal.clone())
.unwrap();
let follower = Arc::new(VarTree::<[u8; 8]>::open(follower_dir.path(), cfg).unwrap());
let registry = Arc::new(ReplicationRegistry::new(follower.as_replication_target()));
let follower_signal = ShutdownSignal::new();
let _client = follower
.start_replication_client(addr, registry, follower_signal.clone())
.unwrap();
assert!(
wait_until(Duration::from_secs(15), || {
(0u64..100).all(|i| follower.contains(&i.to_be_bytes()))
}),
"encrypted follower did not catch up (len={})",
follower.len()
);
follower_signal.shutdown();
leader_signal.shutdown();
}
#[test]
fn reject_second_follower() {
let leader_dir = tempfile::tempdir().unwrap();
let follower1_dir = tempfile::tempdir().unwrap();
let follower2_dir = tempfile::tempdir().unwrap();
let addr = next_bind_addr();
let leader = VarTree::<[u8; 8]>::open(leader_dir.path(), test_cfg()).unwrap();
let leader_signal = ShutdownSignal::new();
let _server = leader
.start_replication_server(addr, leader_signal.clone())
.unwrap();
let follower1 = Arc::new(VarTree::<[u8; 8]>::open(follower1_dir.path(), test_cfg()).unwrap());
let registry1 = Arc::new(ReplicationRegistry::new(follower1.as_replication_target()));
let signal1 = ShutdownSignal::new();
let _client1 = follower1
.start_replication_client(addr, registry1, signal1.clone())
.unwrap();
std::thread::sleep(Duration::from_millis(300));
let follower2 = Arc::new(VarTree::<[u8; 8]>::open(follower2_dir.path(), test_cfg()).unwrap());
let registry2 = Arc::new(ReplicationRegistry::new(follower2.as_replication_target()));
let signal2 = ShutdownSignal::new();
let _client2 = follower2
.start_replication_client(addr, registry2, signal2.clone())
.unwrap();
for i in 0u64..50 {
leader
.put(&i.to_be_bytes(), format!("v{i}").as_bytes())
.unwrap();
}
assert!(
wait_until(Duration::from_secs(10), || { follower1.len() >= 50 }),
"follower1 did not receive entries (len={})",
follower1.len()
);
std::thread::sleep(Duration::from_secs(3));
assert_eq!(
follower2.len(),
0,
"follower2 should be empty but has {} entries",
follower2.len()
);
signal1.shutdown();
signal2.shutdown();
leader_signal.shutdown();
}
#[test]
fn low_rate_stream_advances_min_replicated_gsn() {
use std::sync::atomic::Ordering;
let leader_dir = tempfile::tempdir().unwrap();
let follower_dir = tempfile::tempdir().unwrap();
let addr = next_bind_addr();
let config = Config::balanced().shard_count(1).hints(true).build();
let leader = VarTree::<[u8; 8]>::open(leader_dir.path(), config.clone()).unwrap();
leader.put(&0u64.to_be_bytes(), b"seed").unwrap();
let leader_signal = ShutdownSignal::new();
let server = leader
.start_replication_server_with_options(
addr,
leader_signal.clone(),
ReplicationServerOptions {
heartbeat_interval_secs: 1,
..Default::default()
},
)
.unwrap();
let follower = Arc::new(VarTree::<[u8; 8]>::open(follower_dir.path(), config).unwrap());
let registry = Arc::new(ReplicationRegistry::new(follower.as_replication_target()));
let follower_signal = ShutdownSignal::new();
let _client = follower
.start_replication_client(addr, registry, follower_signal.clone())
.unwrap();
assert!(wait_until(Duration::from_secs(5), || follower.len() == 1));
assert!(wait_until(Duration::from_secs(5), || {
server.min_replicated_gsn[0].load(Ordering::Relaxed) >= 1
}));
std::thread::sleep(Duration::from_millis(100));
for key in 1u64..=3 {
leader.put(&key.to_be_bytes(), b"slow").unwrap();
}
assert!(wait_until(Duration::from_secs(5), || follower.len() == 4));
assert!(wait_until(Duration::from_secs(5), || {
server.min_replicated_gsn[0].load(Ordering::Relaxed) >= 4
}));
follower_signal.shutdown();
leader_signal.shutdown();
}
#[test]
fn variable_protocol_mismatch_returns_typed_error() {
use armdb::replication::protocol::{
MessageType, ReplicationErrorCode, SyncRequest, VAR_PROTOCOL_VERSION, decode_error,
read_frame, write_frame,
};
use std::io::BufReader;
use std::net::TcpStream;
let leader_dir = tempfile::tempdir().unwrap();
let leader = VarTree::<[u8; 8]>::open(
leader_dir.path(),
Config::balanced().shard_count(1).hints(true).build(),
)
.unwrap();
let addr = next_bind_addr();
let signal = ShutdownSignal::new();
let _server = leader
.start_replication_server(addr, signal.clone())
.unwrap();
let stream = TcpStream::connect(addr).unwrap();
stream
.set_read_timeout(Some(Duration::from_secs(5)))
.unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut writer = stream;
let req = SyncRequest {
protocol_version: VAR_PROTOCOL_VERSION + 1,
shard_id: 0,
from_gsn: 1,
key_len: 8,
};
write_frame(&mut writer, &req.encode()).unwrap();
let frame = read_frame(&mut reader).unwrap();
assert_eq!(frame.msg_type, MessageType::Error);
assert_eq!(
decode_error(&frame.payload).unwrap().code,
ReplicationErrorCode::ProtocolMismatch
);
signal.shutdown();
}
#[test]
fn shard_id_validation_terminates_client() {
use armdb::replication::protocol::{
EntryBatch, ShardInfo, SyncRequest, VAR_PROTOCOL_VERSION, WireEntry, read_frame,
write_frame,
};
use std::io::BufReader;
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let server_addr: SocketAddr = listener.local_addr().unwrap();
let rogue_handle = std::thread::spawn(move || {
let (stream, _) = listener.accept().expect("accept");
let _ = stream.set_nodelay(true);
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut writer = stream;
let frame = read_frame(&mut reader).expect("read SyncRequest");
let req = SyncRequest::decode(&frame.payload).expect("decode SyncRequest");
let info = ShardInfo {
protocol_version: VAR_PROTOCOL_VERSION,
shard_count: 2,
max_file_size: 256 * 1024 * 1024,
};
write_frame(&mut writer, &info.encode()).expect("write ShardInfo");
let wrong_shard_id: u8 = if req.shard_id == 0 { 1 } else { 0 };
let bad_batch = EntryBatch {
shard_id: 99, entries: vec![WireEntry {
entry_len: 0,
key_len: 8,
gsn: 1,
data: vec![],
}],
};
let _ = write_frame(&mut writer, &bad_batch.encode());
let _ = wrong_shard_id;
});
let follower_dir = tempfile::tempdir().unwrap();
let follower = Arc::new(VarTree::<[u8; 8]>::open(follower_dir.path(), test_cfg()).unwrap());
let registry = Arc::new(ReplicationRegistry::new(follower.as_replication_target()));
let signal = ShutdownSignal::new();
let _client = follower
.start_replication_client(server_addr, registry, signal.clone())
.unwrap();
rogue_handle.join().expect("rogue server thread panicked");
std::thread::sleep(Duration::from_millis(500));
assert_eq!(
follower.len(),
0,
"follower must not apply entries from a mismatched shard_id"
);
signal.shutdown();
}
#[test]
fn restart_resumes_from_durable_recovery_without_cursor() {
let one_shard = || Config::balanced().shard_count(1).hints(true).build();
let leader_dir = tempfile::tempdir().unwrap();
let follower_dir = tempfile::tempdir().unwrap();
let addr = next_bind_addr();
let leader = VarTree::<[u8; 8]>::open(leader_dir.path(), one_shard()).unwrap();
let leader_signal = ShutdownSignal::new();
let _server = leader
.start_replication_server_with_options(
addr,
leader_signal.clone(),
ReplicationServerOptions {
heartbeat_interval_secs: 1,
..Default::default()
},
)
.unwrap();
for i in 0u64..100 {
leader.put(&i.to_be_bytes(), b"a").unwrap();
}
{
let follower =
Arc::new(VarTree::<[u8; 8]>::open(follower_dir.path(), one_shard()).unwrap());
let registry = Arc::new(ReplicationRegistry::new(follower.as_replication_target()));
let sig = ShutdownSignal::new();
let client = follower
.start_replication_client(addr, registry, sig.clone())
.unwrap();
assert!(
wait_until(Duration::from_secs(15), || follower.len() >= 100),
"phase A catch-up failed (len={})",
follower.len()
);
sig.shutdown();
drop(client);
follower.clean_shutdown().unwrap();
}
for i in 100u64..200 {
leader.put(&i.to_be_bytes(), b"b").unwrap();
}
let follower = Arc::new(VarTree::<[u8; 8]>::open(follower_dir.path(), one_shard()).unwrap());
let registry = Arc::new(ReplicationRegistry::new(follower.as_replication_target()));
let sig = ShutdownSignal::new();
let _client = follower
.start_replication_client(addr, registry, sig.clone())
.unwrap();
assert!(
wait_until(Duration::from_secs(15), || {
(0u64..200).all(|i| follower.contains(&i.to_be_bytes()))
}),
"follower did not resume from durable recovery (len={})",
follower.len()
);
assert_eq!(follower.len(), 200);
assert!(!follower_dir.path().join("shard_000/repl.cursor").exists());
sig.shutdown();
leader_signal.shutdown();
}
#[test]
fn silent_leader_read_timeout_triggers_reconnect() {
use armdb::replication::protocol::{ShardInfo, VAR_PROTOCOL_VERSION, read_frame, write_frame};
use std::io::BufReader;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let server_addr: SocketAddr = listener.local_addr().unwrap();
listener.set_nonblocking(true).unwrap();
let accepted = Arc::new(AtomicUsize::new(0));
let accepted_mock = accepted.clone();
let stop_mock = Arc::new(AtomicBool::new(false));
let stop_mock2 = stop_mock.clone();
let mock = std::thread::spawn(move || {
let mut held = Vec::new();
while !stop_mock2.load(Ordering::Relaxed) {
match listener.accept() {
Ok((stream, _)) => {
stream.set_nonblocking(false).ok();
let _ = stream.set_nodelay(true);
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut writer = stream.try_clone().unwrap();
if read_frame(&mut reader).is_ok() {
let info = ShardInfo {
protocol_version: VAR_PROTOCOL_VERSION,
shard_count: 1,
max_file_size: 256 * 1024 * 1024,
};
let _ = write_frame(&mut writer, &info.encode());
accepted_mock.fetch_add(1, Ordering::Relaxed);
}
held.push(stream);
}
Err(_) => std::thread::sleep(Duration::from_millis(20)),
}
}
});
let follower_dir = tempfile::tempdir().unwrap();
let follower = Arc::new(
VarTree::<[u8; 8]>::open(
follower_dir.path(),
Config::balanced().shard_count(1).hints(true).build(),
)
.unwrap(),
);
let registry = Arc::new(ReplicationRegistry::new(follower.as_replication_target()));
let signal = ShutdownSignal::new();
let _client = follower
.start_replication_client_with_options(
server_addr,
registry,
signal.clone(),
ReplicationClientOptions {
reconnect_base_ms: 200,
reconnect_max_ms: 200,
heartbeat_interval_secs: 1,
},
)
.unwrap();
assert!(
wait_until(Duration::from_secs(15), || {
accepted.load(Ordering::Relaxed) >= 2
}),
"follower did not reconnect after read timeout (accepted={})",
accepted.load(Ordering::Relaxed)
);
signal.shutdown();
stop_mock.store(true, Ordering::Relaxed);
let _ = mock.join();
}
#[test]
fn dead_peer_releases_streaming_consumer() {
use armdb::replication::protocol::{
AckMessage, MessageType, SyncRequest, VAR_PROTOCOL_VERSION, read_frame, write_frame,
};
use std::io::BufReader;
use std::net::TcpStream;
use std::sync::atomic::{AtomicBool, Ordering};
let leader_dir = tempfile::tempdir().unwrap();
let leader = VarTree::<[u8; 8]>::open(
leader_dir.path(),
Config::balanced().shard_count(1).hints(true).build(),
)
.unwrap();
for i in 0u64..5 {
leader.put(&i.to_be_bytes(), b"x").unwrap();
}
let addr = next_bind_addr();
let leader_signal = ShutdownSignal::new();
let _server = leader
.start_replication_server_with_options(
addr,
leader_signal.clone(),
ReplicationServerOptions {
heartbeat_interval_secs: 1,
..Default::default()
},
)
.unwrap();
let stop_dead = Arc::new(AtomicBool::new(false));
let stop_dead2 = stop_dead.clone();
let dead = std::thread::spawn(move || {
let stream = TcpStream::connect(addr).unwrap();
stream.set_nodelay(true).ok();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut writer = stream.try_clone().unwrap();
let req = SyncRequest {
protocol_version: VAR_PROTOCOL_VERSION,
shard_id: 0,
from_gsn: 1,
key_len: 8,
};
write_frame(&mut writer, &req.encode()).unwrap();
assert_eq!(
read_frame(&mut reader).unwrap().msg_type,
MessageType::ShardInfo
);
loop {
if read_frame(&mut reader).unwrap().msg_type == MessageType::CaughtUp {
break;
}
}
while !stop_dead2.load(Ordering::Relaxed) {
std::thread::sleep(Duration::from_millis(50));
}
drop(stream);
});
std::thread::sleep(Duration::from_secs(5));
let stream = TcpStream::connect(addr).unwrap();
stream.set_nodelay(true).ok();
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut writer = stream.try_clone().unwrap();
let req = SyncRequest {
protocol_version: VAR_PROTOCOL_VERSION,
shard_id: 0,
from_gsn: 1,
key_len: 8,
};
write_frame(&mut writer, &req.encode()).unwrap();
assert_eq!(
read_frame(&mut reader).unwrap().msg_type,
MessageType::ShardInfo
);
let mut frame = read_frame(&mut reader).unwrap();
assert_ne!(
frame.msg_type,
MessageType::Error,
"reconnect rejected — streaming consumer was not released after the peer died (F5)"
);
loop {
if frame.msg_type == MessageType::Heartbeat {
write_frame(
&mut writer,
&AckMessage {
shard_id: 0,
last_gsn: 5,
}
.encode(),
)
.unwrap();
break;
}
frame = read_frame(&mut reader).unwrap();
assert_ne!(frame.msg_type, MessageType::Error);
}
stop_dead.store(true, Ordering::Relaxed);
let _ = dead.join();
leader_signal.shutdown();
}
#[test]
fn replicates_var_value_larger_than_read_ahead_buffer() {
let catchup_value: Vec<u8> = (0..200 * 1024).map(|j| (j % 251) as u8).collect();
let stream_value: Vec<u8> = (0..130 * 1024).map(|j| ((j * 7 + 3) % 251) as u8).collect();
let catchup_key = 1u64.to_be_bytes();
let stream_key = 2u64.to_be_bytes();
let leader_dir = tempfile::tempdir().unwrap();
let follower_dir = tempfile::tempdir().unwrap();
let addr = next_bind_addr();
let leader = VarTree::<[u8; 8]>::open(leader_dir.path(), test_cfg()).unwrap();
leader.put(&catchup_key, &catchup_value).unwrap();
let leader_signal = ShutdownSignal::new();
let _server = leader
.start_replication_server(addr, leader_signal.clone())
.unwrap();
let follower = Arc::new(VarTree::<[u8; 8]>::open(follower_dir.path(), test_cfg()).unwrap());
let registry = Arc::new(ReplicationRegistry::new(follower.as_replication_target()));
let follower_signal = ShutdownSignal::new();
let _client = follower
.start_replication_client(addr, registry, follower_signal.clone())
.unwrap();
assert!(
wait_until(Duration::from_secs(15), || {
follower.contains(&catchup_key)
}),
"follower did not catch up the large value within 15s"
);
let got = follower.get(&catchup_key).expect("catch-up value present");
assert_eq!(
got.as_ref(),
catchup_value.as_slice(),
"catch-up large value corrupted: got {} bytes, expected {}",
got.len(),
catchup_value.len()
);
leader.put(&stream_key, &stream_value).unwrap();
assert!(
wait_until(Duration::from_secs(10), || {
follower.contains(&stream_key)
}),
"follower did not stream the large value within 10s"
);
let got = follower.get(&stream_key).expect("streamed value present");
assert_eq!(
got.as_ref(),
stream_value.as_slice(),
"streamed large value corrupted: got {} bytes, expected {}",
got.len(),
stream_value.len()
);
follower_signal.shutdown();
leader_signal.shutdown();
}