1use std::sync::Arc;
49use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
50
51use melin_journal::JournalWrite;
52use melin_transport_core::pipeline::{JournalStage, JournalStageRun};
53
54use melin_app::Application;
55use melin_transport_core::pipeline::{InputSlot, OutputSlot};
56
57mod auth;
58#[cfg(feature = "dpdk")]
59mod dpdk;
60mod receiver_transport;
61mod tcp_receiver;
62mod tcp_sender;
63
64pub use melin_transport_core::replication::ReplicationMetrics;
71pub use melin_transport_core::replication::ack_queue::{
72 PendingAck, PendingAckQueue, try_flush_dual_track, update_dual_replication_cursor,
73 wait_for_journal_cursor,
74};
75pub use melin_transport_core::replication::protocol::{
76 Ack, Handshake, PrimaryMessage, ReplicaMessage,
77};
78
79#[cfg(feature = "dpdk")]
80pub use dpdk::{DpdkReplicationDriver, run_receiver_dpdk};
81pub use tcp_receiver::{ReceiverResult, run_receiver};
82pub use tcp_sender::{Sender, run_sender};
83
84pub(super) fn log_tcp_info(fd: std::os::unix::io::RawFd, tag: &str, slot: usize) {
93 if !tracing::enabled!(tracing::Level::DEBUG) {
94 return;
95 }
96 let mut info: libc::tcp_info = unsafe { std::mem::zeroed() };
100 let mut len = std::mem::size_of::<libc::tcp_info>() as libc::socklen_t;
101 let rc = unsafe {
102 libc::getsockopt(
103 fd,
104 libc::IPPROTO_TCP,
105 libc::TCP_INFO,
106 &mut info as *mut _ as *mut libc::c_void,
107 &mut len,
108 )
109 };
110 if rc != 0 {
111 tracing::debug!(
112 slot,
113 tag,
114 errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
115 "tcp_info getsockopt failed"
116 );
117 return;
118 }
119 tracing::debug!(
120 slot,
121 tag,
122 state = info.tcpi_state,
123 ca_state = info.tcpi_ca_state,
124 rtt_us = info.tcpi_rtt,
125 rttvar_us = info.tcpi_rttvar,
126 snd_cwnd = info.tcpi_snd_cwnd,
127 snd_ssthresh = info.tcpi_snd_ssthresh,
128 snd_mss = info.tcpi_snd_mss,
129 rcv_mss = info.tcpi_rcv_mss,
130 unacked = info.tcpi_unacked,
131 retrans = info.tcpi_retrans,
132 total_retrans = info.tcpi_total_retrans,
133 lost = info.tcpi_lost,
134 rcv_space = info.tcpi_rcv_space,
135 rto_us = info.tcpi_rto,
136 "tcp_info"
137 );
138}
139
140pub(super) fn sleep_checking_flags(
143 duration: std::time::Duration,
144 shutdown: &AtomicBool,
145 promote: &AtomicBool,
146) {
147 let deadline = std::time::Instant::now() + duration;
148 while std::time::Instant::now() < deadline {
149 if shutdown.load(Ordering::Relaxed) || promote.load(Ordering::Acquire) {
150 return;
151 }
152 std::thread::sleep(std::time::Duration::from_millis(100));
153 }
154}
155
156pub(super) fn shutdown_pipeline<A: Application + Send + 'static, W: Send + 'static>(
171 shutdown_flag: &AtomicBool,
172 journal_handle: std::thread::JoinHandle<Result<W, melin_journal::JournalError>>,
173 matching_handle: std::thread::JoinHandle<A>,
174 drain_handle: std::thread::JoinHandle<()>,
175 shadow_handle: Option<std::thread::JoinHandle<()>>,
176) -> Option<(A, W)> {
177 shutdown_flag.store(true, Ordering::Release);
185 let writer = match journal_handle.join() {
186 Ok(Ok(w)) => w,
187 Ok(Err(e)) => {
188 tracing::error!(error = %e, "replica journal stage returned error on shutdown");
189 return None;
190 }
191 Err(_) => return None,
192 };
193 let exchange = matching_handle.join().ok()?;
194 let _ = drain_handle.join();
195 if let Some(h) = shadow_handle {
196 let _ = h.join();
197 }
198 Some((exchange, writer))
199}
200
201pub(super) struct ReplicaPipelineHandles<A: Application, W: Send + 'static> {
210 pub(super) input_producer: melin_pipeline::ring::Producer<InputSlot<A::Event>>,
211 pub(super) journal_cursor: Arc<melin_pipeline::padding::Sequence>,
212 pub(super) last_seq: Arc<AtomicU64>,
216 pub(super) chain_hash_lock:
220 Option<Arc<melin_pipeline::seqlock::SeqLock<melin_transport_core::pipeline::FsyncState>>>,
221 pub(super) pipeline_shutdown: Arc<AtomicBool>,
224 pub(super) journal_handle: std::thread::JoinHandle<Result<W, melin_journal::JournalError>>,
225 pub(super) matching_handle: std::thread::JoinHandle<A>,
226 pub(super) drain_handle: std::thread::JoinHandle<()>,
227 pub(super) shadow_handle: Option<std::thread::JoinHandle<()>>,
228}
229
230pub(super) fn build_replica_pipeline_with_threads<A, W>(
234 exchange: A,
235 writer: W,
236 cores: crate::server::PipelineCores,
237 snapshot_interval_ms: u64,
238 snapshot_path: std::path::PathBuf,
239 group_commit_delay: std::time::Duration,
240 busy_spin: bool,
241 rotation: Option<(u64, Arc<AtomicBool>)>,
242) -> Result<ReplicaPipelineHandles<A, W>, Box<dyn std::error::Error>>
243where
244 A: Application + Send + 'static,
245 A::Event: Send + Sync + 'static,
246 A::Report: Send + 'static,
247 A::QueryResponse: Send + 'static,
248 W: JournalWrite<A::Event> + Send + 'static,
249 JournalStage<A::Event, W>: JournalStageRun<A::Event, Writer = W>,
250{
251 let shadow_exchange = <A as Application>::clone_via_snapshot(&exchange)?;
252
253 let enable_shadow = snapshot_interval_ms > 0;
254 let pipeline = melin_transport_core::pipeline::build_replica_pipeline(
255 exchange,
256 writer,
257 4096, group_commit_delay,
259 busy_spin,
260 enable_shadow,
261 );
262
263 let pipeline_shutdown = Arc::new(AtomicBool::new(false));
264
265 let ps = Arc::clone(&pipeline_shutdown);
266 let journal_core = cores.journal;
267 let mut journal_stage = pipeline.journal_stage;
268 if let Some((max_bytes, flag)) = rotation {
269 journal_stage.set_rotation(max_bytes, Some(flag));
270 }
271 let journal_handle = std::thread::Builder::new()
272 .name("journal".into())
273 .spawn(move || {
274 melin_app::affinity::pin_thread("journal", journal_core);
275 journal_stage.run(&ps)
276 })
277 .expect("spawn journal thread");
278
279 let ps = Arc::clone(&pipeline_shutdown);
280 let matching_core = cores.matching;
281 let matching_stage = pipeline.matching_stage;
282 let matching_handle = std::thread::Builder::new()
283 .name("matching".into())
284 .spawn(move || {
285 melin_app::affinity::pin_thread("matching", matching_core);
286 matching_stage.run(&ps)
287 })
288 .expect("spawn matching thread");
289
290 let ps = Arc::clone(&pipeline_shutdown);
293 let drain_core = cores.response;
294 let drain_consumer = pipeline.drain_consumer;
295 let drain_handle = std::thread::Builder::new()
296 .name("drain".into())
297 .spawn(move || {
298 melin_app::affinity::pin_thread("drain", drain_core);
299 let mut consumer = drain_consumer;
300 let mut batch = vec![OutputSlot::<A::Report, A::QueryResponse>::default(); 256];
301 loop {
302 if ps.load(Ordering::Relaxed) {
303 return;
304 }
305 let count = consumer.consume_batch(&mut batch, 256);
306 if count == 0 {
307 if busy_spin {
308 std::hint::spin_loop();
309 } else {
310 std::thread::yield_now();
311 }
312 }
313 }
314 })
315 .expect("spawn drain thread");
316
317 let shadow_handle = if let Some(shadow_cons) = pipeline.shadow_consumer {
318 let snap_path = snapshot_path;
319 let chain_lock = pipeline
320 .chain_hash_lock
321 .as_ref()
322 .expect("chain hash lock with shadow")
323 .clone();
324 let ps = Arc::clone(&pipeline_shutdown);
325 let shadow_core = cores.shadow;
326 Some(
327 std::thread::Builder::new()
328 .name("replica-shadow".into())
329 .spawn(move || {
330 melin_app::affinity::pin_thread("replica-shadow", shadow_core);
331 melin_transport_core::shadow::run(
332 shadow_cons,
333 shadow_exchange,
334 snap_path,
335 std::time::Duration::from_millis(snapshot_interval_ms),
336 chain_lock,
337 &ps,
338 false,
339 );
340 })
341 .expect("spawn shadow thread"),
342 )
343 } else {
344 None
345 };
346
347 Ok(ReplicaPipelineHandles {
348 input_producer: pipeline.input_producer,
349 journal_cursor: pipeline.journal_cursor,
350 last_seq: pipeline.last_seq,
351 chain_hash_lock: pipeline.chain_hash_lock,
352 pipeline_shutdown,
353 journal_handle,
354 matching_handle,
355 drain_handle,
356 shadow_handle,
357 })
358}
359
360pub(super) fn teardown_replica_pipeline<A: Application + Send + 'static, W: Send + 'static>(
364 handles: ReplicaPipelineHandles<A, W>,
365) -> Option<(A, W)> {
366 shutdown_pipeline::<A, W>(
367 &handles.pipeline_shutdown,
368 handles.journal_handle,
369 handles.matching_handle,
370 handles.drain_handle,
371 handles.shadow_handle,
372 )
373}
374
375#[cfg(test)]
376mod tests {
377 use std::io::Write;
378 use std::sync::Arc;
379 use std::sync::atomic::AtomicU32;
380
381 use super::auth::{authenticate_replica, authenticate_with_primary};
382 use super::*;
383 use melin_trading::trading_event::TradingEvent;
384 type InputSlot = melin_transport_core::pipeline::InputSlot<TradingEvent>;
385 use melin_transport_core::replication::protocol::{
386 MAX_CONTROL_FRAME, MAX_DATA_FRAME, MSG_AUTH_OK, MSG_CHALLENGE_RESPONSE, MSG_SNAPSHOT_BEGIN,
387 MSG_SNAPSHOT_CHUNK, MSG_SNAPSHOT_END, decode_auth_result, decode_challenge,
388 decode_challenge_response, decode_primary_message, decode_replica_message, encode_ack,
389 encode_auth_failed, encode_auth_ok, encode_challenge, encode_challenge_response,
390 encode_handshake, encode_hash_mismatch, encode_heartbeat, encode_input_batch,
391 encode_need_snapshot, encode_snapshot_begin, encode_snapshot_chunk, encode_snapshot_end,
392 encode_stream_start, read_frame, try_decode_input_batch,
393 };
394
395 fn encode_input_batch_with_seq(end_sequence: u64, buf: &mut Vec<u8>) {
399 let slot = InputSlot {
400 connection_id: 0,
401 key_hash: 0,
402 request_seq: 0,
403 sequence: end_sequence,
404 timestamp_ns: 0,
405 event: melin_journal::JournalEvent::Tick { now_ns: 0 },
406 publish_ts: Default::default(),
407 recv_ts: Default::default(),
408 };
409 encode_input_batch(&[slot], buf);
410 }
411
412 #[test]
413 fn handshake_encode_decode_round_trip() {
414 let handshake = Handshake {
415 last_sequence: 42,
416 chain_hash: [0xAB; 32],
417 };
418 let mut buf = Vec::new();
419 encode_handshake(&handshake, &mut buf);
420
421 let payload = &buf[4..];
423 let msg = decode_replica_message(payload).unwrap();
424 match msg {
425 ReplicaMessage::Handshake(h) => {
426 assert_eq!(h.last_sequence, 42);
427 assert_eq!(h.chain_hash, [0xAB; 32]);
428 }
429 _ => panic!("expected Handshake"),
430 }
431 }
432
433 #[test]
434 fn ack_encode_decode_round_trip() {
435 let ack = Ack {
436 acked_sequence: 1000,
437 in_memory_sequence: 1024,
438 };
439 let mut buf = Vec::new();
440 encode_ack(&ack, &mut buf);
441
442 let payload = &buf[4..];
443 let msg = decode_replica_message(payload).unwrap();
444 match msg {
445 ReplicaMessage::Ack(a) => {
446 assert_eq!(a.acked_sequence, 1000);
447 assert_eq!(a.in_memory_sequence, 1024);
448 }
449 _ => panic!("expected Ack"),
450 }
451 }
452
453 #[test]
461 fn ack_wire_byte_pattern() {
462 let ack = Ack {
463 acked_sequence: 0xDEAD_BEEF_CAFE_F00D,
464 in_memory_sequence: 0x1122_3344_5566_7788,
465 };
466 let mut buf = Vec::new();
467 encode_ack(&ack, &mut buf);
468 let expected: &[u8] = &[
471 0x11, 0x00, 0x00, 0x00, 0x02, 0x0D, 0xF0, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, ];
476 assert_eq!(buf.as_slice(), expected, "Ack wire layout drifted");
477 }
478
479 #[test]
480 fn stream_start_encode_decode_round_trip() {
481 let mut buf = Vec::new();
482 encode_stream_start(99, &[0xAA; 64], &mut buf);
483
484 let payload = &buf[4..];
485 let msg = decode_primary_message(payload).unwrap();
486 match msg {
487 PrimaryMessage::StreamStart {
488 start_sequence,
489 genesis_entry,
490 } => {
491 assert_eq!(start_sequence, 99);
492 assert_eq!(genesis_entry, vec![0xAA; 64]);
493 }
494 _ => panic!("expected StreamStart"),
495 }
496 }
497
498 #[test]
499 fn heartbeat_encode_decode_round_trip() {
500 let mut buf = Vec::new();
501 encode_heartbeat(123, &mut buf);
502
503 let payload = &buf[4..];
504 let msg = decode_primary_message(payload).unwrap();
505 match msg {
506 PrimaryMessage::Heartbeat { sequence } => {
507 assert_eq!(sequence, 123);
508 }
509 _ => panic!("expected Heartbeat"),
510 }
511 }
512
513 #[test]
514 fn need_snapshot_encode_decode_round_trip() {
515 let mut buf = Vec::new();
516 encode_need_snapshot(&mut buf);
517
518 let payload = &buf[4..];
519 let msg = decode_primary_message(payload).unwrap();
520 assert!(matches!(msg, PrimaryMessage::NeedSnapshot));
521 }
522
523 #[test]
524 fn hash_mismatch_encode_decode_round_trip() {
525 let mut buf = Vec::new();
526 encode_hash_mismatch(&mut buf);
527
528 let payload = &buf[4..];
529 let msg = decode_primary_message(payload).unwrap();
530 assert!(matches!(msg, PrimaryMessage::HashMismatch));
531 }
532
533 #[test]
534 fn unknown_replica_message_type_is_error() {
535 let payload = [0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
536 let result = decode_replica_message(&payload);
537 assert!(result.is_err());
538 }
539
540 #[test]
541 fn unknown_primary_message_type_is_error() {
542 let payload = [0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
543 let result = decode_primary_message(&payload);
544 assert!(result.is_err());
545 }
546
547 #[test]
548 fn read_frame_enforces_max_size() {
549 let mut buf = Vec::new();
551 buf.extend_from_slice(&1000u32.to_le_bytes());
552 buf.extend_from_slice(&[0u8; 100]); let mut cursor = std::io::Cursor::new(buf);
555 let result = read_frame(&mut cursor, 64);
556 assert!(result.is_err());
557 }
558
559 #[test]
560 fn challenge_encode_decode_round_trip() {
561 let nonce = [0x42; 32];
562 let mut buf = Vec::new();
563 encode_challenge(&nonce, &mut buf);
564
565 let payload = &buf[4..];
566 let decoded = decode_challenge(payload).unwrap();
567 assert_eq!(decoded, nonce);
568 }
569
570 #[test]
571 fn challenge_response_encode_decode_round_trip() {
572 let sig = [0xAA; 64];
573 let pubkey = [0xBB; 32];
574 let mut buf = Vec::new();
575 encode_challenge_response(&sig, &pubkey, &mut buf);
576
577 let payload = &buf[4..];
578 let (decoded_sig, decoded_pubkey) = decode_challenge_response(payload).unwrap();
579 assert_eq!(decoded_sig, sig);
580 assert_eq!(decoded_pubkey, pubkey);
581 }
582
583 #[test]
584 fn auth_ok_encode_decode_round_trip() {
585 let mut buf = Vec::new();
586 encode_auth_ok(&mut buf);
587
588 let payload = &buf[4..];
589 assert!(decode_auth_result(payload).unwrap());
590 }
591
592 #[test]
593 fn auth_failed_encode_decode_round_trip() {
594 let mut buf = Vec::new();
595 encode_auth_failed(&mut buf);
596
597 let payload = &buf[4..];
598 assert!(!decode_auth_result(payload).unwrap());
599 }
600
601 #[test]
602 fn decode_challenge_rejects_wrong_tag() {
603 let mut payload = [0u8; 33];
604 payload[0] = MSG_AUTH_OK;
605 assert!(decode_challenge(&payload).is_err());
606 }
607
608 #[test]
609 fn decode_challenge_response_rejects_short_payload() {
610 let payload = [MSG_CHALLENGE_RESPONSE; 10]; assert!(decode_challenge_response(&payload).is_err());
612 }
613
614 #[test]
615 fn auth_round_trip_valid_key() {
616 use ed25519_dalek::SigningKey;
617 use std::os::unix::net::UnixStream;
618
619 let repl_key = SigningKey::from_bytes(&[0xFC; 32]);
620 let pub_b64 = base64::Engine::encode(
621 &base64::engine::general_purpose::STANDARD,
622 repl_key.verifying_key().to_bytes(),
623 );
624 let keys_content = format!("replication {pub_b64} test-replica\n");
625 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
626
627 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
628 primary_stream
629 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
630 .unwrap();
631 replica_stream
632 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
633 .unwrap();
634
635 let repl_key_clone = SigningKey::from_bytes(&[0xFC; 32]);
636 let replica_handle = std::thread::spawn(move || {
637 let mut reader = replica_stream.try_clone().unwrap();
638 let mut writer = replica_stream;
639 authenticate_with_primary(&mut reader, &mut writer, &repl_key_clone)
640 });
641
642 let mut reader = primary_stream.try_clone().unwrap();
643 let mut writer = primary_stream;
644 authenticate_replica(&mut reader, &mut writer, &authorized_keys).unwrap();
645
646 replica_handle.join().unwrap().unwrap();
647 }
648
649 #[test]
650 fn auth_rejects_unknown_key() {
651 use ed25519_dalek::SigningKey;
652 use std::os::unix::net::UnixStream;
653
654 let authorized_key = SigningKey::from_bytes(&[0xAA; 32]);
656 let rogue_key = SigningKey::from_bytes(&[0xBB; 32]);
657 let pub_b64 = base64::Engine::encode(
658 &base64::engine::general_purpose::STANDARD,
659 authorized_key.verifying_key().to_bytes(),
660 );
661 let keys_content = format!("replication {pub_b64} authorized-replica\n");
662 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
663
664 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
665 primary_stream
666 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
667 .unwrap();
668 replica_stream
669 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
670 .unwrap();
671
672 let replica_handle = std::thread::spawn(move || {
673 let mut reader = replica_stream.try_clone().unwrap();
674 let mut writer = replica_stream;
675 authenticate_with_primary(&mut reader, &mut writer, &rogue_key)
676 });
677
678 let mut reader = primary_stream.try_clone().unwrap();
679 let mut writer = primary_stream;
680 let result = authenticate_replica(&mut reader, &mut writer, &authorized_keys);
681 assert!(result.is_err());
682 assert!(result.unwrap_err().to_string().contains("unknown"));
683
684 let replica_result = replica_handle.join().unwrap();
686 assert!(replica_result.is_err());
687 }
688
689 #[test]
690 fn auth_rejects_wrong_permission() {
691 use ed25519_dalek::SigningKey;
692 use std::os::unix::net::UnixStream;
693
694 let key = SigningKey::from_bytes(&[0xCC; 32]);
696 let pub_b64 = base64::Engine::encode(
697 &base64::engine::general_purpose::STANDARD,
698 key.verifying_key().to_bytes(),
699 );
700 let keys_content = format!("trader {pub_b64} wrong-role\n");
701 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
702
703 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
704 primary_stream
705 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
706 .unwrap();
707 replica_stream
708 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
709 .unwrap();
710
711 let replica_handle = std::thread::spawn(move || {
712 let mut reader = replica_stream.try_clone().unwrap();
713 let mut writer = replica_stream;
714 authenticate_with_primary(&mut reader, &mut writer, &key)
715 });
716
717 let mut reader = primary_stream.try_clone().unwrap();
718 let mut writer = primary_stream;
719 let result = authenticate_replica(&mut reader, &mut writer, &authorized_keys);
720 assert!(result.is_err());
721 assert!(result.unwrap_err().to_string().contains("Replication"));
722
723 let replica_result = replica_handle.join().unwrap();
724 assert!(replica_result.is_err());
725 }
726
727 #[test]
730 fn auth_rejects_invalid_signature() {
731 use ed25519_dalek::SigningKey;
732 use std::os::unix::net::UnixStream;
733
734 let correct_key = SigningKey::from_bytes(&[0xDD; 32]);
736 let pub_b64 = base64::Engine::encode(
737 &base64::engine::general_purpose::STANDARD,
738 correct_key.verifying_key().to_bytes(),
739 );
740 let keys_content = format!("replication {pub_b64} test-replica\n");
741 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
742
743 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
744 primary_stream
745 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
746 .unwrap();
747 replica_stream
748 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
749 .unwrap();
750
751 let replica_handle = std::thread::spawn(move || {
754 use melin_transport_core::replication::protocol::*;
755
756 let mut reader = replica_stream.try_clone().unwrap();
757 let mut writer = replica_stream;
758
759 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
761 let nonce = decode_challenge(&frame).unwrap();
762
763 let wrong_key = SigningKey::from_bytes(&[0xEE; 32]);
765 let bad_signature = ed25519_dalek::Signer::sign(&wrong_key, &nonce);
766 let correct_pubkey = correct_key.verifying_key();
767
768 let mut buf = Vec::with_capacity(128);
769 encode_challenge_response(
770 &bad_signature.to_bytes(),
771 correct_pubkey.as_bytes(),
772 &mut buf,
773 );
774 writer.write_all(&buf).unwrap();
775 writer.flush().unwrap();
776
777 let result_frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
779 let ok = decode_auth_result(&result_frame).unwrap();
780 assert!(!ok, "should receive auth failure");
781 });
782
783 let mut reader = primary_stream.try_clone().unwrap();
784 let mut writer = primary_stream;
785 let result = authenticate_replica(&mut reader, &mut writer, &authorized_keys);
786 assert!(result.is_err());
787 assert!(
788 result
789 .unwrap_err()
790 .to_string()
791 .contains("signature verification failed"),
792 "should fail on signature verification"
793 );
794
795 replica_handle.join().unwrap();
796 }
797
798 #[test]
799 fn sender_receiver_end_to_end() {
800 use std::os::unix::net::UnixStream;
801
802 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
804
805 let replication_cursor = Arc::new(AtomicU64::new(0));
806
807 let _replica_cursor = Arc::clone(&replication_cursor);
809 let replica_handle = std::thread::spawn(move || {
810 let mut reader = replica_stream.try_clone().unwrap();
811 let mut writer = replica_stream;
812
813 let mut buf = Vec::new();
815 let handshake = Handshake {
816 last_sequence: 0,
817 chain_hash: [0u8; 32],
818 };
819 encode_handshake(&handshake, &mut buf);
820 writer.write_all(&buf).unwrap();
821 writer.flush().unwrap();
822 buf.clear();
823
824 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
826 let msg = decode_primary_message(&frame).unwrap();
827 assert!(matches!(msg, PrimaryMessage::StreamStart { .. }));
828
829 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
831 let slots: Vec<InputSlot> = try_decode_input_batch(&frame).expect("decode InputBatch");
832 let end_seq = slots
833 .last()
834 .map(|s| s.sequence)
835 .expect("InputBatch carried at least one slot");
836
837 let ack = Ack {
839 acked_sequence: end_seq,
840 in_memory_sequence: end_seq,
841 };
842 encode_ack(&ack, &mut buf);
843 writer.write_all(&buf).unwrap();
844 writer.flush().unwrap();
845
846 end_seq
847 });
848
849 let mut p_reader = primary_stream.try_clone().unwrap();
851 let mut p_writer = primary_stream;
852
853 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
855 let handshake = match decode_replica_message(&frame).unwrap() {
856 ReplicaMessage::Handshake(h) => h,
857 _ => panic!("expected Handshake"),
858 };
859 assert_eq!(handshake.last_sequence, 0);
860
861 let mut buf = Vec::new();
863 encode_stream_start(0, &[0u8; 32], &mut buf); p_writer.write_all(&buf).unwrap();
865 p_writer.flush().unwrap();
866 buf.clear();
867
868 encode_input_batch_with_seq(42, &mut buf);
870 p_writer.write_all(&buf).unwrap();
871 p_writer.flush().unwrap();
872 buf.clear();
873
874 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
876 let ack = match decode_replica_message(&frame).unwrap() {
877 ReplicaMessage::Ack(a) => a,
878 _ => panic!("expected Ack"),
879 };
880 assert_eq!(ack.acked_sequence, 42);
881
882 let end_seq = replica_handle.join().unwrap();
884 assert_eq!(end_seq, 42);
885 }
886
887 #[test]
888 fn disconnect_degrades_cursor_to_max() {
889 let cursor = Arc::new(AtomicU64::new(0));
894
895 let handshake_seq = 42u64;
897 cursor.store(handshake_seq + 1, Ordering::Release);
898 assert_eq!(cursor.load(Ordering::Acquire), 43);
899
900 cursor.fetch_max(100 + 1, Ordering::Release);
902 assert_eq!(cursor.load(Ordering::Acquire), 101);
903
904 cursor.store(u64::MAX, Ordering::Release);
906 assert_eq!(cursor.load(Ordering::Acquire), u64::MAX);
907
908 cursor.store(1, Ordering::Release);
910 assert_eq!(cursor.load(Ordering::Acquire), 1);
911 }
912
913 #[test]
914 fn ack_advances_cursor_monotonically() {
915 let cursor = Arc::new(AtomicU64::new(0));
918
919 let new_val = 100 + 1;
921 cursor.fetch_max(new_val, Ordering::Release);
922 assert_eq!(cursor.load(Ordering::Acquire), 101);
923
924 let stale_val = 50 + 1;
926 cursor.fetch_max(stale_val, Ordering::Release);
927 assert_eq!(cursor.load(Ordering::Acquire), 101);
928
929 let newer_val = 200 + 1;
931 cursor.fetch_max(newer_val, Ordering::Release);
932 assert_eq!(cursor.load(Ordering::Acquire), 201);
933 }
934
935 #[test]
936 fn multiple_data_batches_acked_in_order() {
937 use std::os::unix::net::UnixStream;
940
941 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
942
943 let replica_handle = std::thread::spawn(move || {
944 let mut reader = replica_stream.try_clone().unwrap();
945 let mut writer = replica_stream;
946 let mut buf = Vec::new();
947
948 encode_handshake(
950 &Handshake {
951 last_sequence: 0,
952 chain_hash: [0u8; 32],
953 },
954 &mut buf,
955 );
956 writer.write_all(&buf).unwrap();
957 writer.flush().unwrap();
958 buf.clear();
959
960 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
962 assert!(matches!(
963 decode_primary_message(&frame).unwrap(),
964 PrimaryMessage::StreamStart { .. }
965 ));
966
967 let mut acked_seqs = Vec::new();
969 for _ in 0..3 {
970 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
971 let slots: Vec<InputSlot> =
972 try_decode_input_batch(&frame).expect("decode InputBatch");
973 let end_seq = slots
974 .last()
975 .map(|s| s.sequence)
976 .expect("InputBatch carried at least one slot");
977 acked_seqs.push(end_seq);
978
979 encode_ack(
980 &Ack {
981 acked_sequence: end_seq,
982 in_memory_sequence: end_seq,
983 },
984 &mut buf,
985 );
986 writer.write_all(&buf).unwrap();
987 writer.flush().unwrap();
988 buf.clear();
989 }
990
991 acked_seqs
992 });
993
994 let mut p_reader = primary_stream.try_clone().unwrap();
996 let mut p_writer = primary_stream;
997 let mut buf = Vec::new();
998
999 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1001 assert!(matches!(
1002 decode_replica_message(&frame).unwrap(),
1003 ReplicaMessage::Handshake(_)
1004 ));
1005
1006 encode_stream_start(0, &[0u8; 32], &mut buf);
1008 p_writer.write_all(&buf).unwrap();
1009 p_writer.flush().unwrap();
1010 buf.clear();
1011
1012 for seq in [10u64, 20, 30] {
1014 encode_input_batch_with_seq(seq, &mut buf);
1015 p_writer.write_all(&buf).unwrap();
1016 p_writer.flush().unwrap();
1017 buf.clear();
1018 }
1019
1020 for expected_seq in [10u64, 20, 30] {
1022 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1023 let ack = match decode_replica_message(&frame).unwrap() {
1024 ReplicaMessage::Ack(a) => a,
1025 other => panic!("expected Ack, got {other:?}"),
1026 };
1027 assert_eq!(ack.acked_sequence, expected_seq);
1028 }
1029
1030 let acked = replica_handle.join().unwrap();
1031 assert_eq!(acked, vec![10, 20, 30]);
1032 }
1033
1034 #[test]
1035 fn heartbeat_encode_contains_sequence() {
1036 let mut buf = Vec::new();
1039 encode_heartbeat(999, &mut buf);
1040
1041 let payload = &buf[4..];
1042 match decode_primary_message(payload).unwrap() {
1043 PrimaryMessage::Heartbeat { sequence } => {
1044 assert_eq!(sequence, 999);
1045 }
1046 other => panic!("expected Heartbeat, got {other:?}"),
1047 }
1048 }
1049
1050 #[test]
1051 fn replica_mid_stream_handshake_with_nonzero_sequence() {
1052 use std::os::unix::net::UnixStream;
1057
1058 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1059
1060 let replica_handle = std::thread::spawn(move || {
1061 let mut reader = replica_stream.try_clone().unwrap();
1062 let mut writer = replica_stream;
1063 let mut buf = Vec::new();
1064
1065 encode_handshake(
1067 &Handshake {
1068 last_sequence: 100,
1069 chain_hash: [0xBB; 32],
1070 },
1071 &mut buf,
1072 );
1073 writer.write_all(&buf).unwrap();
1074 writer.flush().unwrap();
1075 buf.clear();
1076
1077 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1079 match decode_primary_message(&frame).unwrap() {
1080 PrimaryMessage::StreamStart { start_sequence, .. } => {
1081 assert_eq!(
1082 start_sequence, 100,
1083 "StreamStart should echo replica's last_sequence"
1084 );
1085 }
1086 other => panic!("expected StreamStart, got {other:?}"),
1087 }
1088
1089 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1091 let slots: Vec<InputSlot> = try_decode_input_batch(&frame).expect("decode InputBatch");
1092 let end_sequence = slots
1093 .last()
1094 .map(|s| s.sequence)
1095 .expect("InputBatch carried at least one slot");
1096 assert!(
1097 end_sequence > 100,
1098 "InputBatch should be after replica's last_sequence"
1099 );
1100 });
1101
1102 let mut p_reader = primary_stream.try_clone().unwrap();
1104 let mut p_writer = primary_stream;
1105 let mut buf = Vec::new();
1106
1107 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1109 let handshake = match decode_replica_message(&frame).unwrap() {
1110 ReplicaMessage::Handshake(h) => h,
1111 _ => panic!("expected Handshake"),
1112 };
1113 assert_eq!(handshake.last_sequence, 100);
1114 assert_eq!(handshake.chain_hash, [0xBB; 32]);
1115
1116 encode_stream_start(handshake.last_sequence, &[0u8; 32], &mut buf);
1118 p_writer.write_all(&buf).unwrap();
1119 p_writer.flush().unwrap();
1120 buf.clear();
1121
1122 encode_input_batch_with_seq(150, &mut buf);
1124 p_writer.write_all(&buf).unwrap();
1125 p_writer.flush().unwrap();
1126
1127 replica_handle.join().unwrap();
1128 }
1129
1130 #[test]
1131 fn snapshot_begin_encode_decode_round_trip() {
1132 let mut buf = Vec::new();
1133 encode_snapshot_begin(1_000_000, 42, &[0xAB; 32], &mut buf);
1134
1135 let payload = &buf[4..];
1136 let msg = decode_primary_message(payload).unwrap();
1137 match msg {
1138 PrimaryMessage::SnapshotBegin {
1139 snapshot_len,
1140 snap_sequence,
1141 snap_chain_hash,
1142 } => {
1143 assert_eq!(snapshot_len, 1_000_000);
1144 assert_eq!(snap_sequence, 42);
1145 assert_eq!(snap_chain_hash, [0xAB; 32]);
1146 }
1147 _ => panic!("expected SnapshotBegin"),
1148 }
1149 }
1150
1151 #[test]
1152 fn snapshot_chunk_encode_decode_round_trip() {
1153 let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
1154 let mut buf = Vec::new();
1155 encode_snapshot_chunk(&data, &mut buf);
1156
1157 let payload = &buf[4..];
1158 let msg = decode_primary_message(payload).unwrap();
1159 match msg {
1160 PrimaryMessage::SnapshotChunk(chunk) => {
1161 assert_eq!(chunk, data);
1162 }
1163 _ => panic!("expected SnapshotChunk"),
1164 }
1165 }
1166
1167 #[test]
1168 fn snapshot_end_encode_decode_round_trip() {
1169 let mut buf = Vec::new();
1170 encode_snapshot_end(0xDEADBEEF, &mut buf);
1171
1172 let payload = &buf[4..];
1173 let msg = decode_primary_message(payload).unwrap();
1174 match msg {
1175 PrimaryMessage::SnapshotEnd { crc32c } => {
1176 assert_eq!(crc32c, 0xDEADBEEF);
1177 }
1178 _ => panic!("expected SnapshotEnd"),
1179 }
1180 }
1181
1182 #[test]
1186 fn snapshot_receiver_detects_length_mismatch() {
1187 use std::os::unix::net::UnixStream;
1188
1189 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1190
1191 let receiver = std::thread::spawn(move || -> String {
1193 let mut reader = replica_stream.try_clone().unwrap();
1194
1195 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1197 assert!(matches!(
1198 decode_primary_message(&frame).unwrap(),
1199 PrimaryMessage::NeedSnapshot,
1200 ));
1201
1202 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1204 let (snap_len, _snap_sequence, _snap_chain_hash) =
1205 match decode_primary_message(&frame).unwrap() {
1206 PrimaryMessage::SnapshotBegin {
1207 snapshot_len,
1208 snap_sequence,
1209 snap_chain_hash,
1210 } => (snapshot_len, snap_sequence, snap_chain_hash),
1211 other => panic!("expected SnapshotBegin, got {other:?}"),
1212 };
1213
1214 let mut received: u64 = 0;
1216 loop {
1217 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1218 match decode_primary_message(&frame).unwrap() {
1219 PrimaryMessage::SnapshotChunk(data) => {
1220 received += data.len() as u64;
1221 }
1222 PrimaryMessage::SnapshotEnd { .. } => {
1223 if received != snap_len {
1224 return format!(
1225 "snapshot length mismatch: expected {snap_len} bytes, got {received}"
1226 );
1227 }
1228 return String::new(); }
1230 other => panic!("unexpected message: {other:?}"),
1231 }
1232 }
1233 });
1234
1235 let mut writer = primary_stream;
1237 let mut buf = Vec::new();
1238
1239 let actual_data = vec![0xAA; 100];
1240 let wrong_len = 999u64; encode_need_snapshot(&mut buf);
1243 std::io::Write::write_all(&mut writer, &buf).unwrap();
1244 buf.clear();
1245
1246 encode_snapshot_begin(wrong_len, 42, &[0xBB; 32], &mut buf);
1247 std::io::Write::write_all(&mut writer, &buf).unwrap();
1248 buf.clear();
1249
1250 encode_snapshot_chunk(&actual_data, &mut buf);
1251 std::io::Write::write_all(&mut writer, &buf).unwrap();
1252 buf.clear();
1253
1254 let crc = crc32c::crc32c(&actual_data);
1255 encode_snapshot_end(crc, &mut buf);
1256 std::io::Write::write_all(&mut writer, &buf).unwrap();
1257 std::io::Write::flush(&mut writer).unwrap();
1258
1259 let error_msg = receiver.join().unwrap();
1260 assert!(
1261 error_msg.contains("length mismatch"),
1262 "expected length mismatch error, got: {error_msg:?}"
1263 );
1264 }
1265
1266 #[test]
1270 fn snapshot_receiver_detects_crc_mismatch() {
1271 use std::os::unix::net::UnixStream;
1272
1273 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1274
1275 let receiver = std::thread::spawn(move || -> String {
1276 let mut reader = replica_stream.try_clone().unwrap();
1277
1278 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1280 assert!(matches!(
1281 decode_primary_message(&frame).unwrap(),
1282 PrimaryMessage::NeedSnapshot,
1283 ));
1284
1285 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1287 let snap_len = match decode_primary_message(&frame).unwrap() {
1288 PrimaryMessage::SnapshotBegin { snapshot_len, .. } => snapshot_len,
1289 other => panic!("expected SnapshotBegin, got {other:?}"),
1290 };
1291
1292 let mut received_data = Vec::new();
1294 let mut received: u64 = 0;
1295 loop {
1296 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1297 match decode_primary_message(&frame).unwrap() {
1298 PrimaryMessage::SnapshotChunk(data) => {
1299 received += data.len() as u64;
1300 received_data.extend_from_slice(&data);
1301 }
1302 PrimaryMessage::SnapshotEnd {
1303 crc32c: expected_crc,
1304 } => {
1305 if received != snap_len {
1306 return format!("length mismatch: {snap_len} vs {received}");
1307 }
1308 let actual_crc = crc32c::crc32c(&received_data);
1309 if actual_crc != expected_crc {
1310 return format!(
1311 "CRC mismatch: expected {expected_crc:#x}, got {actual_crc:#x}"
1312 );
1313 }
1314 return String::new();
1315 }
1316 other => panic!("unexpected message: {other:?}"),
1317 }
1318 }
1319 });
1320
1321 let mut writer = primary_stream;
1323 let mut buf = Vec::new();
1324
1325 let data = vec![0xAA; 100];
1326
1327 encode_need_snapshot(&mut buf);
1328 std::io::Write::write_all(&mut writer, &buf).unwrap();
1329 buf.clear();
1330
1331 encode_snapshot_begin(data.len() as u64, 42, &[0xBB; 32], &mut buf);
1332 std::io::Write::write_all(&mut writer, &buf).unwrap();
1333 buf.clear();
1334
1335 encode_snapshot_chunk(&data, &mut buf);
1336 std::io::Write::write_all(&mut writer, &buf).unwrap();
1337 buf.clear();
1338
1339 let wrong_crc = !crc32c::crc32c(&data);
1341 encode_snapshot_end(wrong_crc, &mut buf);
1342 std::io::Write::write_all(&mut writer, &buf).unwrap();
1343 std::io::Write::flush(&mut writer).unwrap();
1344
1345 let error_msg = receiver.join().unwrap();
1346 assert!(
1347 error_msg.contains("CRC mismatch"),
1348 "expected CRC mismatch error, got: {error_msg:?}"
1349 );
1350 }
1351
1352 #[test]
1355 fn snapshot_receiver_detects_chain_hash_mismatch() {
1356 use std::os::unix::net::UnixStream;
1357
1358 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1359
1360 let receiver = std::thread::spawn(move || -> String {
1361 let mut reader = replica_stream.try_clone().unwrap();
1362
1363 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1364 assert!(matches!(
1365 decode_primary_message(&frame).unwrap(),
1366 PrimaryMessage::NeedSnapshot,
1367 ));
1368
1369 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1370 let (snap_len, _snap_sequence, snap_chain_hash) =
1371 match decode_primary_message(&frame).unwrap() {
1372 PrimaryMessage::SnapshotBegin {
1373 snapshot_len,
1374 snap_sequence,
1375 snap_chain_hash,
1376 } => (snapshot_len, snap_sequence, snap_chain_hash),
1377 other => panic!("expected SnapshotBegin, got {other:?}"),
1378 };
1379
1380 let mut received_data = Vec::new();
1382 let mut received: u64 = 0;
1383 loop {
1384 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1385 match decode_primary_message(&frame).unwrap() {
1386 PrimaryMessage::SnapshotChunk(data) => {
1387 received += data.len() as u64;
1388 received_data.extend_from_slice(&data);
1389 }
1390 PrimaryMessage::SnapshotEnd {
1391 crc32c: expected_crc,
1392 } => {
1393 assert_eq!(received, snap_len, "length should match");
1394 let actual_crc = crc32c::crc32c(&received_data);
1395 assert_eq!(actual_crc, expected_crc, "CRC should match");
1396 break;
1397 }
1398 other => panic!("unexpected message: {other:?}"),
1399 }
1400 }
1401
1402 let loaded_hash = [0xFF; 32]; if loaded_hash != snap_chain_hash {
1406 return format!(
1407 "snapshot chain hash mismatch: primary sent {snap_chain_hash:02x?}, \
1408 loaded snapshot has {loaded_hash:02x?}"
1409 );
1410 }
1411 String::new()
1412 });
1413
1414 let mut writer = primary_stream;
1417 let mut buf = Vec::new();
1418
1419 let data = vec![0xAA; 64];
1420 let advertised_hash = [0xBB; 32];
1422
1423 encode_need_snapshot(&mut buf);
1424 std::io::Write::write_all(&mut writer, &buf).unwrap();
1425 buf.clear();
1426
1427 encode_snapshot_begin(data.len() as u64, 10, &advertised_hash, &mut buf);
1428 std::io::Write::write_all(&mut writer, &buf).unwrap();
1429 buf.clear();
1430
1431 encode_snapshot_chunk(&data, &mut buf);
1432 std::io::Write::write_all(&mut writer, &buf).unwrap();
1433 buf.clear();
1434
1435 let crc = crc32c::crc32c(&data);
1436 encode_snapshot_end(crc, &mut buf);
1437 std::io::Write::write_all(&mut writer, &buf).unwrap();
1438 std::io::Write::flush(&mut writer).unwrap();
1439
1440 let error_msg = receiver.join().unwrap();
1441 assert!(
1442 error_msg.contains("chain hash mismatch"),
1443 "expected chain hash mismatch error, got: {error_msg:?}"
1444 );
1445 }
1446
1447 #[test]
1450 fn primary_rejects_snapshot_with_invalid_magic() {
1451 let tmp = tempfile::tempdir().unwrap();
1452 let snap_path = tmp.path().join("test.snapshot");
1453
1454 let mut bad_snap = vec![0u8; 64];
1456 bad_snap[0..4].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes()); std::fs::write(&snap_path, &bad_snap).unwrap();
1459
1460 let snap_data = std::fs::read(&snap_path).unwrap();
1462 assert!(
1463 snap_data.len() >= 48,
1464 "file should be big enough for header"
1465 );
1466
1467 let magic = u32::from_le_bytes(snap_data[0..4].try_into().unwrap());
1468 assert_ne!(magic, 0x534E_4150);
1469 assert_eq!(magic, 0xDEAD_BEEF);
1470 }
1471
1472 #[test]
1475 fn primary_rejects_snapshot_too_small_for_header() {
1476 let tmp = tempfile::tempdir().unwrap();
1477 let snap_path = tmp.path().join("test.snapshot");
1478
1479 std::fs::write(&snap_path, [0u8; 20]).unwrap();
1481
1482 let snap_data = std::fs::read(&snap_path).unwrap();
1483 assert!(
1484 snap_data.len() < 48,
1485 "file must be too small for header validation"
1486 );
1487 }
1488
1489 #[test]
1490 fn decode_snapshot_begin_too_short() {
1491 let payload = [MSG_SNAPSHOT_BEGIN, 0x01, 0x02, 0x03];
1494 let err = decode_primary_message(&payload).unwrap_err();
1495 assert!(
1496 err.to_string().contains("SnapshotBegin too short"),
1497 "expected 'SnapshotBegin too short', got: {err}"
1498 );
1499 }
1500
1501 #[test]
1502 fn decode_snapshot_end_too_short() {
1503 let payload = [MSG_SNAPSHOT_END];
1505 let err = decode_primary_message(&payload).unwrap_err();
1506 assert!(
1507 err.to_string().contains("SnapshotEnd too short"),
1508 "expected 'SnapshotEnd too short', got: {err}"
1509 );
1510 }
1511
1512 #[test]
1513 fn decode_snapshot_chunk_empty_data() {
1514 let payload = [MSG_SNAPSHOT_CHUNK];
1516 let msg = decode_primary_message(&payload).unwrap();
1517 match msg {
1518 PrimaryMessage::SnapshotChunk(data) => {
1519 assert!(data.is_empty());
1520 }
1521 _ => panic!("expected SnapshotChunk"),
1522 }
1523 }
1524
1525 fn make_journal_cursor(val: u64) -> melin_pipeline::padding::Sequence {
1528 melin_pipeline::padding::Sequence::new(AtomicU64::new(val))
1529 }
1530
1531 #[test]
1532 fn pending_ack_queue_push_and_pop_ready() {
1533 let mut q = PendingAckQueue::new(8);
1534 assert!(q.is_empty());
1535 assert!(!q.is_full());
1536
1537 q.push(10, 100);
1538 q.push(20, 200);
1539 assert!(!q.is_empty());
1540
1541 let cursor = make_journal_cursor(5);
1543 assert!(q.pop_ready(&cursor).is_none());
1544
1545 cursor.get().store(15, Ordering::Relaxed);
1547 assert_eq!(q.pop_ready(&cursor), Some(100));
1548 assert!(!q.is_empty());
1550
1551 cursor.get().store(25, Ordering::Relaxed);
1553 assert_eq!(q.pop_ready(&cursor), Some(200));
1554 assert!(q.is_empty());
1555 }
1556
1557 #[test]
1558 fn pending_ack_queue_pop_ready_returns_highest_sequence() {
1559 let mut q = PendingAckQueue::new(8);
1563 q.push(10, 100);
1564 q.push(20, 200);
1565 q.push(30, 300);
1566
1567 let cursor = make_journal_cursor(30);
1568 assert_eq!(q.pop_ready(&cursor), Some(300));
1569 assert!(q.is_empty());
1570 }
1571
1572 #[test]
1573 fn pending_ack_queue_capacity_and_full() {
1574 let mut q = PendingAckQueue::new(8);
1575 for i in 0..8 {
1576 assert!(!q.is_full());
1577 q.push(i as u64 + 1, (i + 1) as u64 * 100);
1578 }
1579 assert!(q.is_full());
1580 }
1581
1582 #[test]
1583 fn pending_ack_queue_pop_oldest_blocking() {
1584 let mut q = PendingAckQueue::new(8);
1585 q.push(10, 100);
1586 q.push(20, 200);
1587
1588 let cursor = make_journal_cursor(25);
1591 let seq = q.pop_oldest_blocking(&cursor, true);
1592 assert_eq!(seq, 200);
1594 assert!(q.is_empty());
1595 }
1596
1597 #[test]
1598 fn pending_ack_queue_wraps_around() {
1599 let mut q = PendingAckQueue::new(8);
1600 let cursor = make_journal_cursor(100);
1601
1602 for round in 0..3 {
1604 for i in 0..8 {
1605 let target = (round * 8 + i) as u64 + 1;
1606 q.push(target, target * 10);
1607 }
1608 assert!(q.is_full());
1609 let seq = q.pop_ready(&cursor).expect("should be ready");
1610 assert_eq!(seq, (round * 8 + 8) as u64 * 10);
1611 assert!(q.is_empty());
1612 }
1613 }
1614
1615 #[test]
1616 fn pending_ack_queue_pop_all_blocking_empty() {
1617 let mut q = PendingAckQueue::new(8);
1618 let cursor = make_journal_cursor(0);
1619 assert!(q.pop_all_blocking(&cursor, true).is_none());
1620 }
1621
1622 #[test]
1625 fn dual_track_returns_none_when_both_tracks_idle() {
1626 let mut q = PendingAckQueue::new(8);
1627 let cursor = make_journal_cursor(0);
1628 assert!(
1629 try_flush_dual_track(&mut q, &cursor, 0, 0, 0).is_none(),
1630 "no advance on either track → no ack"
1631 );
1632 }
1633
1634 #[test]
1635 fn dual_track_fires_on_persisted_advance_only() {
1636 let mut q = PendingAckQueue::new(8);
1641 q.push(10, 100);
1642 let cursor = make_journal_cursor(20);
1643 let ack = try_flush_dual_track(&mut q, &cursor, 50, 0, 50).expect("persisted advanced");
1644 assert_eq!(ack.acked_sequence, 100);
1645 assert_eq!(ack.in_memory_sequence, 50);
1646 }
1647
1648 #[test]
1649 fn dual_track_fires_on_in_memory_advance_only() {
1650 let mut q = PendingAckQueue::new(8);
1653 let cursor = make_journal_cursor(0);
1654 let ack = try_flush_dual_track(&mut q, &cursor, 200, 100, 100).expect("in-memory advanced");
1655 assert_eq!(ack.acked_sequence, 100);
1656 assert_eq!(ack.in_memory_sequence, 200);
1657 }
1658
1659 #[test]
1660 fn dual_track_coalesces_until_caller_updates_trackers() {
1661 let mut q = PendingAckQueue::new(8);
1667 q.push(10, 100);
1668 let cursor = make_journal_cursor(20);
1669 let ack1 = try_flush_dual_track(&mut q, &cursor, 50, 0, 50).expect("call 1 fires");
1670 assert_eq!(ack1.acked_sequence, 100);
1671 let ack2 = try_flush_dual_track(&mut q, &cursor, 80, 0, 50)
1673 .expect("call 2 fires on in-memory advance");
1674 assert_eq!(
1675 ack2.acked_sequence, 0,
1676 "no new persisted pop → unwrap_or(last_sent)"
1677 );
1678 assert_eq!(ack2.in_memory_sequence, 80);
1679 }
1680
1681 #[test]
1682 fn dual_track_no_duplicate_ack_after_backpressure_drain() {
1683 let mut q = PendingAckQueue::new(4);
1701 for i in 1..=4u64 {
1704 q.push(i * 10, i * 100);
1705 }
1706 assert!(q.is_full());
1707
1708 let cursor = make_journal_cursor(40);
1711 let drained = q.pop_ready(&cursor).expect("all entries durable");
1712 assert_eq!(drained, 400);
1713 assert!(q.is_empty());
1714
1715 let mut last_sent_acked = drained;
1718 let mut last_sent_in_mem = 450u64;
1719
1720 assert!(
1725 try_flush_dual_track(
1726 &mut q,
1727 &cursor,
1728 last_sent_in_mem,
1729 last_sent_acked,
1730 last_sent_in_mem,
1731 )
1732 .is_none(),
1733 "no advance on either track after backpressure drain → no duplicate ack",
1734 );
1735
1736 q.push(50, 500);
1739 let cursor2 = make_journal_cursor(50);
1740 let ack = try_flush_dual_track(&mut q, &cursor2, 500, last_sent_acked, last_sent_in_mem)
1741 .expect("fresh batch after drain fires a new ack");
1742 assert!(
1743 ack.acked_sequence >= last_sent_acked,
1744 "regression: drain sent {last_sent_acked} but next ack carries {}",
1745 ack.acked_sequence,
1746 );
1747 assert_eq!(ack.acked_sequence, 500);
1748 assert_eq!(ack.in_memory_sequence, 500);
1749
1750 last_sent_acked = ack.acked_sequence;
1751 last_sent_in_mem = ack.in_memory_sequence;
1752
1753 assert!(
1757 try_flush_dual_track(
1758 &mut q,
1759 &cursor2,
1760 last_sent_in_mem,
1761 last_sent_acked,
1762 last_sent_in_mem,
1763 )
1764 .is_none(),
1765 "post-resume idle → no further ack",
1766 );
1767 }
1768
1769 #[test]
1772 fn dual_cursor_takes_min_and_max_of_both_slots() {
1773 let cursor_min = Arc::new(AtomicU64::new(0));
1774 let cursor_max = Arc::new(AtomicU64::new(0));
1775 update_dual_replication_cursor(100, 50, &cursor_min, &cursor_max);
1777 assert_eq!(cursor_min.load(Ordering::Relaxed), 50);
1778 assert_eq!(cursor_max.load(Ordering::Relaxed), 100);
1779 }
1780
1781 #[test]
1782 fn dual_cursor_idle_slot_uses_max() {
1783 let cursor_min = Arc::new(AtomicU64::new(0));
1784 let cursor_max = Arc::new(AtomicU64::new(0));
1785 update_dual_replication_cursor(100, u64::MAX, &cursor_min, &cursor_max);
1787 assert_eq!(cursor_min.load(Ordering::Relaxed), 100);
1788 assert_eq!(cursor_max.load(Ordering::Relaxed), u64::MAX);
1789 }
1790
1791 #[test]
1792 fn dual_cursor_both_idle() {
1793 let cursor_min = Arc::new(AtomicU64::new(42));
1794 let cursor_max = Arc::new(AtomicU64::new(42));
1795 update_dual_replication_cursor(u64::MAX, u64::MAX, &cursor_min, &cursor_max);
1797 assert_eq!(cursor_min.load(Ordering::Relaxed), u64::MAX);
1798 assert_eq!(cursor_max.load(Ordering::Relaxed), u64::MAX);
1799 }
1800
1801 #[test]
1802 fn dual_cursor_decreases_when_slower_replica_connects() {
1803 let cursor_min = Arc::new(AtomicU64::new(0));
1804 let cursor_max = Arc::new(AtomicU64::new(0));
1805
1806 update_dual_replication_cursor(100, u64::MAX, &cursor_min, &cursor_max);
1808 assert_eq!(cursor_min.load(Ordering::Relaxed), 100);
1809
1810 update_dual_replication_cursor(51, 100, &cursor_min, &cursor_max);
1813 assert_eq!(cursor_min.load(Ordering::Relaxed), 51);
1814 assert_eq!(cursor_max.load(Ordering::Relaxed), 100);
1815 }
1816
1817 #[test]
1818 fn dual_cursor_advances_as_slower_replica_catches_up() {
1819 let cursor_min = Arc::new(AtomicU64::new(0));
1820 let cursor_max = Arc::new(AtomicU64::new(0));
1821
1822 update_dual_replication_cursor(51, 100, &cursor_min, &cursor_max);
1824 assert_eq!(cursor_min.load(Ordering::Relaxed), 51);
1825 assert_eq!(cursor_max.load(Ordering::Relaxed), 100);
1826
1827 update_dual_replication_cursor(80, 100, &cursor_min, &cursor_max);
1829 assert_eq!(cursor_min.load(Ordering::Relaxed), 80);
1830
1831 update_dual_replication_cursor(100, 100, &cursor_min, &cursor_max);
1833 assert_eq!(cursor_min.load(Ordering::Relaxed), 100);
1834 assert_eq!(cursor_max.load(Ordering::Relaxed), 100);
1835
1836 update_dual_replication_cursor(150, 150, &cursor_min, &cursor_max);
1838 assert_eq!(cursor_min.load(Ordering::Relaxed), 150);
1839 assert_eq!(cursor_max.load(Ordering::Relaxed), 150);
1840 }
1841
1842 #[test]
1843 fn dual_cursor_slot_disconnect_raises_to_surviving() {
1844 let cursor_min = Arc::new(AtomicU64::new(0));
1845 let cursor_max = Arc::new(AtomicU64::new(0));
1846
1847 update_dual_replication_cursor(80, 100, &cursor_min, &cursor_max);
1849 assert_eq!(cursor_min.load(Ordering::Relaxed), 80);
1850 assert_eq!(cursor_max.load(Ordering::Relaxed), 100);
1851
1852 update_dual_replication_cursor(100, u64::MAX, &cursor_min, &cursor_max);
1854 assert_eq!(cursor_min.load(Ordering::Relaxed), 100);
1855 assert_eq!(cursor_max.load(Ordering::Relaxed), u64::MAX);
1856 }
1857
1858 #[test]
1861 fn disconnect_resets_cursor_to_max() {
1862 let cursor = Arc::new(AtomicU64::new(42));
1865 let replicas_connected = Arc::new(AtomicU32::new(1));
1866
1867 replicas_connected.fetch_sub(1, Ordering::Release);
1869
1870 if replicas_connected.load(Ordering::Relaxed) == 0 {
1872 cursor.store(u64::MAX, Ordering::Release);
1873 }
1874
1875 assert_eq!(cursor.load(Ordering::Relaxed), u64::MAX);
1876 }
1877
1878 #[test]
1879 fn cursor_not_reset_when_replica_still_connected() {
1880 let cursor = Arc::new(AtomicU64::new(42));
1881 let replicas_connected = Arc::new(AtomicU32::new(2));
1882
1883 replicas_connected.fetch_sub(1, Ordering::Release);
1885
1886 if replicas_connected.load(Ordering::Relaxed) == 0 {
1887 cursor.store(u64::MAX, Ordering::Release);
1888 }
1889
1890 assert_eq!(cursor.load(Ordering::Relaxed), 42);
1892 }
1893}