1use std::sync::Arc;
59#[cfg(test)]
60use std::sync::atomic::AtomicU64;
61use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
62
63use melin_journal::{BufferedWriter, JournalWrite};
64
65use melin_app::Application;
66use melin_transport_core::pipeline::{InputSlot, OutputSlot};
67use melin_transport_core::replication::archive::{ArchiveReason, archive_local_lineage};
68use melin_transport_core::replication::protocol::{MAX_CONTROL_FRAME, decode_primary_message};
69
70mod auth;
71#[cfg(feature = "dpdk")]
72mod dpdk;
73mod receiver_transport;
74mod tcp_receiver;
75mod tcp_sender;
76#[cfg(any(feature = "dpdk", test))]
81mod validation_worker;
82
83use receiver_transport::{ControlFrameSource, SessionExit, StreamingResult, receive_chunked_body};
84
85pub(crate) struct ReplicaGate<'a> {
98 count: &'a AtomicU32,
99}
100
101impl<'a> ReplicaGate<'a> {
102 pub(crate) fn new(count: &'a AtomicU32) -> Self {
103 Self { count }
104 }
105
106 pub(crate) fn lift(&self) {
109 self.count.fetch_add(1, Ordering::Release);
110 }
111
112 pub(crate) fn lower(&self) -> bool {
119 let was_last = self.count.fetch_sub(1, Ordering::Release) == 1;
120 if was_last {
121 tracing::warn!("all replicas disconnected — trading halted");
122 }
123 was_last
124 }
125}
126
127pub use melin_transport_core::replication::ack_queue::{
134 PendingAck, PendingAckQueue, try_flush_dual_track, wait_for_journal_cursor,
135};
136pub use melin_transport_core::replication::protocol::{
137 Ack, Handshake, PrimaryMessage, ReplicaMessage,
138};
139pub use melin_transport_core::replication::{ReplicaCursors, ReplicationMetrics, SentHighWater};
140
141#[cfg(feature = "dpdk")]
142pub(crate) use dpdk::REPL_DISPATCH_BURST;
143#[cfg(feature = "dpdk")]
144pub use dpdk::{DpdkReplicationDriver, run_receiver_dpdk};
145pub use tcp_receiver::{ReceiverResult, run_receiver};
146pub use tcp_sender::{ReplicationListener, Sender, run_sender};
147
148#[derive(Clone)]
155pub struct ReplicaControlPlane {
156 pub promote: crate::promotion::PromotionRequest,
159 pub tip_ready: std::sync::Arc<AtomicBool>,
163 pub journal_tip: melin_transport_core::AdvertisedJournalTip,
166 pub primary_link_up: std::sync::Arc<AtomicBool>,
173 pub primary_ack_policy: std::sync::Arc<std::sync::atomic::AtomicU8>,
183 pub pipeline_healthy: std::sync::Arc<AtomicBool>,
191}
192
193impl ReplicaControlPlane {
194 pub fn new() -> Self {
198 Self {
199 promote: crate::promotion::PromotionRequest::new(),
200 tip_ready: std::sync::Arc::new(AtomicBool::new(false)),
201 journal_tip: melin_transport_core::AdvertisedJournalTip::new(
202 melin_transport_core::WireSeq::new(0),
203 ),
204 primary_link_up: std::sync::Arc::new(AtomicBool::new(false)),
205 primary_ack_policy: std::sync::Arc::new(std::sync::atomic::AtomicU8::new(
206 crate::ack_policy::ACK_POLICY_UNKNOWN,
207 )),
208 pipeline_healthy: std::sync::Arc::new(AtomicBool::new(true)),
209 }
210 }
211}
212
213impl Default for ReplicaControlPlane {
214 fn default() -> Self {
215 Self::new()
216 }
217}
218
219pub(super) fn log_tcp_info(fd: std::os::unix::io::RawFd, tag: &str, slot: usize) {
228 if !tracing::enabled!(tracing::Level::DEBUG) {
229 return;
230 }
231 let mut info: libc::tcp_info = unsafe { std::mem::zeroed() };
235 let mut len = std::mem::size_of::<libc::tcp_info>() as libc::socklen_t;
236 let rc = unsafe {
237 libc::getsockopt(
238 fd,
239 libc::IPPROTO_TCP,
240 libc::TCP_INFO,
241 &mut info as *mut _ as *mut libc::c_void,
242 &mut len,
243 )
244 };
245 if rc != 0 {
246 tracing::debug!(
247 slot,
248 tag,
249 errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
250 "tcp_info getsockopt failed"
251 );
252 return;
253 }
254 tracing::debug!(
255 slot,
256 tag,
257 state = info.tcpi_state,
258 ca_state = info.tcpi_ca_state,
259 rtt_us = info.tcpi_rtt,
260 rttvar_us = info.tcpi_rttvar,
261 snd_cwnd = info.tcpi_snd_cwnd,
262 snd_ssthresh = info.tcpi_snd_ssthresh,
263 snd_mss = info.tcpi_snd_mss,
264 rcv_mss = info.tcpi_rcv_mss,
265 unacked = info.tcpi_unacked,
266 retrans = info.tcpi_retrans,
267 total_retrans = info.tcpi_total_retrans,
268 lost = info.tcpi_lost,
269 rcv_space = info.tcpi_rcv_space,
270 rto_us = info.tcpi_rto,
271 "tcp_info"
272 );
273}
274
275pub(super) fn sleep_checking_flags(
279 duration: std::time::Duration,
280 shutdown: &AtomicBool,
281 promote: &crate::promotion::PromotionRequest,
282) {
283 let deadline = std::time::Instant::now() + duration;
284 while std::time::Instant::now() < deadline {
285 if shutdown.load(Ordering::Relaxed) || promote.is_requested() {
286 return;
287 }
288 std::thread::sleep(std::time::Duration::from_millis(100));
289 }
290}
291
292pub(super) fn sleep_then_double_backoff(
299 backoff: &mut std::time::Duration,
300 shutdown: &AtomicBool,
301 promote: &crate::promotion::PromotionRequest,
302) {
303 sleep_checking_flags(*backoff, shutdown, promote);
304 *backoff = (*backoff * 2).min(MAX_BACKOFF);
305}
306
307pub(super) enum TeardownOutcome<A, W> {
311 Clean(A, W),
314 JournalFailed(melin_journal::JournalError),
320 Panicked,
322}
323
324pub(super) fn shutdown_pipeline<A: Send + 'static, W: Send + 'static>(
339 shutdown_flag: &AtomicBool,
340 journal_handle: std::thread::JoinHandle<Result<W, melin_journal::JournalError>>,
341 matching_handle: std::thread::JoinHandle<A>,
342 drain_handle: std::thread::JoinHandle<()>,
343 shadow_handle: Option<std::thread::JoinHandle<()>>,
344) -> TeardownOutcome<A, W> {
345 shutdown_flag.store(true, Ordering::Release);
359 let journal_result = journal_handle.join();
364 let matching_result = matching_handle.join();
365 let _ = drain_handle.join();
366 if let Some(h) = shadow_handle {
367 let _ = h.join();
368 }
369 let writer = match journal_result {
370 Ok(Ok(w)) => w,
371 Ok(Err(e)) => return TeardownOutcome::JournalFailed(e),
373 Err(_) => return TeardownOutcome::Panicked,
374 };
375 match matching_result {
376 Ok(exchange) => TeardownOutcome::Clean(exchange, writer),
377 Err(_) => TeardownOutcome::Panicked,
378 }
379}
380
381pub(super) struct ReplicaPipelineHandles<A: Application, W: Send + 'static> {
390 pub(super) input_producer: melin_pipeline::ring::Producer<InputSlot<A::Event>>,
391 pub(super) journal_cursor: Arc<melin_pipeline::padding::Sequence>,
392 pub(super) last_seq: melin_transport_core::DurableWireSeqCursor,
398 pub(super) chain_hash_lock:
402 Option<melin_pipeline::seqlock::SeqLockReader<melin_transport_core::pipeline::FsyncState>>,
403 pub(super) stream_marks: melin_transport_core::pipeline::StreamMarkQueue,
408 pub(super) journal_failed: Arc<AtomicBool>,
415 pub(super) pipeline_shutdown: Arc<AtomicBool>,
418 pub(super) journal_handle: std::thread::JoinHandle<Result<W, melin_journal::JournalError>>,
419 pub(super) matching_handle: std::thread::JoinHandle<A>,
420 pub(super) drain_handle: std::thread::JoinHandle<()>,
421 pub(super) shadow_handle: Option<std::thread::JoinHandle<()>>,
422}
423
424pub(super) type ReplicaHandles<A> =
429 ReplicaPipelineHandles<A, BufferedWriter<<A as Application>::Event>>;
430
431#[allow(clippy::too_many_arguments)]
435pub(super) fn build_replica_pipeline_with_threads<A>(
436 mut exchange: A,
437 writer: BufferedWriter<A::Event>,
438 cores: crate::layout::PipelineCores,
439 staging_mode: melin_journal::StagingMode,
444 snapshot_interval_ms: u64,
445 snapshot_path: std::path::PathBuf,
446 group_commit_delay: std::time::Duration,
447 fence_state: Arc<melin_transport_core::fence::FenceState>,
448 pipeline_healthy: Arc<AtomicBool>,
454 sizing: &A::Sizing,
457) -> Result<ReplicaHandles<A>, Box<dyn std::error::Error>>
458where
459 A: Application + Send + 'static,
460 A::Event: Send + Sync + 'static,
461 A::Report: Send + 'static,
462 A::QueryResponse: Send + 'static,
463{
464 <A as Application>::prefault(&mut exchange, sizing);
469 let shadow_exchange = <A as Application>::clone_via_snapshot(&exchange)?;
470
471 let enable_shadow = snapshot_interval_ms > 0;
472 let shadow_initial_epoch = fence_state.epoch();
475 let pipeline = melin_transport_core::pipeline::build_replica_pipeline(
476 exchange,
477 writer,
478 4096, group_commit_delay,
480 cores.stage_waits(),
481 enable_shadow,
482 fence_state,
483 );
484
485 let pipeline_shutdown = Arc::new(AtomicBool::new(false));
486
487 let ps = Arc::clone(&pipeline_shutdown);
488 let journal_core = cores.journal_seq.core;
489 let mut journal_stage = pipeline.journal_stage;
490 let stream_marks: melin_transport_core::pipeline::StreamMarkQueue =
495 Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new()));
496 journal_stage.set_stream_marks(Arc::clone(&stream_marks));
497 cores.place_journal_children(&mut journal_stage);
498 journal_stage.set_staging_mode(staging_mode);
499 let journal_failed = Arc::new(AtomicBool::new(false));
500 let journal_failed_latch = Arc::clone(&journal_failed);
501 let sequencer = journal_stage.start()?;
505 pipeline_healthy.store(true, Ordering::Release);
508 let journal_handle = std::thread::Builder::new()
509 .name("journal-seq".into())
510 .spawn(move || {
511 melin_app::affinity::pin_thread("journal-seq", journal_core);
512 let result = sequencer.run(&ps);
513 if let Err(ref e) = result {
514 journal_failed_latch.store(true, Ordering::Release);
521 pipeline_healthy.store(false, Ordering::Release);
524 tracing::error!(error = %e, "replica journal stage failed — session teardown");
525 }
526 result
527 })
528 .expect("spawn journal thread");
529
530 let ps = Arc::clone(&pipeline_shutdown);
531 let matching_core = cores.matching.core;
532 let matching_stage = pipeline.matching_stage;
533 let matching_handle = std::thread::Builder::new()
534 .name("matching".into())
535 .spawn(move || {
536 melin_app::affinity::pin_thread("matching", matching_core);
537 matching_stage.run(&ps)
538 })
539 .expect("spawn matching thread");
540
541 let ps = Arc::clone(&pipeline_shutdown);
544 let drain = cores.response;
545 let drain_consumer = pipeline.drain_consumer;
546 let drain_handle = std::thread::Builder::new()
547 .name("drain".into())
548 .spawn(move || {
549 melin_app::affinity::pin_thread("drain", drain.core);
550 let mut consumer = drain_consumer;
551 let mut batch = vec![OutputSlot::<A::Report, A::QueryResponse>::default(); 256];
552 let mut waiter = drain.wait.waiter();
553 loop {
554 if ps.load(Ordering::Relaxed) {
555 return;
556 }
557 let count = consumer.consume_batch(&mut batch, 256);
558 if count == 0 {
559 waiter.idle();
560 } else {
561 waiter.reset();
562 }
563 }
564 })
565 .expect("spawn drain thread");
566
567 let shadow_handle = if let Some(shadow_cons) = pipeline.shadow_consumer {
568 let snap_path = snapshot_path;
569 let chain_lock = pipeline
570 .chain_hash_lock
571 .as_ref()
572 .expect("chain hash lock with shadow")
573 .clone();
574 let ps = Arc::clone(&pipeline_shutdown);
575 let shadow = cores.shadow;
576 Some(
577 std::thread::Builder::new()
578 .name("replica-shadow".into())
579 .spawn(move || {
580 melin_app::affinity::pin_thread("replica-shadow", shadow.core);
581 melin_transport_core::shadow::run(
582 shadow_cons,
583 shadow_exchange,
584 snap_path,
585 std::time::Duration::from_millis(snapshot_interval_ms),
586 chain_lock,
587 &ps,
588 shadow.wait,
589 shadow_initial_epoch,
590 );
591 })
592 .expect("spawn shadow thread"),
593 )
594 } else {
595 None
596 };
597
598 Ok(ReplicaPipelineHandles {
599 input_producer: pipeline.input_producer,
600 journal_cursor: pipeline.cursors.journal_ring_arc(),
601 last_seq: pipeline.cursors.durable_wire_seq(),
602 chain_hash_lock: pipeline.chain_hash_lock,
603 stream_marks,
604 journal_failed,
605 pipeline_shutdown,
606 journal_handle,
607 matching_handle,
608 drain_handle,
609 shadow_handle,
610 })
611}
612
613pub(super) fn teardown_replica_pipeline<A: Application + Send + 'static, W: Send + 'static>(
618 mut handles: ReplicaPipelineHandles<A, W>,
619) -> TeardownOutcome<A, W> {
620 while !handles.journal_failed.load(Ordering::Acquire) {
637 match handles
638 .input_producer
639 .try_publish(InputSlot::<A::Event>::shutdown_sentinel())
640 {
641 Ok(_) => break,
642 Err(_) => std::thread::yield_now(),
644 }
645 }
646 shutdown_pipeline::<A, W>(
647 &handles.pipeline_shutdown,
648 handles.journal_handle,
649 handles.matching_handle,
650 handles.drain_handle,
651 handles.shadow_handle,
652 )
653}
654
655pub(super) const MAX_INPROCESS_DIVERGENCE_RESYNCS: u32 = 1;
666
667#[allow(clippy::type_complexity)]
684pub(super) fn recover_replica_state<A, W>(
685 journal_path: &std::path::Path,
686 snapshot_path: &std::path::Path,
687 fence_state: &melin_transport_core::fence::FenceState,
688 sizing: &A::Sizing,
691) -> Result<(Option<A>, Option<W>, u64, [u8; 32]), Box<dyn std::error::Error>>
692where
693 A: Application,
694 W: melin_journal::JournalWrite<A::Event>,
695{
696 let lineage_exists =
697 journal_path.exists() || !melin_journal::segment::list_archives(journal_path)?.is_empty();
698 if !lineage_exists {
699 return Ok((None, None, 0u64, [0u8; 32]));
700 }
701 let engine = if snapshot_path.exists() {
702 tracing::info!("recovering replica from snapshot + journal");
703 melin_transport_core::JournaledApp::<A, W>::recover_from_snapshot(
704 snapshot_path,
705 journal_path,
706 )?
707 } else {
708 let mut app = A::default();
709 <A as Application>::prefault(&mut app, sizing);
710 melin_transport_core::JournaledApp::<A, W>::recover(app, journal_path)?
711 };
712 let next = engine.next_sequence();
713 let last = next.saturating_sub(1);
714 let hash = engine.chain_hash().unwrap_or([0u8; 32]);
715 fence_state.observe_epoch(engine.recovered_epoch());
718 let (exchange, writer) = engine.into_parts();
719 Ok((Some(exchange), Some(writer), last, hash))
720}
721
722pub(super) const MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(30);
728
729pub(in crate::replication) enum AfterSession<A, W> {
733 Return(ReceiverResult<A, W>),
737 Resync {
742 exchange: Option<A>,
743 journal_writer: Option<W>,
744 last_sequence: u64,
745 chain_hash: [u8; 32],
746 },
747 Reconnect,
751}
752
753#[allow(clippy::too_many_arguments)]
774pub(in crate::replication) fn handle_session_exit<A, W>(
775 result: StreamingResult,
776 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
777 divergence_resyncs: &mut u32,
778 backoff: &mut std::time::Duration,
779 last_sequence: u64,
780 journal_path: &std::path::Path,
781 snapshot_path: &std::path::Path,
782 fence_state: &melin_transport_core::fence::FenceState,
783 shutdown: &AtomicBool,
784 promote: &crate::promotion::PromotionRequest,
785 mut close: impl FnMut(),
786 sizing: &A::Sizing,
789) -> AfterSession<A, W>
790where
791 A: Application + Send + 'static,
792 W: JournalWrite<A::Event> + Send + 'static,
793{
794 let StreamingResult {
795 exit,
796 heard_from_primary,
797 } = result;
798
799 match exit {
800 SessionExit::Shutdown => {
801 if let Some(p) = pipeline.take() {
802 let _ = teardown_replica_pipeline::<A, W>(p);
803 }
804 AfterSession::Return(Ok(None))
805 }
806
807 SessionExit::Promote => AfterSession::Return(match pipeline.take() {
808 Some(p) => match teardown_replica_pipeline::<A, W>(p) {
809 TeardownOutcome::Clean(ex, wr) => Ok(Some((ex, wr))),
810 _ => Err("pipeline failed during promotion".into()),
811 },
812 None => Err("pipeline missing on promote".into()),
813 }),
814
815 SessionExit::Fatal(e) => {
816 let outcome = match pipeline.take() {
817 Some(p) => teardown_replica_pipeline::<A, W>(p),
818 None => return AfterSession::Return(Err(e)),
821 };
822 let TeardownOutcome::JournalFailed(
832 je @ melin_journal::JournalError::ReplicaChainDivergence { .. },
833 ) = outcome
834 else {
835 return AfterSession::Return(Err(e));
836 };
837
838 *divergence_resyncs += 1;
839 let attempt = *divergence_resyncs;
840 if attempt > MAX_INPROCESS_DIVERGENCE_RESYNCS {
841 return AfterSession::Return(Err(format!(
842 "mid-stream chain divergence recurred {attempt} times — giving up on \
843 in-process resync (each cycle archives the local journal and re-seeds \
844 from the primary; recurrence at this rate means the primary keeps \
845 streaming history that forks from what it announces): {je}"
846 )
847 .into()));
848 }
849 tracing::warn!(
850 error = %je,
851 attempt,
852 max_attempts = MAX_INPROCESS_DIVERGENCE_RESYNCS,
853 "mid-stream chain divergence — re-deriving local state for in-process resync"
854 );
855 close();
857 match recover_replica_state::<A, W>(journal_path, snapshot_path, fence_state, sizing) {
858 Ok((exchange, journal_writer, seq, hash)) => AfterSession::Resync {
859 exchange,
860 journal_writer,
861 last_sequence: seq,
862 chain_hash: hash,
863 },
864 Err(e) => AfterSession::Return(Err(e)),
865 }
866 }
867
868 SessionExit::StreamGap(e) => {
869 close();
878 *backoff = std::time::Duration::from_secs(1);
879 tracing::warn!(
880 error = %e,
881 last_sequence,
882 "replication stream broke contiguity — reconnecting from the durable position"
883 );
884 sleep_then_double_backoff(backoff, shutdown, promote);
885 AfterSession::Reconnect
886 }
887
888 SessionExit::Disconnected => {
889 close();
892 if heard_from_primary {
901 *backoff = std::time::Duration::from_secs(1);
902 }
903 tracing::warn!(
904 last_sequence,
905 heard_from_primary,
906 backoff_secs = backoff.as_secs(),
907 "reconnecting to primary"
908 );
909 sleep_then_double_backoff(backoff, shutdown, promote);
910 AfterSession::Reconnect
911 }
912 }
913}
914
915pub(in crate::replication) fn take_pipeline_for_promotion<A, W>(
925 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
926 exchange: &mut Option<A>,
927 journal_writer: &mut Option<W>,
928) -> ReceiverResult<A, W>
929where
930 A: Application + Send + 'static,
931 W: JournalWrite<A::Event> + Send + 'static,
932{
933 if let Some(p) = pipeline.take()
934 && let TeardownOutcome::Clean(e, w) = teardown_replica_pipeline::<A, W>(p)
935 {
936 *exchange = Some(e);
937 *journal_writer = Some(w);
938 }
939 match (exchange.take(), journal_writer.take()) {
940 (Some(e), Some(w)) => Ok(Some((e, w))),
941 _ => Err("promotion requested but no local state available".into()),
942 }
943}
944
945type ResyncTransfer<A> = (A, u64, [u8; 32], u64);
948
949pub(in crate::replication) enum ResyncDecision {
952 Ready {
957 segment_start_sequence: u64,
958 anchor_hash: [u8; 32],
959 resume_sequence: u64,
960 },
961 Retry,
965}
966
967fn receive_resync_transfer<A, S>(
977 source: &mut S,
978 snapshot_path: &std::path::Path,
979 journal_path: &std::path::Path,
980 fence_state: &melin_transport_core::fence::FenceState,
981) -> Result<ResyncTransfer<A>, Box<dyn std::error::Error + Send + Sync>>
982where
983 A: Application,
984 S: ControlFrameSource,
985{
986 let (snap_len, snap_sequence, snap_chain_hash) =
987 match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
988 PrimaryMessage::SnapshotBegin {
989 snapshot_len,
990 snap_sequence,
991 snap_chain_hash,
992 } => (snapshot_len, snap_sequence, snap_chain_hash),
993 other => return Err(format!("expected SnapshotBegin, got {other:?}").into()),
994 };
995
996 tracing::info!(snap_sequence, snap_len, "receiving snapshot");
997 let tmp_path = snapshot_path.with_extension("snapshot.tmp");
998 receive_chunked_body(source, &tmp_path, snap_len, "snapshot")?;
999 std::fs::rename(&tmp_path, snapshot_path)?;
1000 tracing::info!(snap_sequence, snap_len, "snapshot received and verified");
1001
1002 let (snap_exchange, _snap_seq, snap_hash, snap_epoch) =
1003 melin_transport_core::snapshot::load::<A>(snapshot_path)?;
1004 if snap_hash != snap_chain_hash {
1005 return Err(format!(
1006 "snapshot chain hash mismatch: primary sent {snap_chain_hash:02x?}, \
1007 loaded snapshot has {snap_hash:02x?}"
1008 )
1009 .into());
1010 }
1011 fence_state.observe_epoch(snap_epoch);
1014
1015 let seed_len = match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
1020 PrimaryMessage::SegmentSeedBegin { seed_len } => seed_len,
1021 other => {
1022 return Err(format!("expected SegmentSeedBegin after snapshot, got {other:?}").into());
1023 }
1024 };
1025 let seed_tmp = journal_path.with_extension("seed.tmp");
1026 receive_chunked_body(source, &seed_tmp, seed_len, "segment seed")?;
1027 if let Err(e) =
1033 melin_journal::segment::verify_segment_prefix(&seed_tmp, snap_sequence, seed_len)
1034 {
1035 let _ = std::fs::remove_file(&seed_tmp);
1036 return Err(format!("segment seed failed structural verification: {e}").into());
1037 }
1038 std::fs::rename(&seed_tmp, journal_path)?;
1039 melin_journal::segment::fsync_parent_dir(journal_path)?;
1040 Ok((snap_exchange, snap_sequence, snap_chain_hash, seed_len))
1041}
1042
1043#[allow(clippy::too_many_arguments)]
1056pub(in crate::replication) fn handle_resync_verdict<A, W, S>(
1057 divergent: bool,
1058 source: &mut S,
1059 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
1060 exchange: &mut Option<A>,
1061 journal_writer: &mut Option<W>,
1062 journal_path: &std::path::Path,
1063 snapshot_path: &std::path::Path,
1064 fence_state: &melin_transport_core::fence::FenceState,
1065 control: &ReplicaControlPlane,
1066 last_sequence: &mut u64,
1067 chain_hash: &mut [u8; 32],
1068) -> Result<ResyncDecision, Box<dyn std::error::Error + Send + Sync>>
1069where
1070 A: Application + Send + 'static,
1071 W: JournalWrite<A::Event> + Send + 'static,
1072 S: ControlFrameSource,
1073{
1074 if divergent {
1079 tracing::warn!(
1080 last_sequence = *last_sequence,
1081 "primary reports chain divergence — archiving local journal, resyncing from snapshot"
1082 );
1083 } else {
1084 tracing::info!("primary requires snapshot transfer — receiving snapshot");
1085 }
1086
1087 if let Some(p) = pipeline.take() {
1088 let _ = teardown_replica_pipeline::<A, W>(p);
1089 }
1090
1091 *exchange = None;
1098 *journal_writer = None;
1099
1100 let reason = if divergent {
1104 ArchiveReason::Divergent
1105 } else {
1106 ArchiveReason::Resync
1107 };
1108 archive_local_lineage(journal_path, snapshot_path, reason)?;
1109 *last_sequence = 0;
1111 *chain_hash = [0u8; 32];
1112 control
1117 .journal_tip
1118 .reset(melin_transport_core::WireSeq::new(0));
1119
1120 let (snap_exchange, snap_sequence, snap_chain_hash, seed_len) =
1121 match receive_resync_transfer::<A, S>(source, snapshot_path, journal_path, fence_state) {
1122 Ok(v) => v,
1123 Err(e) => {
1124 tracing::warn!(error = %e, "snapshot transfer failed — retrying");
1125 let _ = std::fs::remove_file(snapshot_path);
1129 return Ok(ResyncDecision::Retry);
1130 }
1131 };
1132 *exchange = Some(snap_exchange);
1133
1134 let writer = W::open_append(journal_path, snap_sequence, seed_len)?;
1141 let seeded_chain = writer.chain_hash().unwrap_or(snap_chain_hash);
1142 if snap_chain_hash != [0u8; 32] && seeded_chain != snap_chain_hash {
1143 return Err(format!(
1144 "segment seed chain at {snap_sequence} disagrees with the transferred snapshot's \
1145 hash — inconsistent primary"
1146 )
1147 .into());
1148 }
1149 let seeded_info = melin_journal::segment::read_header_info(journal_path)?;
1150 *journal_writer = Some(writer);
1151
1152 match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
1155 PrimaryMessage::StreamStart {
1156 start_sequence,
1157 segment_start_sequence,
1158 anchor_hash,
1159 epoch,
1160 ack_policy,
1161 } => {
1162 if segment_start_sequence != seeded_info.starting_sequence
1163 || anchor_hash != seeded_info.anchor_hash
1164 {
1165 return Err(format!(
1166 "post-snapshot StreamStart lineage (start {segment_start_sequence}) \
1167 disagrees with the transferred segment seed (start {}) — inconsistent \
1168 primary",
1169 seeded_info.starting_sequence
1170 )
1171 .into());
1172 }
1173 fence_state.observe_epoch(epoch);
1177 control
1178 .primary_ack_policy
1179 .store(ack_policy, Ordering::Release);
1180 tracing::info!(
1181 start_sequence,
1182 epoch,
1183 ack_policy,
1184 "streaming resumed after snapshot transfer"
1185 );
1186 control
1189 .journal_tip
1190 .reset(melin_transport_core::WireSeq::new(snap_sequence));
1191 Ok(ResyncDecision::Ready {
1192 segment_start_sequence,
1193 anchor_hash,
1194 resume_sequence: snap_sequence,
1195 })
1196 }
1197 other => Err(format!("expected StreamStart after snapshot, got {other:?}").into()),
1198 }
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203 use std::io::Write;
1204 use std::sync::Arc;
1205
1206 use super::auth::{authenticate_replica, authenticate_with_primary};
1207 use super::*;
1208 use counter_server::CounterEvent;
1214 type InputSlot = melin_transport_core::pipeline::InputSlot<CounterEvent>;
1215 use melin_transport_core::replication::protocol::{
1216 MAX_CONTROL_FRAME, MAX_DATA_FRAME, MSG_AUTH_OK, MSG_CHALLENGE_RESPONSE, MSG_SNAPSHOT_BEGIN,
1217 MSG_SNAPSHOT_CHUNK, MSG_SNAPSHOT_END, decode_auth_result, decode_challenge,
1218 decode_challenge_response, decode_primary_message, decode_replica_message, encode_ack,
1219 encode_auth_failed, encode_auth_ok, encode_chain_check, encode_challenge,
1220 encode_challenge_response, encode_handshake, encode_hash_mismatch, encode_heartbeat,
1221 encode_input_batch, encode_need_snapshot, encode_rotate, encode_segment_seed_begin,
1222 encode_snapshot_begin, encode_snapshot_chunk, encode_snapshot_end, encode_stream_start,
1223 read_frame, try_decode_input_batch,
1224 };
1225
1226 fn encode_input_batch_with_seq(end_sequence: u64, buf: &mut Vec<u8>) {
1230 let slot = InputSlot {
1231 connection_id: 0,
1232 key_hash: 0,
1233 request_seq: 0,
1234 sequence: end_sequence,
1235 timestamp_ns: 0,
1236 event: melin_journal::JournalEvent::Tick { now_ns: 0 },
1237 publish_ts: Default::default(),
1238 recv_ts: Default::default(),
1239 };
1240 encode_input_batch(&[slot], buf);
1241 }
1242
1243 #[test]
1244 fn handshake_encode_decode_round_trip() {
1245 let handshake = Handshake {
1246 last_sequence: 42,
1247 chain_hash: [0xAB; 32],
1248 epoch: 9,
1250 };
1251 let mut buf = Vec::new();
1252 encode_handshake(&handshake, &mut buf);
1253
1254 let payload = &buf[4..];
1256 let msg = decode_replica_message(payload).unwrap();
1257 match msg {
1258 ReplicaMessage::Handshake(h) => {
1259 assert_eq!(h.last_sequence, 42);
1260 assert_eq!(h.chain_hash, [0xAB; 32]);
1261 assert_eq!(h.epoch, 9);
1262 }
1263 _ => panic!("expected Handshake"),
1264 }
1265 }
1266
1267 #[test]
1268 fn ack_encode_decode_round_trip() {
1269 let ack = Ack {
1270 acked_sequence: 1000,
1271 in_memory_sequence: 1024,
1272 };
1273 let mut buf = Vec::new();
1274 encode_ack(&ack, &mut buf);
1275
1276 let payload = &buf[4..];
1277 let msg = decode_replica_message(payload).unwrap();
1278 match msg {
1279 ReplicaMessage::Ack(a) => {
1280 assert_eq!(a.acked_sequence, 1000);
1281 assert_eq!(a.in_memory_sequence, 1024);
1282 }
1283 _ => panic!("expected Ack"),
1284 }
1285 }
1286
1287 #[test]
1295 fn ack_wire_byte_pattern() {
1296 let ack = Ack {
1297 acked_sequence: 0xDEAD_BEEF_CAFE_F00D,
1298 in_memory_sequence: 0x1122_3344_5566_7788,
1299 };
1300 let mut buf = Vec::new();
1301 encode_ack(&ack, &mut buf);
1302 let expected: &[u8] = &[
1305 0x11, 0x00, 0x00, 0x00, 0x02, 0x0D, 0xF0, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, ];
1310 assert_eq!(buf.as_slice(), expected, "Ack wire layout drifted");
1311 }
1312
1313 #[test]
1314 fn stream_start_encode_decode_round_trip() {
1315 let mut buf = Vec::new();
1316 encode_stream_start(99, 42, [0xAA; 32], 5, 2, &mut buf);
1318
1319 let payload = &buf[4..];
1320 let msg = decode_primary_message(payload).unwrap();
1321 match msg {
1322 PrimaryMessage::StreamStart {
1323 start_sequence,
1324 segment_start_sequence,
1325 anchor_hash,
1326 epoch,
1327 ack_policy,
1328 } => {
1329 assert_eq!(start_sequence, 99);
1330 assert_eq!(segment_start_sequence, 42);
1331 assert_eq!(anchor_hash, [0xAA; 32]);
1332 assert_eq!(ack_policy, 2);
1333 assert_eq!(epoch, 5);
1334 }
1335 _ => panic!("expected StreamStart"),
1336 }
1337 }
1338
1339 #[test]
1340 fn heartbeat_encode_decode_round_trip() {
1341 let mut buf = Vec::new();
1342 encode_heartbeat(123, 2, &mut buf);
1343
1344 let payload = &buf[4..];
1345 let msg = decode_primary_message(payload).unwrap();
1346 match msg {
1347 PrimaryMessage::Heartbeat {
1348 sequence,
1349 ack_policy,
1350 } => {
1351 assert_eq!(sequence, 123);
1352 assert_eq!(ack_policy, 2);
1353 }
1354 _ => panic!("expected Heartbeat"),
1355 }
1356 }
1357
1358 #[test]
1359 fn need_snapshot_encode_decode_round_trip() {
1360 let mut buf = Vec::new();
1361 encode_need_snapshot(&mut buf);
1362
1363 let payload = &buf[4..];
1364 let msg = decode_primary_message(payload).unwrap();
1365 assert!(matches!(msg, PrimaryMessage::NeedSnapshot));
1366 }
1367
1368 #[test]
1369 fn hash_mismatch_encode_decode_round_trip() {
1370 let mut buf = Vec::new();
1371 encode_hash_mismatch(&mut buf);
1372
1373 let payload = &buf[4..];
1374 let msg = decode_primary_message(payload).unwrap();
1375 assert!(matches!(msg, PrimaryMessage::HashMismatch));
1376 }
1377
1378 #[test]
1379 fn rotate_encode_decode_round_trip() {
1380 let mut buf = Vec::new();
1381 encode_rotate(7_000_000, &[0xBB; 32], &mut buf);
1382
1383 let payload = &buf[4..];
1384 let msg = decode_primary_message(payload).unwrap();
1385 match msg {
1386 PrimaryMessage::Rotate {
1387 boundary_seq,
1388 tail_hash,
1389 } => {
1390 assert_eq!(boundary_seq, 7_000_000);
1391 assert_eq!(tail_hash, [0xBB; 32]);
1392 }
1393 _ => panic!("expected Rotate"),
1394 }
1395 }
1396
1397 #[test]
1398 fn chain_check_encode_decode_round_trip() {
1399 let mut buf = Vec::new();
1400 encode_chain_check(555, &[0xCC; 32], &mut buf);
1401
1402 let payload = &buf[4..];
1403 let msg = decode_primary_message(payload).unwrap();
1404 match msg {
1405 PrimaryMessage::ChainCheck {
1406 sequence,
1407 chain_hash,
1408 } => {
1409 assert_eq!(sequence, 555);
1410 assert_eq!(chain_hash, [0xCC; 32]);
1411 }
1412 _ => panic!("expected ChainCheck"),
1413 }
1414 }
1415
1416 #[test]
1420 fn rotate_wire_byte_pattern() {
1421 let mut hash = [0u8; 32];
1422 for (i, b) in hash.iter_mut().enumerate() {
1423 *b = i as u8;
1424 }
1425 let mut buf = Vec::new();
1426 encode_rotate(0xDEAD_BEEF_CAFE_F00D, &hash, &mut buf);
1427 let mut expected = vec![
1430 0x29, 0x00, 0x00, 0x00, 0x16, 0x0D, 0xF0, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE, ];
1434 expected.extend_from_slice(&hash);
1435 assert_eq!(
1436 buf.as_slice(),
1437 expected.as_slice(),
1438 "Rotate wire layout drifted"
1439 );
1440 }
1441
1442 #[test]
1443 fn segment_seed_begin_encode_decode_round_trip() {
1444 let mut buf = Vec::new();
1445 encode_segment_seed_begin(123_456, &mut buf);
1446
1447 let payload = &buf[4..];
1448 let msg = decode_primary_message(payload).unwrap();
1449 match msg {
1450 PrimaryMessage::SegmentSeedBegin { seed_len } => {
1451 assert_eq!(seed_len, 123_456);
1452 }
1453 _ => panic!("expected SegmentSeedBegin"),
1454 }
1455 }
1456
1457 #[test]
1458 fn unknown_replica_message_type_is_error() {
1459 let payload = [0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
1460 let result = decode_replica_message(&payload);
1461 assert!(result.is_err());
1462 }
1463
1464 #[test]
1465 fn unknown_primary_message_type_is_error() {
1466 let payload = [0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
1467 let result = decode_primary_message(&payload);
1468 assert!(result.is_err());
1469 }
1470
1471 #[test]
1472 fn read_frame_enforces_max_size() {
1473 let mut buf = Vec::new();
1475 buf.extend_from_slice(&1000u32.to_le_bytes());
1476 buf.extend_from_slice(&[0u8; 100]); let mut cursor = std::io::Cursor::new(buf);
1479 let result = read_frame(&mut cursor, 64);
1480 assert!(result.is_err());
1481 }
1482
1483 #[test]
1484 fn challenge_encode_decode_round_trip() {
1485 let nonce = [0x42; 32];
1486 let mut buf = Vec::new();
1487 encode_challenge(&nonce, &mut buf);
1488
1489 let payload = &buf[4..];
1490 let decoded = decode_challenge(payload).unwrap();
1491 assert_eq!(decoded, nonce);
1492 }
1493
1494 #[test]
1495 fn challenge_response_encode_decode_round_trip() {
1496 let sig = [0xAA; 64];
1497 let pubkey = [0xBB; 32];
1498 let mut buf = Vec::new();
1499 encode_challenge_response(&sig, &pubkey, &mut buf);
1500
1501 let payload = &buf[4..];
1502 let (decoded_sig, decoded_pubkey) = decode_challenge_response(payload).unwrap();
1503 assert_eq!(decoded_sig, sig);
1504 assert_eq!(decoded_pubkey, pubkey);
1505 }
1506
1507 #[test]
1508 fn auth_ok_encode_decode_round_trip() {
1509 let mut buf = Vec::new();
1510 encode_auth_ok(&mut buf);
1511
1512 let payload = &buf[4..];
1513 assert!(decode_auth_result(payload).unwrap());
1514 }
1515
1516 #[test]
1517 fn auth_failed_encode_decode_round_trip() {
1518 let mut buf = Vec::new();
1519 encode_auth_failed(&mut buf);
1520
1521 let payload = &buf[4..];
1522 assert!(!decode_auth_result(payload).unwrap());
1523 }
1524
1525 #[test]
1526 fn decode_challenge_rejects_wrong_tag() {
1527 let mut payload = [0u8; 33];
1528 payload[0] = MSG_AUTH_OK;
1529 assert!(decode_challenge(&payload).is_err());
1530 }
1531
1532 #[test]
1533 fn decode_challenge_response_rejects_short_payload() {
1534 let payload = [MSG_CHALLENGE_RESPONSE; 10]; assert!(decode_challenge_response(&payload).is_err());
1536 }
1537
1538 #[test]
1539 fn auth_round_trip_valid_key() {
1540 use ed25519_dalek::SigningKey;
1541 use std::os::unix::net::UnixStream;
1542
1543 let repl_key = SigningKey::from_bytes(&[0xFC; 32]);
1544 let pub_b64 = base64::Engine::encode(
1545 &base64::engine::general_purpose::STANDARD,
1546 repl_key.verifying_key().to_bytes(),
1547 );
1548 let keys_content = format!("replication {pub_b64} test-replica\n");
1549 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1550
1551 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1552 primary_stream
1553 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1554 .unwrap();
1555 replica_stream
1556 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1557 .unwrap();
1558
1559 let repl_key_clone = SigningKey::from_bytes(&[0xFC; 32]);
1560 let replica_handle = std::thread::spawn(move || {
1561 let mut conn = replica_stream;
1562 authenticate_with_primary(&mut conn, &repl_key_clone)
1563 });
1564
1565 let mut conn = primary_stream;
1566 authenticate_replica(&mut conn, &authorized_keys).unwrap();
1567
1568 replica_handle.join().unwrap().unwrap();
1569 }
1570
1571 #[test]
1572 fn auth_rejects_unknown_key() {
1573 use ed25519_dalek::SigningKey;
1574 use std::os::unix::net::UnixStream;
1575
1576 let authorized_key = SigningKey::from_bytes(&[0xAA; 32]);
1578 let rogue_key = SigningKey::from_bytes(&[0xBB; 32]);
1579 let pub_b64 = base64::Engine::encode(
1580 &base64::engine::general_purpose::STANDARD,
1581 authorized_key.verifying_key().to_bytes(),
1582 );
1583 let keys_content = format!("replication {pub_b64} authorized-replica\n");
1584 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1585
1586 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1587 primary_stream
1588 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1589 .unwrap();
1590 replica_stream
1591 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1592 .unwrap();
1593
1594 let replica_handle = std::thread::spawn(move || {
1595 let mut conn = replica_stream;
1596 authenticate_with_primary(&mut conn, &rogue_key)
1597 });
1598
1599 let mut conn = primary_stream;
1600 let result = authenticate_replica(&mut conn, &authorized_keys);
1601 assert!(result.is_err());
1602 assert!(result.unwrap_err().to_string().contains("unknown"));
1603
1604 let replica_result = replica_handle.join().unwrap();
1606 assert!(replica_result.is_err());
1607 }
1608
1609 #[test]
1610 fn auth_rejects_wrong_permission() {
1611 use ed25519_dalek::SigningKey;
1612 use std::os::unix::net::UnixStream;
1613
1614 let key = SigningKey::from_bytes(&[0xCC; 32]);
1616 let pub_b64 = base64::Engine::encode(
1617 &base64::engine::general_purpose::STANDARD,
1618 key.verifying_key().to_bytes(),
1619 );
1620 let keys_content = format!("trader {pub_b64} wrong-role\n");
1621 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1622
1623 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1624 primary_stream
1625 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1626 .unwrap();
1627 replica_stream
1628 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1629 .unwrap();
1630
1631 let replica_handle = std::thread::spawn(move || {
1632 let mut conn = replica_stream;
1633 authenticate_with_primary(&mut conn, &key)
1634 });
1635
1636 let mut conn = primary_stream;
1637 let result = authenticate_replica(&mut conn, &authorized_keys);
1638 assert!(result.is_err());
1639 assert!(result.unwrap_err().to_string().contains("Replication"));
1640
1641 let replica_result = replica_handle.join().unwrap();
1642 assert!(replica_result.is_err());
1643 }
1644
1645 #[test]
1648 fn auth_rejects_invalid_signature() {
1649 use ed25519_dalek::SigningKey;
1650 use std::os::unix::net::UnixStream;
1651
1652 let correct_key = SigningKey::from_bytes(&[0xDD; 32]);
1654 let pub_b64 = base64::Engine::encode(
1655 &base64::engine::general_purpose::STANDARD,
1656 correct_key.verifying_key().to_bytes(),
1657 );
1658 let keys_content = format!("replication {pub_b64} test-replica\n");
1659 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1660
1661 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1662 primary_stream
1663 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1664 .unwrap();
1665 replica_stream
1666 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1667 .unwrap();
1668
1669 let replica_handle = std::thread::spawn(move || {
1672 use melin_transport_core::replication::protocol::*;
1673
1674 let mut reader = replica_stream.try_clone().unwrap();
1675 let mut writer = replica_stream;
1676
1677 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1679 let nonce = decode_challenge(&frame).unwrap();
1680
1681 let wrong_key = SigningKey::from_bytes(&[0xEE; 32]);
1683 let bad_signature = ed25519_dalek::Signer::sign(&wrong_key, &nonce);
1684 let correct_pubkey = correct_key.verifying_key();
1685
1686 let mut buf = Vec::with_capacity(128);
1687 encode_challenge_response(
1688 &bad_signature.to_bytes(),
1689 correct_pubkey.as_bytes(),
1690 &mut buf,
1691 );
1692 writer.write_all(&buf).unwrap();
1693 writer.flush().unwrap();
1694
1695 let result_frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1697 let ok = decode_auth_result(&result_frame).unwrap();
1698 assert!(!ok, "should receive auth failure");
1699 });
1700
1701 let mut conn = primary_stream;
1702 let result = authenticate_replica(&mut conn, &authorized_keys);
1703 assert!(result.is_err());
1704 assert!(
1705 result
1706 .unwrap_err()
1707 .to_string()
1708 .contains("signature verification failed"),
1709 "should fail on signature verification"
1710 );
1711
1712 replica_handle.join().unwrap();
1713 }
1714
1715 #[test]
1716 fn sender_receiver_end_to_end() {
1717 use std::os::unix::net::UnixStream;
1718
1719 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1721
1722 let replica_handle = std::thread::spawn(move || {
1724 let mut reader = replica_stream.try_clone().unwrap();
1725 let mut writer = replica_stream;
1726
1727 let mut buf = Vec::new();
1729 let handshake = Handshake {
1730 last_sequence: 0,
1731 chain_hash: [0u8; 32],
1732 epoch: 0,
1733 };
1734 encode_handshake(&handshake, &mut buf);
1735 writer.write_all(&buf).unwrap();
1736 writer.flush().unwrap();
1737 buf.clear();
1738
1739 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1741 let msg = decode_primary_message(&frame).unwrap();
1742 assert!(matches!(msg, PrimaryMessage::StreamStart { .. }));
1743
1744 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1746 let slots: Vec<InputSlot> = try_decode_input_batch(&frame).expect("decode InputBatch");
1747 let end_seq = slots
1748 .last()
1749 .map(|s| s.sequence)
1750 .expect("InputBatch carried at least one slot");
1751
1752 let ack = Ack {
1754 acked_sequence: end_seq,
1755 in_memory_sequence: end_seq,
1756 };
1757 encode_ack(&ack, &mut buf);
1758 writer.write_all(&buf).unwrap();
1759 writer.flush().unwrap();
1760
1761 end_seq
1762 });
1763
1764 let mut p_reader = primary_stream.try_clone().unwrap();
1766 let mut p_writer = primary_stream;
1767
1768 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1770 let handshake = match decode_replica_message(&frame).unwrap() {
1771 ReplicaMessage::Handshake(h) => h,
1772 _ => panic!("expected Handshake"),
1773 };
1774 assert_eq!(handshake.last_sequence, 0);
1775
1776 let mut buf = Vec::new();
1778 encode_stream_start(0, 1, [0u8; 32], 0, 1, &mut buf); p_writer.write_all(&buf).unwrap();
1780 p_writer.flush().unwrap();
1781 buf.clear();
1782
1783 encode_input_batch_with_seq(42, &mut buf);
1785 p_writer.write_all(&buf).unwrap();
1786 p_writer.flush().unwrap();
1787 buf.clear();
1788
1789 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1791 let ack = match decode_replica_message(&frame).unwrap() {
1792 ReplicaMessage::Ack(a) => a,
1793 _ => panic!("expected Ack"),
1794 };
1795 assert_eq!(ack.acked_sequence, 42);
1796
1797 let end_seq = replica_handle.join().unwrap();
1799 assert_eq!(end_seq, 42);
1800 }
1801
1802 #[test]
1803 fn multiple_data_batches_acked_in_order() {
1804 use std::os::unix::net::UnixStream;
1807
1808 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1809
1810 let replica_handle = std::thread::spawn(move || {
1811 let mut reader = replica_stream.try_clone().unwrap();
1812 let mut writer = replica_stream;
1813 let mut buf = Vec::new();
1814
1815 encode_handshake(
1817 &Handshake {
1818 last_sequence: 0,
1819 chain_hash: [0u8; 32],
1820 epoch: 0,
1821 },
1822 &mut buf,
1823 );
1824 writer.write_all(&buf).unwrap();
1825 writer.flush().unwrap();
1826 buf.clear();
1827
1828 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1830 assert!(matches!(
1831 decode_primary_message(&frame).unwrap(),
1832 PrimaryMessage::StreamStart { .. }
1833 ));
1834
1835 let mut acked_seqs = Vec::new();
1837 for _ in 0..3 {
1838 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1839 let slots: Vec<InputSlot> =
1840 try_decode_input_batch(&frame).expect("decode InputBatch");
1841 let end_seq = slots
1842 .last()
1843 .map(|s| s.sequence)
1844 .expect("InputBatch carried at least one slot");
1845 acked_seqs.push(end_seq);
1846
1847 encode_ack(
1848 &Ack {
1849 acked_sequence: end_seq,
1850 in_memory_sequence: end_seq,
1851 },
1852 &mut buf,
1853 );
1854 writer.write_all(&buf).unwrap();
1855 writer.flush().unwrap();
1856 buf.clear();
1857 }
1858
1859 acked_seqs
1860 });
1861
1862 let mut p_reader = primary_stream.try_clone().unwrap();
1864 let mut p_writer = primary_stream;
1865 let mut buf = Vec::new();
1866
1867 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1869 assert!(matches!(
1870 decode_replica_message(&frame).unwrap(),
1871 ReplicaMessage::Handshake(_)
1872 ));
1873
1874 encode_stream_start(0, 1, [0u8; 32], 0, 1, &mut buf);
1876 p_writer.write_all(&buf).unwrap();
1877 p_writer.flush().unwrap();
1878 buf.clear();
1879
1880 for seq in [10u64, 20, 30] {
1882 encode_input_batch_with_seq(seq, &mut buf);
1883 p_writer.write_all(&buf).unwrap();
1884 p_writer.flush().unwrap();
1885 buf.clear();
1886 }
1887
1888 for expected_seq in [10u64, 20, 30] {
1890 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1891 let ack = match decode_replica_message(&frame).unwrap() {
1892 ReplicaMessage::Ack(a) => a,
1893 other => panic!("expected Ack, got {other:?}"),
1894 };
1895 assert_eq!(ack.acked_sequence, expected_seq);
1896 }
1897
1898 let acked = replica_handle.join().unwrap();
1899 assert_eq!(acked, vec![10, 20, 30]);
1900 }
1901
1902 #[test]
1903 fn heartbeat_encode_contains_sequence() {
1904 let mut buf = Vec::new();
1907 encode_heartbeat(999, 1, &mut buf);
1908
1909 let payload = &buf[4..];
1910 match decode_primary_message(payload).unwrap() {
1911 PrimaryMessage::Heartbeat { sequence, .. } => {
1912 assert_eq!(sequence, 999);
1913 }
1914 other => panic!("expected Heartbeat, got {other:?}"),
1915 }
1916 }
1917
1918 #[test]
1919 fn replica_mid_stream_handshake_with_nonzero_sequence() {
1920 use std::os::unix::net::UnixStream;
1925
1926 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1927
1928 let replica_handle = std::thread::spawn(move || {
1929 let mut reader = replica_stream.try_clone().unwrap();
1930 let mut writer = replica_stream;
1931 let mut buf = Vec::new();
1932
1933 encode_handshake(
1935 &Handshake {
1936 last_sequence: 100,
1937 chain_hash: [0xBB; 32],
1938 epoch: 0,
1939 },
1940 &mut buf,
1941 );
1942 writer.write_all(&buf).unwrap();
1943 writer.flush().unwrap();
1944 buf.clear();
1945
1946 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1948 match decode_primary_message(&frame).unwrap() {
1949 PrimaryMessage::StreamStart { start_sequence, .. } => {
1950 assert_eq!(
1951 start_sequence, 100,
1952 "StreamStart should echo replica's last_sequence"
1953 );
1954 }
1955 other => panic!("expected StreamStart, got {other:?}"),
1956 }
1957
1958 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1960 let slots: Vec<InputSlot> = try_decode_input_batch(&frame).expect("decode InputBatch");
1961 let end_sequence = slots
1962 .last()
1963 .map(|s| s.sequence)
1964 .expect("InputBatch carried at least one slot");
1965 assert!(
1966 end_sequence > 100,
1967 "InputBatch should be after replica's last_sequence"
1968 );
1969 });
1970
1971 let mut p_reader = primary_stream.try_clone().unwrap();
1973 let mut p_writer = primary_stream;
1974 let mut buf = Vec::new();
1975
1976 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1978 let handshake = match decode_replica_message(&frame).unwrap() {
1979 ReplicaMessage::Handshake(h) => h,
1980 _ => panic!("expected Handshake"),
1981 };
1982 assert_eq!(handshake.last_sequence, 100);
1983 assert_eq!(handshake.chain_hash, [0xBB; 32]);
1984
1985 encode_stream_start(handshake.last_sequence, 1, [0u8; 32], 0, 1, &mut buf);
1987 p_writer.write_all(&buf).unwrap();
1988 p_writer.flush().unwrap();
1989 buf.clear();
1990
1991 encode_input_batch_with_seq(150, &mut buf);
1993 p_writer.write_all(&buf).unwrap();
1994 p_writer.flush().unwrap();
1995
1996 replica_handle.join().unwrap();
1997 }
1998
1999 #[test]
2014 fn receiver_refuses_stream_start_from_stale_primary() {
2015 use std::os::unix::net::UnixStream;
2016
2017 use melin_transport_core::fence::FenceState;
2018
2019 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2020
2021 const STALE_EPOCH: u64 = 3;
2025 let primary_handle = std::thread::spawn(move || {
2026 let mut reader = primary_stream.try_clone().unwrap();
2027 let mut writer = primary_stream;
2028
2029 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2030 let handshake = match decode_replica_message(&frame).unwrap() {
2031 ReplicaMessage::Handshake(h) => h,
2032 _ => panic!("expected Handshake"),
2033 };
2034 assert_eq!(handshake.epoch, 5, "replica advertises its real epoch");
2037
2038 let mut buf = Vec::new();
2039 encode_stream_start(
2040 handshake.last_sequence,
2041 1,
2042 [0u8; 32],
2043 STALE_EPOCH,
2044 1,
2045 &mut buf,
2046 );
2047 writer.write_all(&buf).unwrap();
2048 writer.flush().unwrap();
2049 });
2050
2051 let fence_state = Arc::new(FenceState::new(5));
2054 let mut r_reader = replica_stream.try_clone().unwrap();
2055 let mut r_writer = replica_stream;
2056
2057 let mut buf = Vec::new();
2058 encode_handshake(
2059 &Handshake {
2060 last_sequence: 0,
2061 chain_hash: [0u8; 32],
2062 epoch: fence_state.epoch(),
2063 },
2064 &mut buf,
2065 );
2066 r_writer.write_all(&buf).unwrap();
2067 r_writer.flush().unwrap();
2068
2069 let frame = read_frame(&mut r_reader, MAX_CONTROL_FRAME).unwrap();
2070 let stream_epoch = match decode_primary_message(&frame).unwrap() {
2071 PrimaryMessage::StreamStart { epoch, .. } => epoch,
2072 other => panic!("expected StreamStart, got {other:?}"),
2073 };
2074 assert_eq!(stream_epoch, STALE_EPOCH, "epoch must survive the wire");
2075
2076 assert!(
2079 fence_state.refuses_primary(stream_epoch),
2080 "replica at epoch 5 must refuse a StreamStart from epoch {STALE_EPOCH}"
2081 );
2082 assert_eq!(fence_state.epoch(), 5, "refusal must not lower our epoch");
2083
2084 assert!(!fence_state.refuses_primary(5), "same tenure is followed");
2087 assert!(!fence_state.refuses_primary(6), "newer primary is followed");
2088
2089 primary_handle.join().unwrap();
2090 }
2091
2092 #[test]
2093 fn snapshot_begin_encode_decode_round_trip() {
2094 let mut buf = Vec::new();
2095 encode_snapshot_begin(1_000_000, 42, &[0xAB; 32], &mut buf);
2096
2097 let payload = &buf[4..];
2098 let msg = decode_primary_message(payload).unwrap();
2099 match msg {
2100 PrimaryMessage::SnapshotBegin {
2101 snapshot_len,
2102 snap_sequence,
2103 snap_chain_hash,
2104 } => {
2105 assert_eq!(snapshot_len, 1_000_000);
2106 assert_eq!(snap_sequence, 42);
2107 assert_eq!(snap_chain_hash, [0xAB; 32]);
2108 }
2109 _ => panic!("expected SnapshotBegin"),
2110 }
2111 }
2112
2113 #[test]
2114 fn snapshot_chunk_encode_decode_round_trip() {
2115 let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
2116 let mut buf = Vec::new();
2117 encode_snapshot_chunk(&data, &mut buf);
2118
2119 let payload = &buf[4..];
2120 let msg = decode_primary_message(payload).unwrap();
2121 match msg {
2122 PrimaryMessage::SnapshotChunk(chunk) => {
2123 assert_eq!(chunk, data);
2124 }
2125 _ => panic!("expected SnapshotChunk"),
2126 }
2127 }
2128
2129 #[test]
2130 fn snapshot_end_encode_decode_round_trip() {
2131 let mut buf = Vec::new();
2132 encode_snapshot_end(0xDEADBEEF, &mut buf);
2133
2134 let payload = &buf[4..];
2135 let msg = decode_primary_message(payload).unwrap();
2136 match msg {
2137 PrimaryMessage::SnapshotEnd { crc32c } => {
2138 assert_eq!(crc32c, 0xDEADBEEF);
2139 }
2140 _ => panic!("expected SnapshotEnd"),
2141 }
2142 }
2143
2144 #[test]
2148 fn snapshot_receiver_detects_length_mismatch() {
2149 use std::os::unix::net::UnixStream;
2150
2151 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2152
2153 let receiver = std::thread::spawn(move || -> String {
2155 let mut reader = replica_stream.try_clone().unwrap();
2156
2157 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2159 assert!(matches!(
2160 decode_primary_message(&frame).unwrap(),
2161 PrimaryMessage::NeedSnapshot,
2162 ));
2163
2164 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2166 let (snap_len, _snap_sequence, _snap_chain_hash) =
2167 match decode_primary_message(&frame).unwrap() {
2168 PrimaryMessage::SnapshotBegin {
2169 snapshot_len,
2170 snap_sequence,
2171 snap_chain_hash,
2172 } => (snapshot_len, snap_sequence, snap_chain_hash),
2173 other => panic!("expected SnapshotBegin, got {other:?}"),
2174 };
2175
2176 let mut received: u64 = 0;
2178 loop {
2179 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
2180 match decode_primary_message(&frame).unwrap() {
2181 PrimaryMessage::SnapshotChunk(data) => {
2182 received += data.len() as u64;
2183 }
2184 PrimaryMessage::SnapshotEnd { .. } => {
2185 if received != snap_len {
2186 return format!(
2187 "snapshot length mismatch: expected {snap_len} bytes, got {received}"
2188 );
2189 }
2190 return String::new(); }
2192 other => panic!("unexpected message: {other:?}"),
2193 }
2194 }
2195 });
2196
2197 let mut writer = primary_stream;
2199 let mut buf = Vec::new();
2200
2201 let actual_data = vec![0xAA; 100];
2202 let wrong_len = 999u64; encode_need_snapshot(&mut buf);
2205 std::io::Write::write_all(&mut writer, &buf).unwrap();
2206 buf.clear();
2207
2208 encode_snapshot_begin(wrong_len, 42, &[0xBB; 32], &mut buf);
2209 std::io::Write::write_all(&mut writer, &buf).unwrap();
2210 buf.clear();
2211
2212 encode_snapshot_chunk(&actual_data, &mut buf);
2213 std::io::Write::write_all(&mut writer, &buf).unwrap();
2214 buf.clear();
2215
2216 let crc = crc32c::crc32c(&actual_data);
2217 encode_snapshot_end(crc, &mut buf);
2218 std::io::Write::write_all(&mut writer, &buf).unwrap();
2219 std::io::Write::flush(&mut writer).unwrap();
2220
2221 let error_msg = receiver.join().unwrap();
2222 assert!(
2223 error_msg.contains("length mismatch"),
2224 "expected length mismatch error, got: {error_msg:?}"
2225 );
2226 }
2227
2228 #[test]
2232 fn snapshot_receiver_detects_crc_mismatch() {
2233 use std::os::unix::net::UnixStream;
2234
2235 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2236
2237 let receiver = std::thread::spawn(move || -> String {
2238 let mut reader = replica_stream.try_clone().unwrap();
2239
2240 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2242 assert!(matches!(
2243 decode_primary_message(&frame).unwrap(),
2244 PrimaryMessage::NeedSnapshot,
2245 ));
2246
2247 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2249 let snap_len = match decode_primary_message(&frame).unwrap() {
2250 PrimaryMessage::SnapshotBegin { snapshot_len, .. } => snapshot_len,
2251 other => panic!("expected SnapshotBegin, got {other:?}"),
2252 };
2253
2254 let mut received_data = Vec::new();
2256 let mut received: u64 = 0;
2257 loop {
2258 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
2259 match decode_primary_message(&frame).unwrap() {
2260 PrimaryMessage::SnapshotChunk(data) => {
2261 received += data.len() as u64;
2262 received_data.extend_from_slice(&data);
2263 }
2264 PrimaryMessage::SnapshotEnd {
2265 crc32c: expected_crc,
2266 } => {
2267 if received != snap_len {
2268 return format!("length mismatch: {snap_len} vs {received}");
2269 }
2270 let actual_crc = crc32c::crc32c(&received_data);
2271 if actual_crc != expected_crc {
2272 return format!(
2273 "CRC mismatch: expected {expected_crc:#x}, got {actual_crc:#x}"
2274 );
2275 }
2276 return String::new();
2277 }
2278 other => panic!("unexpected message: {other:?}"),
2279 }
2280 }
2281 });
2282
2283 let mut writer = primary_stream;
2285 let mut buf = Vec::new();
2286
2287 let data = vec![0xAA; 100];
2288
2289 encode_need_snapshot(&mut buf);
2290 std::io::Write::write_all(&mut writer, &buf).unwrap();
2291 buf.clear();
2292
2293 encode_snapshot_begin(data.len() as u64, 42, &[0xBB; 32], &mut buf);
2294 std::io::Write::write_all(&mut writer, &buf).unwrap();
2295 buf.clear();
2296
2297 encode_snapshot_chunk(&data, &mut buf);
2298 std::io::Write::write_all(&mut writer, &buf).unwrap();
2299 buf.clear();
2300
2301 let wrong_crc = !crc32c::crc32c(&data);
2303 encode_snapshot_end(wrong_crc, &mut buf);
2304 std::io::Write::write_all(&mut writer, &buf).unwrap();
2305 std::io::Write::flush(&mut writer).unwrap();
2306
2307 let error_msg = receiver.join().unwrap();
2308 assert!(
2309 error_msg.contains("CRC mismatch"),
2310 "expected CRC mismatch error, got: {error_msg:?}"
2311 );
2312 }
2313
2314 #[test]
2317 fn snapshot_receiver_detects_chain_hash_mismatch() {
2318 use std::os::unix::net::UnixStream;
2319
2320 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2321
2322 let receiver = std::thread::spawn(move || -> String {
2323 let mut reader = replica_stream.try_clone().unwrap();
2324
2325 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2326 assert!(matches!(
2327 decode_primary_message(&frame).unwrap(),
2328 PrimaryMessage::NeedSnapshot,
2329 ));
2330
2331 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2332 let (snap_len, _snap_sequence, snap_chain_hash) =
2333 match decode_primary_message(&frame).unwrap() {
2334 PrimaryMessage::SnapshotBegin {
2335 snapshot_len,
2336 snap_sequence,
2337 snap_chain_hash,
2338 } => (snapshot_len, snap_sequence, snap_chain_hash),
2339 other => panic!("expected SnapshotBegin, got {other:?}"),
2340 };
2341
2342 let mut received_data = Vec::new();
2344 let mut received: u64 = 0;
2345 loop {
2346 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
2347 match decode_primary_message(&frame).unwrap() {
2348 PrimaryMessage::SnapshotChunk(data) => {
2349 received += data.len() as u64;
2350 received_data.extend_from_slice(&data);
2351 }
2352 PrimaryMessage::SnapshotEnd {
2353 crc32c: expected_crc,
2354 } => {
2355 assert_eq!(received, snap_len, "length should match");
2356 let actual_crc = crc32c::crc32c(&received_data);
2357 assert_eq!(actual_crc, expected_crc, "CRC should match");
2358 break;
2359 }
2360 other => panic!("unexpected message: {other:?}"),
2361 }
2362 }
2363
2364 let loaded_hash = [0xFF; 32]; if loaded_hash != snap_chain_hash {
2368 return format!(
2369 "snapshot chain hash mismatch: primary sent {snap_chain_hash:02x?}, \
2370 loaded snapshot has {loaded_hash:02x?}"
2371 );
2372 }
2373 String::new()
2374 });
2375
2376 let mut writer = primary_stream;
2379 let mut buf = Vec::new();
2380
2381 let data = vec![0xAA; 64];
2382 let advertised_hash = [0xBB; 32];
2384
2385 encode_need_snapshot(&mut buf);
2386 std::io::Write::write_all(&mut writer, &buf).unwrap();
2387 buf.clear();
2388
2389 encode_snapshot_begin(data.len() as u64, 10, &advertised_hash, &mut buf);
2390 std::io::Write::write_all(&mut writer, &buf).unwrap();
2391 buf.clear();
2392
2393 encode_snapshot_chunk(&data, &mut buf);
2394 std::io::Write::write_all(&mut writer, &buf).unwrap();
2395 buf.clear();
2396
2397 let crc = crc32c::crc32c(&data);
2398 encode_snapshot_end(crc, &mut buf);
2399 std::io::Write::write_all(&mut writer, &buf).unwrap();
2400 std::io::Write::flush(&mut writer).unwrap();
2401
2402 let error_msg = receiver.join().unwrap();
2403 assert!(
2404 error_msg.contains("chain hash mismatch"),
2405 "expected chain hash mismatch error, got: {error_msg:?}"
2406 );
2407 }
2408
2409 #[test]
2412 fn primary_rejects_snapshot_with_invalid_magic() {
2413 let tmp = tempfile::tempdir().unwrap();
2414 let snap_path = tmp.path().join("test.snapshot");
2415
2416 let mut bad_snap = vec![0u8; 64];
2418 bad_snap[0..4].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes()); std::fs::write(&snap_path, &bad_snap).unwrap();
2421
2422 let snap_data = std::fs::read(&snap_path).unwrap();
2424 assert!(
2425 snap_data.len() >= 48,
2426 "file should be big enough for header"
2427 );
2428
2429 let magic = u32::from_le_bytes(snap_data[0..4].try_into().unwrap());
2430 assert_ne!(magic, 0x534E_4150);
2431 assert_eq!(magic, 0xDEAD_BEEF);
2432 }
2433
2434 #[test]
2437 fn primary_rejects_snapshot_too_small_for_header() {
2438 let tmp = tempfile::tempdir().unwrap();
2439 let snap_path = tmp.path().join("test.snapshot");
2440
2441 std::fs::write(&snap_path, [0u8; 20]).unwrap();
2443
2444 let snap_data = std::fs::read(&snap_path).unwrap();
2445 assert!(
2446 snap_data.len() < 48,
2447 "file must be too small for header validation"
2448 );
2449 }
2450
2451 #[test]
2452 fn decode_snapshot_begin_too_short() {
2453 let payload = [MSG_SNAPSHOT_BEGIN, 0x01, 0x02, 0x03];
2456 let err = decode_primary_message(&payload).unwrap_err();
2457 assert!(
2458 err.to_string().contains("SnapshotBegin too short"),
2459 "expected 'SnapshotBegin too short', got: {err}"
2460 );
2461 }
2462
2463 #[test]
2464 fn decode_snapshot_end_too_short() {
2465 let payload = [MSG_SNAPSHOT_END];
2467 let err = decode_primary_message(&payload).unwrap_err();
2468 assert!(
2469 err.to_string().contains("SnapshotEnd too short"),
2470 "expected 'SnapshotEnd too short', got: {err}"
2471 );
2472 }
2473
2474 #[test]
2475 fn decode_snapshot_chunk_empty_data() {
2476 let payload = [MSG_SNAPSHOT_CHUNK];
2478 let msg = decode_primary_message(&payload).unwrap();
2479 match msg {
2480 PrimaryMessage::SnapshotChunk(data) => {
2481 assert!(data.is_empty());
2482 }
2483 _ => panic!("expected SnapshotChunk"),
2484 }
2485 }
2486
2487 fn make_journal_cursor(val: u64) -> melin_pipeline::padding::Sequence {
2490 melin_pipeline::padding::Sequence::new(AtomicU64::new(val))
2491 }
2492
2493 #[test]
2494 fn pending_ack_queue_push_and_pop_ready() {
2495 let mut q = PendingAckQueue::new(8);
2496 assert!(q.is_empty());
2497 assert!(!q.is_full());
2498
2499 q.push(10, 100);
2500 q.push(20, 200);
2501 assert!(!q.is_empty());
2502
2503 let cursor = make_journal_cursor(5);
2505 assert!(q.pop_ready(&cursor).is_none());
2506
2507 cursor.get().store(15, Ordering::Relaxed);
2509 assert_eq!(q.pop_ready(&cursor), Some(100));
2510 assert!(!q.is_empty());
2512
2513 cursor.get().store(25, Ordering::Relaxed);
2515 assert_eq!(q.pop_ready(&cursor), Some(200));
2516 assert!(q.is_empty());
2517 }
2518
2519 #[test]
2520 fn pending_ack_queue_pop_ready_returns_highest_sequence() {
2521 let mut q = PendingAckQueue::new(8);
2525 q.push(10, 100);
2526 q.push(20, 200);
2527 q.push(30, 300);
2528
2529 let cursor = make_journal_cursor(30);
2530 assert_eq!(q.pop_ready(&cursor), Some(300));
2531 assert!(q.is_empty());
2532 }
2533
2534 #[test]
2535 fn pending_ack_queue_capacity_and_full() {
2536 let mut q = PendingAckQueue::new(8);
2537 for i in 0..8 {
2538 assert!(!q.is_full());
2539 q.push(i as u64 + 1, (i + 1) as u64 * 100);
2540 }
2541 assert!(q.is_full());
2542 }
2543
2544 #[test]
2549 fn pending_ack_queue_merges_into_the_newest_entry_when_full() {
2550 let mut q = PendingAckQueue::new(4);
2551 for i in 1..=4u64 {
2552 q.push(i * 10, i * 100);
2553 }
2554 assert!(q.is_full());
2555 assert_eq!(q.merged(), 0);
2556
2557 q.push(50, 500);
2559 assert_eq!(q.merged(), 1);
2560 assert_eq!(q.len(), 4, "merge must not grow the queue");
2561
2562 let cursor = make_journal_cursor(40);
2565 assert_eq!(q.pop_ready(&cursor), Some(300));
2566 assert!(!q.is_empty(), "merged entry is not durable yet");
2567
2568 cursor.get().store(50, Ordering::Relaxed);
2571 assert_eq!(q.pop_ready(&cursor), Some(500));
2572 assert!(q.is_empty());
2573 }
2574
2575 #[test]
2578 fn pending_ack_queue_merges_repeatedly_without_growing() {
2579 let mut q = PendingAckQueue::new(2);
2580 for i in 1..=100u64 {
2581 q.push(i, i * 10);
2582 }
2583 assert_eq!(q.len(), 2);
2584 assert_eq!(q.merged(), 98);
2585
2586 let cursor = make_journal_cursor(99);
2588 assert_eq!(q.pop_ready(&cursor), Some(10), "only the first entry");
2589 cursor.get().store(100, Ordering::Relaxed);
2591 assert_eq!(q.pop_ready(&cursor), Some(1000));
2592 }
2593
2594 #[test]
2595 fn pending_ack_queue_pop_oldest_blocking() {
2596 let mut q = PendingAckQueue::new(8);
2597 q.push(10, 100);
2598 q.push(20, 200);
2599
2600 let cursor = make_journal_cursor(25);
2603 let seq = q.pop_oldest_blocking(
2604 &cursor,
2605 melin_pipeline::wait::WaitStrategy::BusySpin,
2606 &AtomicBool::new(false),
2607 );
2608 assert_eq!(seq, Some(200));
2610 assert!(q.is_empty());
2611 }
2612
2613 #[test]
2616 fn pending_ack_queue_blocking_wait_aborts_on_journal_failure() {
2617 let mut q = PendingAckQueue::new(8);
2618 q.push(10, 100);
2619
2620 let cursor = make_journal_cursor(5);
2622 let abort = AtomicBool::new(true);
2623 assert_eq!(
2624 q.pop_oldest_blocking(
2625 &cursor,
2626 melin_pipeline::wait::WaitStrategy::BusySpin,
2627 &abort
2628 ),
2629 None
2630 );
2631 assert!(!q.is_empty(), "aborted wait must not pop the entry");
2632 assert_eq!(
2633 q.pop_all_blocking(
2634 &cursor,
2635 melin_pipeline::wait::WaitStrategy::BusySpin,
2636 &abort
2637 ),
2638 None
2639 );
2640 }
2641
2642 #[test]
2651 fn shutdown_pipeline_surfaces_journal_error_kind() {
2652 let flag = AtomicBool::new(false);
2653 let matching_joined = Arc::new(AtomicBool::new(false));
2654 let mj = Arc::clone(&matching_joined);
2655
2656 let journal = std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> {
2657 Err(melin_journal::JournalError::ReplicaChainDivergence {
2658 sequence: 42,
2659 expected: [1u8; 32],
2660 actual: [2u8; 32],
2661 })
2662 });
2663 let matching = std::thread::spawn(move || {
2664 mj.store(true, Ordering::Release);
2665 7u64
2666 });
2667 let drain = std::thread::spawn(|| {});
2668
2669 let outcome = shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None);
2670 assert!(flag.load(Ordering::Acquire), "shutdown flag must be set");
2671 assert!(
2672 matching_joined.load(Ordering::Acquire),
2673 "matching thread must be joined even when the journal failed"
2674 );
2675 match outcome {
2676 TeardownOutcome::JournalFailed(
2677 melin_journal::JournalError::ReplicaChainDivergence { sequence, .. },
2678 ) => assert_eq!(sequence, 42),
2679 _ => panic!("expected JournalFailed(ReplicaChainDivergence)"),
2680 }
2681 }
2682
2683 #[test]
2684 fn shutdown_pipeline_clean_returns_both_states() {
2685 let flag = AtomicBool::new(false);
2686 let journal =
2687 std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> { Ok(11u32) });
2688 let matching = std::thread::spawn(|| 7u64);
2689 let drain = std::thread::spawn(|| {});
2690
2691 match shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None) {
2692 TeardownOutcome::Clean(app, writer) => {
2693 assert_eq!(app, 7);
2694 assert_eq!(writer, 11);
2695 }
2696 _ => panic!("expected Clean"),
2697 }
2698 }
2699
2700 #[test]
2701 fn shutdown_pipeline_panicked_stage_reports_panicked() {
2702 let flag = AtomicBool::new(false);
2703 let journal =
2704 std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> { Ok(11u32) });
2705 let matching = std::thread::spawn(|| -> u64 { panic!("matching stage died") });
2706 let drain = std::thread::spawn(|| {});
2707
2708 assert!(matches!(
2709 shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None),
2710 TeardownOutcome::Panicked
2711 ));
2712 }
2713
2714 fn teardown_fixture(
2719 capacity: usize,
2720 ) -> (
2721 ReplicaPipelineHandles<counter_server::Counter, u32>,
2722 melin_pipeline::ring::Consumer<InputSlot>,
2723 ) {
2724 let (input_producer, mut consumers) =
2725 melin_pipeline::ring::DisruptorBuilder::<InputSlot>::new(capacity)
2726 .add_consumer()
2727 .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
2728 let consumer = consumers.pop().expect("one consumer");
2729 let handles = ReplicaPipelineHandles {
2730 input_producer,
2731 journal_cursor: Arc::new(make_journal_cursor(0)),
2732 last_seq: melin_transport_core::DurableWireSeqCursor::detached(
2733 melin_transport_core::WireSeq::new(0),
2734 ),
2735 chain_hash_lock: None,
2736 stream_marks: Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new())),
2737 journal_failed: Arc::new(AtomicBool::new(false)),
2738 pipeline_shutdown: Arc::new(AtomicBool::new(false)),
2739 journal_handle: std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> {
2740 Ok(11)
2741 }),
2742 matching_handle: std::thread::spawn(counter_server::Counter::default),
2743 drain_handle: std::thread::spawn(|| {}),
2744 shadow_handle: None,
2745 };
2746 (handles, consumer)
2747 }
2748
2749 #[test]
2755 fn teardown_publishes_shutdown_sentinel() {
2756 let (handles, mut consumer) = teardown_fixture(8);
2757 assert!(matches!(
2758 teardown_replica_pipeline::<counter_server::Counter, u32>(handles),
2759 TeardownOutcome::Clean(..)
2760 ));
2761 let (_, slot) = consumer.try_consume().expect("sentinel on the ring");
2762 assert!(
2763 slot.event.is_shutdown(),
2764 "published slot must be the sentinel"
2765 );
2766 assert!(consumer.try_consume().is_none(), "exactly one sentinel");
2767 }
2768
2769 fn backoff_after_disconnect(heard_from_primary: bool) -> std::time::Duration {
2776 let dir = tempfile::tempdir().expect("tempdir");
2777 type Writer = melin_journal::BufferedWriter<CounterEvent>;
2778 let mut pipeline: Option<ReplicaPipelineHandles<counter_server::Counter, Writer>> = None;
2779 let mut divergence_resyncs = 0u32;
2780 let mut backoff = MAX_BACKOFF;
2781 let shutdown = AtomicBool::new(true);
2782 let promote = crate::promotion::PromotionRequest::new();
2783 let after = handle_session_exit::<counter_server::Counter, Writer>(
2784 StreamingResult {
2785 exit: SessionExit::Disconnected,
2786 heard_from_primary,
2787 },
2788 &mut pipeline,
2789 &mut divergence_resyncs,
2790 &mut backoff,
2791 0,
2792 &dir.path().join("r.journal"),
2793 &dir.path().join("r.snapshot"),
2794 &melin_transport_core::fence::FenceState::new(0),
2795 &shutdown,
2796 &promote,
2797 || {},
2798 &(),
2799 );
2800 assert!(matches!(after, AfterSession::Reconnect));
2801 backoff
2802 }
2803
2804 #[test]
2809 fn disconnect_resets_backoff_when_primary_spoke() {
2810 assert_eq!(
2813 backoff_after_disconnect(true),
2814 std::time::Duration::from_secs(2)
2815 );
2816 }
2817
2818 #[test]
2823 fn disconnect_keeps_escalated_backoff_when_primary_silent() {
2824 assert_eq!(backoff_after_disconnect(false), MAX_BACKOFF);
2825 }
2826
2827 #[test]
2836 fn stream_gap_reconnects_and_keeps_pipeline() {
2837 let dir = tempfile::tempdir().expect("tempdir");
2838 type Writer = melin_journal::BufferedWriter<CounterEvent>;
2839 let (input_producer, mut consumers) =
2843 melin_pipeline::ring::DisruptorBuilder::<InputSlot>::new(4)
2844 .add_consumer()
2845 .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
2846 let _consumer = consumers.pop().expect("one consumer");
2847 let writer_path = dir.path().join("w.journal");
2848 let handles = ReplicaPipelineHandles {
2849 input_producer,
2850 journal_cursor: Arc::new(make_journal_cursor(0)),
2851 last_seq: melin_transport_core::DurableWireSeqCursor::detached(
2852 melin_transport_core::WireSeq::new(0),
2853 ),
2854 chain_hash_lock: None,
2855 stream_marks: Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new())),
2856 journal_failed: Arc::new(AtomicBool::new(false)),
2857 pipeline_shutdown: Arc::new(AtomicBool::new(false)),
2858 journal_handle: std::thread::spawn(move || Writer::create(&writer_path)),
2859 matching_handle: std::thread::spawn(counter_server::Counter::default),
2860 drain_handle: std::thread::spawn(|| {}),
2861 shadow_handle: None,
2862 };
2863 let mut pipeline: Option<ReplicaPipelineHandles<counter_server::Counter, Writer>> =
2864 Some(handles);
2865 let mut divergence_resyncs = 0u32;
2866 let mut backoff = MAX_BACKOFF;
2867 let shutdown = AtomicBool::new(true);
2868 let promote = crate::promotion::PromotionRequest::new();
2869 let mut closed = false;
2870 let after = handle_session_exit::<counter_server::Counter, Writer>(
2871 StreamingResult {
2872 exit: SessionExit::StreamGap("sequence gap: expected 3, got 5".into()),
2873 heard_from_primary: true,
2874 },
2875 &mut pipeline,
2876 &mut divergence_resyncs,
2877 &mut backoff,
2878 2,
2879 &dir.path().join("r.journal"),
2880 &dir.path().join("r.snapshot"),
2881 &melin_transport_core::fence::FenceState::new(0),
2882 &shutdown,
2883 &promote,
2884 || closed = true,
2885 &(),
2886 );
2887 assert!(
2888 matches!(after, AfterSession::Reconnect),
2889 "a stream gap must reconnect, not return an error"
2890 );
2891 assert!(
2892 pipeline.is_some(),
2893 "the pipeline must survive for the reconnect"
2894 );
2895 assert!(closed, "the transport must be closed before redialing");
2896 assert_eq!(
2897 backoff,
2898 std::time::Duration::from_secs(2),
2899 "backoff reset to 1s then doubled by the shared helper"
2900 );
2901 assert_eq!(divergence_resyncs, 0, "not a resync");
2902 if let Some(p) = pipeline.take() {
2903 let _ = teardown_replica_pipeline::<counter_server::Counter, Writer>(p);
2904 }
2905 }
2906
2907 #[test]
2912 fn teardown_skips_sentinel_when_journal_failed_and_ring_full() {
2913 let (mut handles, mut consumer) = teardown_fixture(4);
2914 for seq in 0..4 {
2915 assert!(
2916 handles
2917 .input_producer
2918 .try_publish(InputSlot {
2919 sequence: seq,
2920 ..InputSlot::default()
2921 })
2922 .is_ok(),
2923 "ring must have space while filling"
2924 );
2925 }
2926 assert!(
2927 handles
2928 .input_producer
2929 .try_publish(InputSlot::default())
2930 .is_err(),
2931 "ring must be full"
2932 );
2933 handles.journal_failed.store(true, Ordering::Release);
2934 assert!(matches!(
2936 teardown_replica_pipeline::<counter_server::Counter, u32>(handles),
2937 TeardownOutcome::Clean(..)
2938 ));
2939 while let Some((_, slot)) = consumer.try_consume() {
2940 assert!(!slot.event.is_shutdown(), "no sentinel may be published");
2941 }
2942 }
2943
2944 #[test]
2945 fn pending_ack_queue_wraps_around() {
2946 let mut q = PendingAckQueue::new(8);
2947 let cursor = make_journal_cursor(100);
2948
2949 for round in 0..3 {
2951 for i in 0..8 {
2952 let target = (round * 8 + i) as u64 + 1;
2953 q.push(target, target * 10);
2954 }
2955 assert!(q.is_full());
2956 let seq = q.pop_ready(&cursor).expect("should be ready");
2957 assert_eq!(seq, (round * 8 + 8) as u64 * 10);
2958 assert!(q.is_empty());
2959 }
2960 }
2961
2962 #[test]
2963 fn pending_ack_queue_pop_all_blocking_empty() {
2964 let mut q = PendingAckQueue::new(8);
2965 let cursor = make_journal_cursor(0);
2966 assert!(
2967 q.pop_all_blocking(
2968 &cursor,
2969 melin_pipeline::wait::WaitStrategy::BusySpin,
2970 &AtomicBool::new(false)
2971 )
2972 .is_none()
2973 );
2974 }
2975
2976 #[test]
2979 fn dual_track_returns_none_when_both_tracks_idle() {
2980 let mut q = PendingAckQueue::new(8);
2981 let cursor = make_journal_cursor(0);
2982 assert!(
2983 try_flush_dual_track(&mut q, &cursor, 0, 0, 0).is_none(),
2984 "no advance on either track → no ack"
2985 );
2986 }
2987
2988 #[test]
2989 fn dual_track_fires_on_persisted_advance_only() {
2990 let mut q = PendingAckQueue::new(8);
2995 q.push(10, 100);
2996 let cursor = make_journal_cursor(20);
2997 let ack = try_flush_dual_track(&mut q, &cursor, 50, 0, 50).expect("persisted advanced");
2998 assert_eq!(ack.acked_sequence, 100);
2999 assert_eq!(ack.in_memory_sequence, 50);
3000 }
3001
3002 #[test]
3003 fn dual_track_fires_on_in_memory_advance_only() {
3004 let mut q = PendingAckQueue::new(8);
3007 let cursor = make_journal_cursor(0);
3008 let ack = try_flush_dual_track(&mut q, &cursor, 200, 100, 100).expect("in-memory advanced");
3009 assert_eq!(ack.acked_sequence, 100);
3010 assert_eq!(ack.in_memory_sequence, 200);
3011 }
3012
3013 #[test]
3014 fn dual_track_coalesces_until_caller_updates_trackers() {
3015 let mut q = PendingAckQueue::new(8);
3021 q.push(10, 100);
3022 let cursor = make_journal_cursor(20);
3023 let ack1 = try_flush_dual_track(&mut q, &cursor, 50, 0, 50).expect("call 1 fires");
3024 assert_eq!(ack1.acked_sequence, 100);
3025 let ack2 = try_flush_dual_track(&mut q, &cursor, 80, 0, 50)
3027 .expect("call 2 fires on in-memory advance");
3028 assert_eq!(
3029 ack2.acked_sequence, 0,
3030 "no new persisted pop → unwrap_or(last_sent)"
3031 );
3032 assert_eq!(ack2.in_memory_sequence, 80);
3033 }
3034
3035 #[test]
3036 fn dual_track_no_duplicate_ack_after_backpressure_drain() {
3037 let mut q = PendingAckQueue::new(4);
3055 for i in 1..=4u64 {
3058 q.push(i * 10, i * 100);
3059 }
3060 assert!(q.is_full());
3061
3062 let cursor = make_journal_cursor(40);
3065 let drained = q.pop_ready(&cursor).expect("all entries durable");
3066 assert_eq!(drained, 400);
3067 assert!(q.is_empty());
3068
3069 let mut last_sent_acked = drained;
3072 let mut last_sent_in_mem = 450u64;
3073
3074 assert!(
3079 try_flush_dual_track(
3080 &mut q,
3081 &cursor,
3082 last_sent_in_mem,
3083 last_sent_acked,
3084 last_sent_in_mem,
3085 )
3086 .is_none(),
3087 "no advance on either track after backpressure drain → no duplicate ack",
3088 );
3089
3090 q.push(50, 500);
3093 let cursor2 = make_journal_cursor(50);
3094 let ack = try_flush_dual_track(&mut q, &cursor2, 500, last_sent_acked, last_sent_in_mem)
3095 .expect("fresh batch after drain fires a new ack");
3096 assert!(
3097 ack.acked_sequence >= last_sent_acked,
3098 "regression: drain sent {last_sent_acked} but next ack carries {}",
3099 ack.acked_sequence,
3100 );
3101 assert_eq!(ack.acked_sequence, 500);
3102 assert_eq!(ack.in_memory_sequence, 500);
3103
3104 last_sent_acked = ack.acked_sequence;
3105 last_sent_in_mem = ack.in_memory_sequence;
3106
3107 assert!(
3111 try_flush_dual_track(
3112 &mut q,
3113 &cursor2,
3114 last_sent_in_mem,
3115 last_sent_acked,
3116 last_sent_in_mem,
3117 )
3118 .is_none(),
3119 "post-resume idle → no further ack",
3120 );
3121 }
3122}