use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use melin_journal::JournalWrite;
use melin_transport_core::pipeline::{JournalStage, JournalStageRun};
use melin_app::Application;
use melin_transport_core::pipeline::{InputSlot, OutputSlot};
mod auth;
#[cfg(feature = "dpdk")]
mod dpdk;
mod receiver_transport;
mod tcp_receiver;
mod tcp_sender;
pub use melin_transport_core::replication::ReplicationMetrics;
pub use melin_transport_core::replication::ack_queue::{
PendingAck, PendingAckQueue, try_flush_dual_track, update_dual_replication_cursor,
wait_for_journal_cursor,
};
pub use melin_transport_core::replication::protocol::{
Ack, Handshake, PrimaryMessage, ReplicaMessage,
};
#[cfg(feature = "dpdk")]
pub use dpdk::{DpdkReplicationDriver, run_receiver_dpdk};
pub use tcp_receiver::{ReceiverResult, run_receiver};
pub use tcp_sender::{Sender, run_sender};
pub(super) fn log_tcp_info(fd: std::os::unix::io::RawFd, tag: &str, slot: usize) {
if !tracing::enabled!(tracing::Level::DEBUG) {
return;
}
let mut info: libc::tcp_info = unsafe { std::mem::zeroed() };
let mut len = std::mem::size_of::<libc::tcp_info>() as libc::socklen_t;
let rc = unsafe {
libc::getsockopt(
fd,
libc::IPPROTO_TCP,
libc::TCP_INFO,
&mut info as *mut _ as *mut libc::c_void,
&mut len,
)
};
if rc != 0 {
tracing::debug!(
slot,
tag,
errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
"tcp_info getsockopt failed"
);
return;
}
tracing::debug!(
slot,
tag,
state = info.tcpi_state,
ca_state = info.tcpi_ca_state,
rtt_us = info.tcpi_rtt,
rttvar_us = info.tcpi_rttvar,
snd_cwnd = info.tcpi_snd_cwnd,
snd_ssthresh = info.tcpi_snd_ssthresh,
snd_mss = info.tcpi_snd_mss,
rcv_mss = info.tcpi_rcv_mss,
unacked = info.tcpi_unacked,
retrans = info.tcpi_retrans,
total_retrans = info.tcpi_total_retrans,
lost = info.tcpi_lost,
rcv_space = info.tcpi_rcv_space,
rto_us = info.tcpi_rto,
"tcp_info"
);
}
pub(super) fn sleep_checking_flags(
duration: std::time::Duration,
shutdown: &AtomicBool,
promote: &AtomicBool,
) {
let deadline = std::time::Instant::now() + duration;
while std::time::Instant::now() < deadline {
if shutdown.load(Ordering::Relaxed) || promote.load(Ordering::Acquire) {
return;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
pub(super) fn shutdown_pipeline<A: Application + Send + 'static, W: Send + 'static>(
shutdown_flag: &AtomicBool,
journal_handle: std::thread::JoinHandle<Result<W, melin_journal::JournalError>>,
matching_handle: std::thread::JoinHandle<A>,
drain_handle: std::thread::JoinHandle<()>,
shadow_handle: Option<std::thread::JoinHandle<()>>,
) -> Option<(A, W)> {
shutdown_flag.store(true, Ordering::Release);
let writer = match journal_handle.join() {
Ok(Ok(w)) => w,
Ok(Err(e)) => {
tracing::error!(error = %e, "replica journal stage returned error on shutdown");
return None;
}
Err(_) => return None,
};
let exchange = matching_handle.join().ok()?;
let _ = drain_handle.join();
if let Some(h) = shadow_handle {
let _ = h.join();
}
Some((exchange, writer))
}
pub(super) struct ReplicaPipelineHandles<A: Application, W: Send + 'static> {
pub(super) input_producer: melin_pipeline::ring::Producer<InputSlot<A::Event>>,
pub(super) journal_cursor: Arc<melin_pipeline::padding::Sequence>,
pub(super) last_seq: Arc<AtomicU64>,
pub(super) chain_hash_lock:
Option<Arc<melin_pipeline::seqlock::SeqLock<melin_transport_core::pipeline::FsyncState>>>,
pub(super) pipeline_shutdown: Arc<AtomicBool>,
pub(super) journal_handle: std::thread::JoinHandle<Result<W, melin_journal::JournalError>>,
pub(super) matching_handle: std::thread::JoinHandle<A>,
pub(super) drain_handle: std::thread::JoinHandle<()>,
pub(super) shadow_handle: Option<std::thread::JoinHandle<()>>,
}
pub(super) fn build_replica_pipeline_with_threads<A, W>(
exchange: A,
writer: W,
cores: crate::server::PipelineCores,
snapshot_interval_ms: u64,
snapshot_path: std::path::PathBuf,
group_commit_delay: std::time::Duration,
busy_spin: bool,
rotation: Option<(u64, Arc<AtomicBool>)>,
) -> Result<ReplicaPipelineHandles<A, W>, Box<dyn std::error::Error>>
where
A: Application + Send + 'static,
A::Event: Send + Sync + 'static,
A::Report: Send + 'static,
A::QueryResponse: Send + 'static,
W: JournalWrite<A::Event> + Send + 'static,
JournalStage<A::Event, W>: JournalStageRun<A::Event, Writer = W>,
{
let shadow_exchange = <A as Application>::clone_via_snapshot(&exchange)?;
let enable_shadow = snapshot_interval_ms > 0;
let pipeline = melin_transport_core::pipeline::build_replica_pipeline(
exchange,
writer,
4096, group_commit_delay,
busy_spin,
enable_shadow,
);
let pipeline_shutdown = Arc::new(AtomicBool::new(false));
let ps = Arc::clone(&pipeline_shutdown);
let journal_core = cores.journal;
let mut journal_stage = pipeline.journal_stage;
if let Some((max_bytes, flag)) = rotation {
journal_stage.set_rotation(max_bytes, Some(flag));
}
let journal_handle = std::thread::Builder::new()
.name("journal".into())
.spawn(move || {
melin_app::affinity::pin_thread("journal", journal_core);
journal_stage.run(&ps)
})
.expect("spawn journal thread");
let ps = Arc::clone(&pipeline_shutdown);
let matching_core = cores.matching;
let matching_stage = pipeline.matching_stage;
let matching_handle = std::thread::Builder::new()
.name("matching".into())
.spawn(move || {
melin_app::affinity::pin_thread("matching", matching_core);
matching_stage.run(&ps)
})
.expect("spawn matching thread");
let ps = Arc::clone(&pipeline_shutdown);
let drain_core = cores.response;
let drain_consumer = pipeline.drain_consumer;
let drain_handle = std::thread::Builder::new()
.name("drain".into())
.spawn(move || {
melin_app::affinity::pin_thread("drain", drain_core);
let mut consumer = drain_consumer;
let mut batch = vec![OutputSlot::<A::Report, A::QueryResponse>::default(); 256];
loop {
if ps.load(Ordering::Relaxed) {
return;
}
let count = consumer.consume_batch(&mut batch, 256);
if count == 0 {
if busy_spin {
std::hint::spin_loop();
} else {
std::thread::yield_now();
}
}
}
})
.expect("spawn drain thread");
let shadow_handle = if let Some(shadow_cons) = pipeline.shadow_consumer {
let snap_path = snapshot_path;
let chain_lock = pipeline
.chain_hash_lock
.as_ref()
.expect("chain hash lock with shadow")
.clone();
let ps = Arc::clone(&pipeline_shutdown);
let shadow_core = cores.shadow;
Some(
std::thread::Builder::new()
.name("replica-shadow".into())
.spawn(move || {
melin_app::affinity::pin_thread("replica-shadow", shadow_core);
melin_transport_core::shadow::run(
shadow_cons,
shadow_exchange,
snap_path,
std::time::Duration::from_millis(snapshot_interval_ms),
chain_lock,
&ps,
false,
);
})
.expect("spawn shadow thread"),
)
} else {
None
};
Ok(ReplicaPipelineHandles {
input_producer: pipeline.input_producer,
journal_cursor: pipeline.journal_cursor,
last_seq: pipeline.last_seq,
chain_hash_lock: pipeline.chain_hash_lock,
pipeline_shutdown,
journal_handle,
matching_handle,
drain_handle,
shadow_handle,
})
}
pub(super) fn teardown_replica_pipeline<A: Application + Send + 'static, W: Send + 'static>(
handles: ReplicaPipelineHandles<A, W>,
) -> Option<(A, W)> {
shutdown_pipeline::<A, W>(
&handles.pipeline_shutdown,
handles.journal_handle,
handles.matching_handle,
handles.drain_handle,
handles.shadow_handle,
)
}
#[cfg(test)]
mod tests {
use std::io::Write;
use std::sync::Arc;
use std::sync::atomic::AtomicU32;
use super::auth::{authenticate_replica, authenticate_with_primary};
use super::*;
use melin_trading::trading_event::TradingEvent;
type InputSlot = melin_transport_core::pipeline::InputSlot<TradingEvent>;
use melin_transport_core::replication::protocol::{
MAX_CONTROL_FRAME, MAX_DATA_FRAME, MSG_AUTH_OK, MSG_CHALLENGE_RESPONSE, MSG_SNAPSHOT_BEGIN,
MSG_SNAPSHOT_CHUNK, MSG_SNAPSHOT_END, decode_auth_result, decode_challenge,
decode_challenge_response, decode_primary_message, decode_replica_message, encode_ack,
encode_auth_failed, encode_auth_ok, encode_challenge, encode_challenge_response,
encode_handshake, encode_hash_mismatch, encode_heartbeat, encode_input_batch,
encode_need_snapshot, encode_snapshot_begin, encode_snapshot_chunk, encode_snapshot_end,
encode_stream_start, read_frame, try_decode_input_batch,
};
fn encode_input_batch_with_seq(end_sequence: u64, buf: &mut Vec<u8>) {
let slot = InputSlot {
connection_id: 0,
key_hash: 0,
request_seq: 0,
sequence: end_sequence,
timestamp_ns: 0,
event: melin_journal::JournalEvent::Tick { now_ns: 0 },
publish_ts: Default::default(),
recv_ts: Default::default(),
};
encode_input_batch(&[slot], buf);
}
#[test]
fn handshake_encode_decode_round_trip() {
let handshake = Handshake {
last_sequence: 42,
chain_hash: [0xAB; 32],
};
let mut buf = Vec::new();
encode_handshake(&handshake, &mut buf);
let payload = &buf[4..];
let msg = decode_replica_message(payload).unwrap();
match msg {
ReplicaMessage::Handshake(h) => {
assert_eq!(h.last_sequence, 42);
assert_eq!(h.chain_hash, [0xAB; 32]);
}
_ => panic!("expected Handshake"),
}
}
#[test]
fn ack_encode_decode_round_trip() {
let ack = Ack {
acked_sequence: 1000,
in_memory_sequence: 1024,
};
let mut buf = Vec::new();
encode_ack(&ack, &mut buf);
let payload = &buf[4..];
let msg = decode_replica_message(payload).unwrap();
match msg {
ReplicaMessage::Ack(a) => {
assert_eq!(a.acked_sequence, 1000);
assert_eq!(a.in_memory_sequence, 1024);
}
_ => panic!("expected Ack"),
}
}
#[test]
fn ack_wire_byte_pattern() {
let ack = Ack {
acked_sequence: 0xDEAD_BEEF_CAFE_F00D,
in_memory_sequence: 0x1122_3344_5566_7788,
};
let mut buf = Vec::new();
encode_ack(&ack, &mut buf);
let expected: &[u8] = &[
0x11, 0x00, 0x00, 0x00, 0x02, 0x0D, 0xF0, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, ];
assert_eq!(buf.as_slice(), expected, "Ack wire layout drifted");
}
#[test]
fn stream_start_encode_decode_round_trip() {
let mut buf = Vec::new();
encode_stream_start(99, &[0xAA; 64], &mut buf);
let payload = &buf[4..];
let msg = decode_primary_message(payload).unwrap();
match msg {
PrimaryMessage::StreamStart {
start_sequence,
genesis_entry,
} => {
assert_eq!(start_sequence, 99);
assert_eq!(genesis_entry, vec![0xAA; 64]);
}
_ => panic!("expected StreamStart"),
}
}
#[test]
fn heartbeat_encode_decode_round_trip() {
let mut buf = Vec::new();
encode_heartbeat(123, &mut buf);
let payload = &buf[4..];
let msg = decode_primary_message(payload).unwrap();
match msg {
PrimaryMessage::Heartbeat { sequence } => {
assert_eq!(sequence, 123);
}
_ => panic!("expected Heartbeat"),
}
}
#[test]
fn need_snapshot_encode_decode_round_trip() {
let mut buf = Vec::new();
encode_need_snapshot(&mut buf);
let payload = &buf[4..];
let msg = decode_primary_message(payload).unwrap();
assert!(matches!(msg, PrimaryMessage::NeedSnapshot));
}
#[test]
fn hash_mismatch_encode_decode_round_trip() {
let mut buf = Vec::new();
encode_hash_mismatch(&mut buf);
let payload = &buf[4..];
let msg = decode_primary_message(payload).unwrap();
assert!(matches!(msg, PrimaryMessage::HashMismatch));
}
#[test]
fn unknown_replica_message_type_is_error() {
let payload = [0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
let result = decode_replica_message(&payload);
assert!(result.is_err());
}
#[test]
fn unknown_primary_message_type_is_error() {
let payload = [0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
let result = decode_primary_message(&payload);
assert!(result.is_err());
}
#[test]
fn read_frame_enforces_max_size() {
let mut buf = Vec::new();
buf.extend_from_slice(&1000u32.to_le_bytes());
buf.extend_from_slice(&[0u8; 100]);
let mut cursor = std::io::Cursor::new(buf);
let result = read_frame(&mut cursor, 64);
assert!(result.is_err());
}
#[test]
fn challenge_encode_decode_round_trip() {
let nonce = [0x42; 32];
let mut buf = Vec::new();
encode_challenge(&nonce, &mut buf);
let payload = &buf[4..];
let decoded = decode_challenge(payload).unwrap();
assert_eq!(decoded, nonce);
}
#[test]
fn challenge_response_encode_decode_round_trip() {
let sig = [0xAA; 64];
let pubkey = [0xBB; 32];
let mut buf = Vec::new();
encode_challenge_response(&sig, &pubkey, &mut buf);
let payload = &buf[4..];
let (decoded_sig, decoded_pubkey) = decode_challenge_response(payload).unwrap();
assert_eq!(decoded_sig, sig);
assert_eq!(decoded_pubkey, pubkey);
}
#[test]
fn auth_ok_encode_decode_round_trip() {
let mut buf = Vec::new();
encode_auth_ok(&mut buf);
let payload = &buf[4..];
assert!(decode_auth_result(payload).unwrap());
}
#[test]
fn auth_failed_encode_decode_round_trip() {
let mut buf = Vec::new();
encode_auth_failed(&mut buf);
let payload = &buf[4..];
assert!(!decode_auth_result(payload).unwrap());
}
#[test]
fn decode_challenge_rejects_wrong_tag() {
let mut payload = [0u8; 33];
payload[0] = MSG_AUTH_OK;
assert!(decode_challenge(&payload).is_err());
}
#[test]
fn decode_challenge_response_rejects_short_payload() {
let payload = [MSG_CHALLENGE_RESPONSE; 10]; assert!(decode_challenge_response(&payload).is_err());
}
#[test]
fn auth_round_trip_valid_key() {
use ed25519_dalek::SigningKey;
use std::os::unix::net::UnixStream;
let repl_key = SigningKey::from_bytes(&[0xFC; 32]);
let pub_b64 = base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
repl_key.verifying_key().to_bytes(),
);
let keys_content = format!("replication {pub_b64} test-replica\n");
let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
primary_stream
.set_read_timeout(Some(std::time::Duration::from_secs(5)))
.unwrap();
replica_stream
.set_read_timeout(Some(std::time::Duration::from_secs(5)))
.unwrap();
let repl_key_clone = SigningKey::from_bytes(&[0xFC; 32]);
let replica_handle = std::thread::spawn(move || {
let mut reader = replica_stream.try_clone().unwrap();
let mut writer = replica_stream;
authenticate_with_primary(&mut reader, &mut writer, &repl_key_clone)
});
let mut reader = primary_stream.try_clone().unwrap();
let mut writer = primary_stream;
authenticate_replica(&mut reader, &mut writer, &authorized_keys).unwrap();
replica_handle.join().unwrap().unwrap();
}
#[test]
fn auth_rejects_unknown_key() {
use ed25519_dalek::SigningKey;
use std::os::unix::net::UnixStream;
let authorized_key = SigningKey::from_bytes(&[0xAA; 32]);
let rogue_key = SigningKey::from_bytes(&[0xBB; 32]);
let pub_b64 = base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
authorized_key.verifying_key().to_bytes(),
);
let keys_content = format!("replication {pub_b64} authorized-replica\n");
let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
primary_stream
.set_read_timeout(Some(std::time::Duration::from_secs(5)))
.unwrap();
replica_stream
.set_read_timeout(Some(std::time::Duration::from_secs(5)))
.unwrap();
let replica_handle = std::thread::spawn(move || {
let mut reader = replica_stream.try_clone().unwrap();
let mut writer = replica_stream;
authenticate_with_primary(&mut reader, &mut writer, &rogue_key)
});
let mut reader = primary_stream.try_clone().unwrap();
let mut writer = primary_stream;
let result = authenticate_replica(&mut reader, &mut writer, &authorized_keys);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("unknown"));
let replica_result = replica_handle.join().unwrap();
assert!(replica_result.is_err());
}
#[test]
fn auth_rejects_wrong_permission() {
use ed25519_dalek::SigningKey;
use std::os::unix::net::UnixStream;
let key = SigningKey::from_bytes(&[0xCC; 32]);
let pub_b64 = base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
key.verifying_key().to_bytes(),
);
let keys_content = format!("trader {pub_b64} wrong-role\n");
let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
primary_stream
.set_read_timeout(Some(std::time::Duration::from_secs(5)))
.unwrap();
replica_stream
.set_read_timeout(Some(std::time::Duration::from_secs(5)))
.unwrap();
let replica_handle = std::thread::spawn(move || {
let mut reader = replica_stream.try_clone().unwrap();
let mut writer = replica_stream;
authenticate_with_primary(&mut reader, &mut writer, &key)
});
let mut reader = primary_stream.try_clone().unwrap();
let mut writer = primary_stream;
let result = authenticate_replica(&mut reader, &mut writer, &authorized_keys);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Replication"));
let replica_result = replica_handle.join().unwrap();
assert!(replica_result.is_err());
}
#[test]
fn auth_rejects_invalid_signature() {
use ed25519_dalek::SigningKey;
use std::os::unix::net::UnixStream;
let correct_key = SigningKey::from_bytes(&[0xDD; 32]);
let pub_b64 = base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
correct_key.verifying_key().to_bytes(),
);
let keys_content = format!("replication {pub_b64} test-replica\n");
let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
primary_stream
.set_read_timeout(Some(std::time::Duration::from_secs(5)))
.unwrap();
replica_stream
.set_read_timeout(Some(std::time::Duration::from_secs(5)))
.unwrap();
let replica_handle = std::thread::spawn(move || {
use melin_transport_core::replication::protocol::*;
let mut reader = replica_stream.try_clone().unwrap();
let mut writer = replica_stream;
let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
let nonce = decode_challenge(&frame).unwrap();
let wrong_key = SigningKey::from_bytes(&[0xEE; 32]);
let bad_signature = ed25519_dalek::Signer::sign(&wrong_key, &nonce);
let correct_pubkey = correct_key.verifying_key();
let mut buf = Vec::with_capacity(128);
encode_challenge_response(
&bad_signature.to_bytes(),
correct_pubkey.as_bytes(),
&mut buf,
);
writer.write_all(&buf).unwrap();
writer.flush().unwrap();
let result_frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
let ok = decode_auth_result(&result_frame).unwrap();
assert!(!ok, "should receive auth failure");
});
let mut reader = primary_stream.try_clone().unwrap();
let mut writer = primary_stream;
let result = authenticate_replica(&mut reader, &mut writer, &authorized_keys);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("signature verification failed"),
"should fail on signature verification"
);
replica_handle.join().unwrap();
}
#[test]
fn sender_receiver_end_to_end() {
use std::os::unix::net::UnixStream;
let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
let replication_cursor = Arc::new(AtomicU64::new(0));
let _replica_cursor = Arc::clone(&replication_cursor);
let replica_handle = std::thread::spawn(move || {
let mut reader = replica_stream.try_clone().unwrap();
let mut writer = replica_stream;
let mut buf = Vec::new();
let handshake = Handshake {
last_sequence: 0,
chain_hash: [0u8; 32],
};
encode_handshake(&handshake, &mut buf);
writer.write_all(&buf).unwrap();
writer.flush().unwrap();
buf.clear();
let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
let msg = decode_primary_message(&frame).unwrap();
assert!(matches!(msg, PrimaryMessage::StreamStart { .. }));
let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
let slots: Vec<InputSlot> = try_decode_input_batch(&frame).expect("decode InputBatch");
let end_seq = slots
.last()
.map(|s| s.sequence)
.expect("InputBatch carried at least one slot");
let ack = Ack {
acked_sequence: end_seq,
in_memory_sequence: end_seq,
};
encode_ack(&ack, &mut buf);
writer.write_all(&buf).unwrap();
writer.flush().unwrap();
end_seq
});
let mut p_reader = primary_stream.try_clone().unwrap();
let mut p_writer = primary_stream;
let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
let handshake = match decode_replica_message(&frame).unwrap() {
ReplicaMessage::Handshake(h) => h,
_ => panic!("expected Handshake"),
};
assert_eq!(handshake.last_sequence, 0);
let mut buf = Vec::new();
encode_stream_start(0, &[0u8; 32], &mut buf); p_writer.write_all(&buf).unwrap();
p_writer.flush().unwrap();
buf.clear();
encode_input_batch_with_seq(42, &mut buf);
p_writer.write_all(&buf).unwrap();
p_writer.flush().unwrap();
buf.clear();
let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
let ack = match decode_replica_message(&frame).unwrap() {
ReplicaMessage::Ack(a) => a,
_ => panic!("expected Ack"),
};
assert_eq!(ack.acked_sequence, 42);
let end_seq = replica_handle.join().unwrap();
assert_eq!(end_seq, 42);
}
#[test]
fn disconnect_degrades_cursor_to_max() {
let cursor = Arc::new(AtomicU64::new(0));
let handshake_seq = 42u64;
cursor.store(handshake_seq + 1, Ordering::Release);
assert_eq!(cursor.load(Ordering::Acquire), 43);
cursor.fetch_max(100 + 1, Ordering::Release);
assert_eq!(cursor.load(Ordering::Acquire), 101);
cursor.store(u64::MAX, Ordering::Release);
assert_eq!(cursor.load(Ordering::Acquire), u64::MAX);
cursor.store(1, Ordering::Release);
assert_eq!(cursor.load(Ordering::Acquire), 1);
}
#[test]
fn ack_advances_cursor_monotonically() {
let cursor = Arc::new(AtomicU64::new(0));
let new_val = 100 + 1;
cursor.fetch_max(new_val, Ordering::Release);
assert_eq!(cursor.load(Ordering::Acquire), 101);
let stale_val = 50 + 1;
cursor.fetch_max(stale_val, Ordering::Release);
assert_eq!(cursor.load(Ordering::Acquire), 101);
let newer_val = 200 + 1;
cursor.fetch_max(newer_val, Ordering::Release);
assert_eq!(cursor.load(Ordering::Acquire), 201);
}
#[test]
fn multiple_data_batches_acked_in_order() {
use std::os::unix::net::UnixStream;
let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
let replica_handle = std::thread::spawn(move || {
let mut reader = replica_stream.try_clone().unwrap();
let mut writer = replica_stream;
let mut buf = Vec::new();
encode_handshake(
&Handshake {
last_sequence: 0,
chain_hash: [0u8; 32],
},
&mut buf,
);
writer.write_all(&buf).unwrap();
writer.flush().unwrap();
buf.clear();
let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
assert!(matches!(
decode_primary_message(&frame).unwrap(),
PrimaryMessage::StreamStart { .. }
));
let mut acked_seqs = Vec::new();
for _ in 0..3 {
let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
let slots: Vec<InputSlot> =
try_decode_input_batch(&frame).expect("decode InputBatch");
let end_seq = slots
.last()
.map(|s| s.sequence)
.expect("InputBatch carried at least one slot");
acked_seqs.push(end_seq);
encode_ack(
&Ack {
acked_sequence: end_seq,
in_memory_sequence: end_seq,
},
&mut buf,
);
writer.write_all(&buf).unwrap();
writer.flush().unwrap();
buf.clear();
}
acked_seqs
});
let mut p_reader = primary_stream.try_clone().unwrap();
let mut p_writer = primary_stream;
let mut buf = Vec::new();
let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
assert!(matches!(
decode_replica_message(&frame).unwrap(),
ReplicaMessage::Handshake(_)
));
encode_stream_start(0, &[0u8; 32], &mut buf);
p_writer.write_all(&buf).unwrap();
p_writer.flush().unwrap();
buf.clear();
for seq in [10u64, 20, 30] {
encode_input_batch_with_seq(seq, &mut buf);
p_writer.write_all(&buf).unwrap();
p_writer.flush().unwrap();
buf.clear();
}
for expected_seq in [10u64, 20, 30] {
let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
let ack = match decode_replica_message(&frame).unwrap() {
ReplicaMessage::Ack(a) => a,
other => panic!("expected Ack, got {other:?}"),
};
assert_eq!(ack.acked_sequence, expected_seq);
}
let acked = replica_handle.join().unwrap();
assert_eq!(acked, vec![10, 20, 30]);
}
#[test]
fn heartbeat_encode_contains_sequence() {
let mut buf = Vec::new();
encode_heartbeat(999, &mut buf);
let payload = &buf[4..];
match decode_primary_message(payload).unwrap() {
PrimaryMessage::Heartbeat { sequence } => {
assert_eq!(sequence, 999);
}
other => panic!("expected Heartbeat, got {other:?}"),
}
}
#[test]
fn replica_mid_stream_handshake_with_nonzero_sequence() {
use std::os::unix::net::UnixStream;
let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
let replica_handle = std::thread::spawn(move || {
let mut reader = replica_stream.try_clone().unwrap();
let mut writer = replica_stream;
let mut buf = Vec::new();
encode_handshake(
&Handshake {
last_sequence: 100,
chain_hash: [0xBB; 32],
},
&mut buf,
);
writer.write_all(&buf).unwrap();
writer.flush().unwrap();
buf.clear();
let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
match decode_primary_message(&frame).unwrap() {
PrimaryMessage::StreamStart { start_sequence, .. } => {
assert_eq!(
start_sequence, 100,
"StreamStart should echo replica's last_sequence"
);
}
other => panic!("expected StreamStart, got {other:?}"),
}
let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
let slots: Vec<InputSlot> = try_decode_input_batch(&frame).expect("decode InputBatch");
let end_sequence = slots
.last()
.map(|s| s.sequence)
.expect("InputBatch carried at least one slot");
assert!(
end_sequence > 100,
"InputBatch should be after replica's last_sequence"
);
});
let mut p_reader = primary_stream.try_clone().unwrap();
let mut p_writer = primary_stream;
let mut buf = Vec::new();
let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
let handshake = match decode_replica_message(&frame).unwrap() {
ReplicaMessage::Handshake(h) => h,
_ => panic!("expected Handshake"),
};
assert_eq!(handshake.last_sequence, 100);
assert_eq!(handshake.chain_hash, [0xBB; 32]);
encode_stream_start(handshake.last_sequence, &[0u8; 32], &mut buf);
p_writer.write_all(&buf).unwrap();
p_writer.flush().unwrap();
buf.clear();
encode_input_batch_with_seq(150, &mut buf);
p_writer.write_all(&buf).unwrap();
p_writer.flush().unwrap();
replica_handle.join().unwrap();
}
#[test]
fn snapshot_begin_encode_decode_round_trip() {
let mut buf = Vec::new();
encode_snapshot_begin(1_000_000, 42, &[0xAB; 32], &mut buf);
let payload = &buf[4..];
let msg = decode_primary_message(payload).unwrap();
match msg {
PrimaryMessage::SnapshotBegin {
snapshot_len,
snap_sequence,
snap_chain_hash,
} => {
assert_eq!(snapshot_len, 1_000_000);
assert_eq!(snap_sequence, 42);
assert_eq!(snap_chain_hash, [0xAB; 32]);
}
_ => panic!("expected SnapshotBegin"),
}
}
#[test]
fn snapshot_chunk_encode_decode_round_trip() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
let mut buf = Vec::new();
encode_snapshot_chunk(&data, &mut buf);
let payload = &buf[4..];
let msg = decode_primary_message(payload).unwrap();
match msg {
PrimaryMessage::SnapshotChunk(chunk) => {
assert_eq!(chunk, data);
}
_ => panic!("expected SnapshotChunk"),
}
}
#[test]
fn snapshot_end_encode_decode_round_trip() {
let mut buf = Vec::new();
encode_snapshot_end(0xDEADBEEF, &mut buf);
let payload = &buf[4..];
let msg = decode_primary_message(payload).unwrap();
match msg {
PrimaryMessage::SnapshotEnd { crc32c } => {
assert_eq!(crc32c, 0xDEADBEEF);
}
_ => panic!("expected SnapshotEnd"),
}
}
#[test]
fn snapshot_receiver_detects_length_mismatch() {
use std::os::unix::net::UnixStream;
let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
let receiver = std::thread::spawn(move || -> String {
let mut reader = replica_stream.try_clone().unwrap();
let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
assert!(matches!(
decode_primary_message(&frame).unwrap(),
PrimaryMessage::NeedSnapshot,
));
let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
let (snap_len, _snap_sequence, _snap_chain_hash) =
match decode_primary_message(&frame).unwrap() {
PrimaryMessage::SnapshotBegin {
snapshot_len,
snap_sequence,
snap_chain_hash,
} => (snapshot_len, snap_sequence, snap_chain_hash),
other => panic!("expected SnapshotBegin, got {other:?}"),
};
let mut received: u64 = 0;
loop {
let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
match decode_primary_message(&frame).unwrap() {
PrimaryMessage::SnapshotChunk(data) => {
received += data.len() as u64;
}
PrimaryMessage::SnapshotEnd { .. } => {
if received != snap_len {
return format!(
"snapshot length mismatch: expected {snap_len} bytes, got {received}"
);
}
return String::new(); }
other => panic!("unexpected message: {other:?}"),
}
}
});
let mut writer = primary_stream;
let mut buf = Vec::new();
let actual_data = vec![0xAA; 100];
let wrong_len = 999u64;
encode_need_snapshot(&mut buf);
std::io::Write::write_all(&mut writer, &buf).unwrap();
buf.clear();
encode_snapshot_begin(wrong_len, 42, &[0xBB; 32], &mut buf);
std::io::Write::write_all(&mut writer, &buf).unwrap();
buf.clear();
encode_snapshot_chunk(&actual_data, &mut buf);
std::io::Write::write_all(&mut writer, &buf).unwrap();
buf.clear();
let crc = crc32c::crc32c(&actual_data);
encode_snapshot_end(crc, &mut buf);
std::io::Write::write_all(&mut writer, &buf).unwrap();
std::io::Write::flush(&mut writer).unwrap();
let error_msg = receiver.join().unwrap();
assert!(
error_msg.contains("length mismatch"),
"expected length mismatch error, got: {error_msg:?}"
);
}
#[test]
fn snapshot_receiver_detects_crc_mismatch() {
use std::os::unix::net::UnixStream;
let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
let receiver = std::thread::spawn(move || -> String {
let mut reader = replica_stream.try_clone().unwrap();
let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
assert!(matches!(
decode_primary_message(&frame).unwrap(),
PrimaryMessage::NeedSnapshot,
));
let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
let snap_len = match decode_primary_message(&frame).unwrap() {
PrimaryMessage::SnapshotBegin { snapshot_len, .. } => snapshot_len,
other => panic!("expected SnapshotBegin, got {other:?}"),
};
let mut received_data = Vec::new();
let mut received: u64 = 0;
loop {
let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
match decode_primary_message(&frame).unwrap() {
PrimaryMessage::SnapshotChunk(data) => {
received += data.len() as u64;
received_data.extend_from_slice(&data);
}
PrimaryMessage::SnapshotEnd {
crc32c: expected_crc,
} => {
if received != snap_len {
return format!("length mismatch: {snap_len} vs {received}");
}
let actual_crc = crc32c::crc32c(&received_data);
if actual_crc != expected_crc {
return format!(
"CRC mismatch: expected {expected_crc:#x}, got {actual_crc:#x}"
);
}
return String::new();
}
other => panic!("unexpected message: {other:?}"),
}
}
});
let mut writer = primary_stream;
let mut buf = Vec::new();
let data = vec![0xAA; 100];
encode_need_snapshot(&mut buf);
std::io::Write::write_all(&mut writer, &buf).unwrap();
buf.clear();
encode_snapshot_begin(data.len() as u64, 42, &[0xBB; 32], &mut buf);
std::io::Write::write_all(&mut writer, &buf).unwrap();
buf.clear();
encode_snapshot_chunk(&data, &mut buf);
std::io::Write::write_all(&mut writer, &buf).unwrap();
buf.clear();
let wrong_crc = !crc32c::crc32c(&data);
encode_snapshot_end(wrong_crc, &mut buf);
std::io::Write::write_all(&mut writer, &buf).unwrap();
std::io::Write::flush(&mut writer).unwrap();
let error_msg = receiver.join().unwrap();
assert!(
error_msg.contains("CRC mismatch"),
"expected CRC mismatch error, got: {error_msg:?}"
);
}
#[test]
fn snapshot_receiver_detects_chain_hash_mismatch() {
use std::os::unix::net::UnixStream;
let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
let receiver = std::thread::spawn(move || -> String {
let mut reader = replica_stream.try_clone().unwrap();
let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
assert!(matches!(
decode_primary_message(&frame).unwrap(),
PrimaryMessage::NeedSnapshot,
));
let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
let (snap_len, _snap_sequence, snap_chain_hash) =
match decode_primary_message(&frame).unwrap() {
PrimaryMessage::SnapshotBegin {
snapshot_len,
snap_sequence,
snap_chain_hash,
} => (snapshot_len, snap_sequence, snap_chain_hash),
other => panic!("expected SnapshotBegin, got {other:?}"),
};
let mut received_data = Vec::new();
let mut received: u64 = 0;
loop {
let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
match decode_primary_message(&frame).unwrap() {
PrimaryMessage::SnapshotChunk(data) => {
received += data.len() as u64;
received_data.extend_from_slice(&data);
}
PrimaryMessage::SnapshotEnd {
crc32c: expected_crc,
} => {
assert_eq!(received, snap_len, "length should match");
let actual_crc = crc32c::crc32c(&received_data);
assert_eq!(actual_crc, expected_crc, "CRC should match");
break;
}
other => panic!("unexpected message: {other:?}"),
}
}
let loaded_hash = [0xFF; 32]; if loaded_hash != snap_chain_hash {
return format!(
"snapshot chain hash mismatch: primary sent {snap_chain_hash:02x?}, \
loaded snapshot has {loaded_hash:02x?}"
);
}
String::new()
});
let mut writer = primary_stream;
let mut buf = Vec::new();
let data = vec![0xAA; 64];
let advertised_hash = [0xBB; 32];
encode_need_snapshot(&mut buf);
std::io::Write::write_all(&mut writer, &buf).unwrap();
buf.clear();
encode_snapshot_begin(data.len() as u64, 10, &advertised_hash, &mut buf);
std::io::Write::write_all(&mut writer, &buf).unwrap();
buf.clear();
encode_snapshot_chunk(&data, &mut buf);
std::io::Write::write_all(&mut writer, &buf).unwrap();
buf.clear();
let crc = crc32c::crc32c(&data);
encode_snapshot_end(crc, &mut buf);
std::io::Write::write_all(&mut writer, &buf).unwrap();
std::io::Write::flush(&mut writer).unwrap();
let error_msg = receiver.join().unwrap();
assert!(
error_msg.contains("chain hash mismatch"),
"expected chain hash mismatch error, got: {error_msg:?}"
);
}
#[test]
fn primary_rejects_snapshot_with_invalid_magic() {
let tmp = tempfile::tempdir().unwrap();
let snap_path = tmp.path().join("test.snapshot");
let mut bad_snap = vec![0u8; 64];
bad_snap[0..4].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes());
std::fs::write(&snap_path, &bad_snap).unwrap();
let snap_data = std::fs::read(&snap_path).unwrap();
assert!(
snap_data.len() >= 48,
"file should be big enough for header"
);
let magic = u32::from_le_bytes(snap_data[0..4].try_into().unwrap());
assert_ne!(magic, 0x534E_4150);
assert_eq!(magic, 0xDEAD_BEEF);
}
#[test]
fn primary_rejects_snapshot_too_small_for_header() {
let tmp = tempfile::tempdir().unwrap();
let snap_path = tmp.path().join("test.snapshot");
std::fs::write(&snap_path, [0u8; 20]).unwrap();
let snap_data = std::fs::read(&snap_path).unwrap();
assert!(
snap_data.len() < 48,
"file must be too small for header validation"
);
}
#[test]
fn decode_snapshot_begin_too_short() {
let payload = [MSG_SNAPSHOT_BEGIN, 0x01, 0x02, 0x03];
let err = decode_primary_message(&payload).unwrap_err();
assert!(
err.to_string().contains("SnapshotBegin too short"),
"expected 'SnapshotBegin too short', got: {err}"
);
}
#[test]
fn decode_snapshot_end_too_short() {
let payload = [MSG_SNAPSHOT_END];
let err = decode_primary_message(&payload).unwrap_err();
assert!(
err.to_string().contains("SnapshotEnd too short"),
"expected 'SnapshotEnd too short', got: {err}"
);
}
#[test]
fn decode_snapshot_chunk_empty_data() {
let payload = [MSG_SNAPSHOT_CHUNK];
let msg = decode_primary_message(&payload).unwrap();
match msg {
PrimaryMessage::SnapshotChunk(data) => {
assert!(data.is_empty());
}
_ => panic!("expected SnapshotChunk"),
}
}
fn make_journal_cursor(val: u64) -> melin_pipeline::padding::Sequence {
melin_pipeline::padding::Sequence::new(AtomicU64::new(val))
}
#[test]
fn pending_ack_queue_push_and_pop_ready() {
let mut q = PendingAckQueue::new(8);
assert!(q.is_empty());
assert!(!q.is_full());
q.push(10, 100);
q.push(20, 200);
assert!(!q.is_empty());
let cursor = make_journal_cursor(5);
assert!(q.pop_ready(&cursor).is_none());
cursor.get().store(15, Ordering::Relaxed);
assert_eq!(q.pop_ready(&cursor), Some(100));
assert!(!q.is_empty());
cursor.get().store(25, Ordering::Relaxed);
assert_eq!(q.pop_ready(&cursor), Some(200));
assert!(q.is_empty());
}
#[test]
fn pending_ack_queue_pop_ready_returns_highest_sequence() {
let mut q = PendingAckQueue::new(8);
q.push(10, 100);
q.push(20, 200);
q.push(30, 300);
let cursor = make_journal_cursor(30);
assert_eq!(q.pop_ready(&cursor), Some(300));
assert!(q.is_empty());
}
#[test]
fn pending_ack_queue_capacity_and_full() {
let mut q = PendingAckQueue::new(8);
for i in 0..8 {
assert!(!q.is_full());
q.push(i as u64 + 1, (i + 1) as u64 * 100);
}
assert!(q.is_full());
}
#[test]
fn pending_ack_queue_pop_oldest_blocking() {
let mut q = PendingAckQueue::new(8);
q.push(10, 100);
q.push(20, 200);
let cursor = make_journal_cursor(25);
let seq = q.pop_oldest_blocking(&cursor, true);
assert_eq!(seq, 200);
assert!(q.is_empty());
}
#[test]
fn pending_ack_queue_wraps_around() {
let mut q = PendingAckQueue::new(8);
let cursor = make_journal_cursor(100);
for round in 0..3 {
for i in 0..8 {
let target = (round * 8 + i) as u64 + 1;
q.push(target, target * 10);
}
assert!(q.is_full());
let seq = q.pop_ready(&cursor).expect("should be ready");
assert_eq!(seq, (round * 8 + 8) as u64 * 10);
assert!(q.is_empty());
}
}
#[test]
fn pending_ack_queue_pop_all_blocking_empty() {
let mut q = PendingAckQueue::new(8);
let cursor = make_journal_cursor(0);
assert!(q.pop_all_blocking(&cursor, true).is_none());
}
#[test]
fn dual_track_returns_none_when_both_tracks_idle() {
let mut q = PendingAckQueue::new(8);
let cursor = make_journal_cursor(0);
assert!(
try_flush_dual_track(&mut q, &cursor, 0, 0, 0).is_none(),
"no advance on either track → no ack"
);
}
#[test]
fn dual_track_fires_on_persisted_advance_only() {
let mut q = PendingAckQueue::new(8);
q.push(10, 100);
let cursor = make_journal_cursor(20);
let ack = try_flush_dual_track(&mut q, &cursor, 50, 0, 50).expect("persisted advanced");
assert_eq!(ack.acked_sequence, 100);
assert_eq!(ack.in_memory_sequence, 50);
}
#[test]
fn dual_track_fires_on_in_memory_advance_only() {
let mut q = PendingAckQueue::new(8);
let cursor = make_journal_cursor(0);
let ack = try_flush_dual_track(&mut q, &cursor, 200, 100, 100).expect("in-memory advanced");
assert_eq!(ack.acked_sequence, 100);
assert_eq!(ack.in_memory_sequence, 200);
}
#[test]
fn dual_track_coalesces_until_caller_updates_trackers() {
let mut q = PendingAckQueue::new(8);
q.push(10, 100);
let cursor = make_journal_cursor(20);
let ack1 = try_flush_dual_track(&mut q, &cursor, 50, 0, 50).expect("call 1 fires");
assert_eq!(ack1.acked_sequence, 100);
let ack2 = try_flush_dual_track(&mut q, &cursor, 80, 0, 50)
.expect("call 2 fires on in-memory advance");
assert_eq!(
ack2.acked_sequence, 0,
"no new persisted pop → unwrap_or(last_sent)"
);
assert_eq!(ack2.in_memory_sequence, 80);
}
#[test]
fn dual_track_no_duplicate_ack_after_backpressure_drain() {
let mut q = PendingAckQueue::new(4);
for i in 1..=4u64 {
q.push(i * 10, i * 100);
}
assert!(q.is_full());
let cursor = make_journal_cursor(40);
let drained = q.pop_ready(&cursor).expect("all entries durable");
assert_eq!(drained, 400);
assert!(q.is_empty());
let mut last_sent_acked = drained;
let mut last_sent_in_mem = 450u64;
assert!(
try_flush_dual_track(
&mut q,
&cursor,
last_sent_in_mem,
last_sent_acked,
last_sent_in_mem,
)
.is_none(),
"no advance on either track after backpressure drain → no duplicate ack",
);
q.push(50, 500);
let cursor2 = make_journal_cursor(50);
let ack = try_flush_dual_track(&mut q, &cursor2, 500, last_sent_acked, last_sent_in_mem)
.expect("fresh batch after drain fires a new ack");
assert!(
ack.acked_sequence >= last_sent_acked,
"regression: drain sent {last_sent_acked} but next ack carries {}",
ack.acked_sequence,
);
assert_eq!(ack.acked_sequence, 500);
assert_eq!(ack.in_memory_sequence, 500);
last_sent_acked = ack.acked_sequence;
last_sent_in_mem = ack.in_memory_sequence;
assert!(
try_flush_dual_track(
&mut q,
&cursor2,
last_sent_in_mem,
last_sent_acked,
last_sent_in_mem,
)
.is_none(),
"post-resume idle → no further ack",
);
}
#[test]
fn dual_cursor_takes_min_and_max_of_both_slots() {
let cursor_min = Arc::new(AtomicU64::new(0));
let cursor_max = Arc::new(AtomicU64::new(0));
update_dual_replication_cursor(100, 50, &cursor_min, &cursor_max);
assert_eq!(cursor_min.load(Ordering::Relaxed), 50);
assert_eq!(cursor_max.load(Ordering::Relaxed), 100);
}
#[test]
fn dual_cursor_idle_slot_uses_max() {
let cursor_min = Arc::new(AtomicU64::new(0));
let cursor_max = Arc::new(AtomicU64::new(0));
update_dual_replication_cursor(100, u64::MAX, &cursor_min, &cursor_max);
assert_eq!(cursor_min.load(Ordering::Relaxed), 100);
assert_eq!(cursor_max.load(Ordering::Relaxed), u64::MAX);
}
#[test]
fn dual_cursor_both_idle() {
let cursor_min = Arc::new(AtomicU64::new(42));
let cursor_max = Arc::new(AtomicU64::new(42));
update_dual_replication_cursor(u64::MAX, u64::MAX, &cursor_min, &cursor_max);
assert_eq!(cursor_min.load(Ordering::Relaxed), u64::MAX);
assert_eq!(cursor_max.load(Ordering::Relaxed), u64::MAX);
}
#[test]
fn dual_cursor_decreases_when_slower_replica_connects() {
let cursor_min = Arc::new(AtomicU64::new(0));
let cursor_max = Arc::new(AtomicU64::new(0));
update_dual_replication_cursor(100, u64::MAX, &cursor_min, &cursor_max);
assert_eq!(cursor_min.load(Ordering::Relaxed), 100);
update_dual_replication_cursor(51, 100, &cursor_min, &cursor_max);
assert_eq!(cursor_min.load(Ordering::Relaxed), 51);
assert_eq!(cursor_max.load(Ordering::Relaxed), 100);
}
#[test]
fn dual_cursor_advances_as_slower_replica_catches_up() {
let cursor_min = Arc::new(AtomicU64::new(0));
let cursor_max = Arc::new(AtomicU64::new(0));
update_dual_replication_cursor(51, 100, &cursor_min, &cursor_max);
assert_eq!(cursor_min.load(Ordering::Relaxed), 51);
assert_eq!(cursor_max.load(Ordering::Relaxed), 100);
update_dual_replication_cursor(80, 100, &cursor_min, &cursor_max);
assert_eq!(cursor_min.load(Ordering::Relaxed), 80);
update_dual_replication_cursor(100, 100, &cursor_min, &cursor_max);
assert_eq!(cursor_min.load(Ordering::Relaxed), 100);
assert_eq!(cursor_max.load(Ordering::Relaxed), 100);
update_dual_replication_cursor(150, 150, &cursor_min, &cursor_max);
assert_eq!(cursor_min.load(Ordering::Relaxed), 150);
assert_eq!(cursor_max.load(Ordering::Relaxed), 150);
}
#[test]
fn dual_cursor_slot_disconnect_raises_to_surviving() {
let cursor_min = Arc::new(AtomicU64::new(0));
let cursor_max = Arc::new(AtomicU64::new(0));
update_dual_replication_cursor(80, 100, &cursor_min, &cursor_max);
assert_eq!(cursor_min.load(Ordering::Relaxed), 80);
assert_eq!(cursor_max.load(Ordering::Relaxed), 100);
update_dual_replication_cursor(100, u64::MAX, &cursor_min, &cursor_max);
assert_eq!(cursor_min.load(Ordering::Relaxed), 100);
assert_eq!(cursor_max.load(Ordering::Relaxed), u64::MAX);
}
#[test]
fn disconnect_resets_cursor_to_max() {
let cursor = Arc::new(AtomicU64::new(42));
let replicas_connected = Arc::new(AtomicU32::new(1));
replicas_connected.fetch_sub(1, Ordering::Release);
if replicas_connected.load(Ordering::Relaxed) == 0 {
cursor.store(u64::MAX, Ordering::Release);
}
assert_eq!(cursor.load(Ordering::Relaxed), u64::MAX);
}
#[test]
fn cursor_not_reset_when_replica_still_connected() {
let cursor = Arc::new(AtomicU64::new(42));
let replicas_connected = Arc::new(AtomicU32::new(2));
replicas_connected.fetch_sub(1, Ordering::Release);
if replicas_connected.load(Ordering::Relaxed) == 0 {
cursor.store(u64::MAX, Ordering::Release);
}
assert_eq!(cursor.load(Ordering::Relaxed), 42);
}
}