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 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) -> Result<ReplicaHandles<A>, Box<dyn std::error::Error>>
455where
456 A: Application + Send + 'static,
457 A::Event: Send + Sync + 'static,
458 A::Report: Send + 'static,
459 A::QueryResponse: Send + 'static,
460{
461 let shadow_exchange = <A as Application>::clone_via_snapshot(&exchange)?;
462
463 let enable_shadow = snapshot_interval_ms > 0;
464 let shadow_initial_epoch = fence_state.epoch();
467 let pipeline = melin_transport_core::pipeline::build_replica_pipeline(
468 exchange,
469 writer,
470 4096, group_commit_delay,
472 cores.stage_waits(),
473 enable_shadow,
474 fence_state,
475 );
476
477 let pipeline_shutdown = Arc::new(AtomicBool::new(false));
478
479 let ps = Arc::clone(&pipeline_shutdown);
480 let journal_core = cores.journal_seq.core;
481 let mut journal_stage = pipeline.journal_stage;
482 let stream_marks: melin_transport_core::pipeline::StreamMarkQueue =
487 Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new()));
488 journal_stage.set_stream_marks(Arc::clone(&stream_marks));
489 cores.place_journal_children(&mut journal_stage);
490 journal_stage.set_staging_mode(staging_mode);
491 let journal_failed = Arc::new(AtomicBool::new(false));
492 let journal_failed_latch = Arc::clone(&journal_failed);
493 let sequencer = journal_stage.start()?;
497 pipeline_healthy.store(true, Ordering::Release);
500 let journal_handle = std::thread::Builder::new()
501 .name("journal-seq".into())
502 .spawn(move || {
503 melin_app::affinity::pin_thread("journal-seq", journal_core);
504 let result = sequencer.run(&ps);
505 if let Err(ref e) = result {
506 journal_failed_latch.store(true, Ordering::Release);
513 pipeline_healthy.store(false, Ordering::Release);
516 tracing::error!(error = %e, "replica journal stage failed — session teardown");
517 }
518 result
519 })
520 .expect("spawn journal thread");
521
522 let ps = Arc::clone(&pipeline_shutdown);
523 let matching_core = cores.matching.core;
524 let matching_stage = pipeline.matching_stage;
525 let matching_handle = std::thread::Builder::new()
526 .name("matching".into())
527 .spawn(move || {
528 melin_app::affinity::pin_thread("matching", matching_core);
529 matching_stage.run(&ps)
530 })
531 .expect("spawn matching thread");
532
533 let ps = Arc::clone(&pipeline_shutdown);
536 let drain = cores.response;
537 let drain_consumer = pipeline.drain_consumer;
538 let drain_handle = std::thread::Builder::new()
539 .name("drain".into())
540 .spawn(move || {
541 melin_app::affinity::pin_thread("drain", drain.core);
542 let mut consumer = drain_consumer;
543 let mut batch = vec![OutputSlot::<A::Report, A::QueryResponse>::default(); 256];
544 let mut waiter = drain.wait.waiter();
545 loop {
546 if ps.load(Ordering::Relaxed) {
547 return;
548 }
549 let count = consumer.consume_batch(&mut batch, 256);
550 if count == 0 {
551 waiter.idle();
552 } else {
553 waiter.reset();
554 }
555 }
556 })
557 .expect("spawn drain thread");
558
559 let shadow_handle = if let Some(shadow_cons) = pipeline.shadow_consumer {
560 let snap_path = snapshot_path;
561 let chain_lock = pipeline
562 .chain_hash_lock
563 .as_ref()
564 .expect("chain hash lock with shadow")
565 .clone();
566 let ps = Arc::clone(&pipeline_shutdown);
567 let shadow = cores.shadow;
568 Some(
569 std::thread::Builder::new()
570 .name("replica-shadow".into())
571 .spawn(move || {
572 melin_app::affinity::pin_thread("replica-shadow", shadow.core);
573 melin_transport_core::shadow::run(
574 shadow_cons,
575 shadow_exchange,
576 snap_path,
577 std::time::Duration::from_millis(snapshot_interval_ms),
578 chain_lock,
579 &ps,
580 shadow.wait,
581 shadow_initial_epoch,
582 );
583 })
584 .expect("spawn shadow thread"),
585 )
586 } else {
587 None
588 };
589
590 Ok(ReplicaPipelineHandles {
591 input_producer: pipeline.input_producer,
592 journal_cursor: pipeline.cursors.journal_ring_arc(),
593 last_seq: pipeline.cursors.durable_wire_seq(),
594 chain_hash_lock: pipeline.chain_hash_lock,
595 stream_marks,
596 journal_failed,
597 pipeline_shutdown,
598 journal_handle,
599 matching_handle,
600 drain_handle,
601 shadow_handle,
602 })
603}
604
605pub(super) fn teardown_replica_pipeline<A: Application + Send + 'static, W: Send + 'static>(
610 mut handles: ReplicaPipelineHandles<A, W>,
611) -> TeardownOutcome<A, W> {
612 while !handles.journal_failed.load(Ordering::Acquire) {
629 match handles
630 .input_producer
631 .try_publish(InputSlot::<A::Event>::shutdown_sentinel())
632 {
633 Ok(_) => break,
634 Err(_) => std::thread::yield_now(),
636 }
637 }
638 shutdown_pipeline::<A, W>(
639 &handles.pipeline_shutdown,
640 handles.journal_handle,
641 handles.matching_handle,
642 handles.drain_handle,
643 handles.shadow_handle,
644 )
645}
646
647pub(super) const MAX_INPROCESS_DIVERGENCE_RESYNCS: u32 = 1;
658
659#[allow(clippy::type_complexity)]
676pub(super) fn recover_replica_state<A, W>(
677 journal_path: &std::path::Path,
678 snapshot_path: &std::path::Path,
679 factory: &dyn melin_app::app_factory::AppFactory<App = A>,
680 fence_state: &melin_transport_core::fence::FenceState,
681) -> Result<(Option<A>, Option<W>, u64, [u8; 32]), Box<dyn std::error::Error>>
682where
683 A: Application,
684 W: melin_journal::JournalWrite<A::Event>,
685{
686 let lineage_exists =
687 journal_path.exists() || !melin_journal::segment::list_archives(journal_path)?.is_empty();
688 if !lineage_exists {
689 return Ok((None, None, 0u64, [0u8; 32]));
690 }
691 let engine = if snapshot_path.exists() {
692 tracing::info!("recovering replica from snapshot + journal");
693 melin_transport_core::JournaledApp::<A, W>::recover_from_snapshot(
694 snapshot_path,
695 journal_path,
696 )?
697 } else {
698 melin_transport_core::JournaledApp::<A, W>::recover(factory.empty(), journal_path)?
699 };
700 let next = engine.next_sequence();
701 let last = next.saturating_sub(1);
702 let hash = engine.chain_hash().unwrap_or([0u8; 32]);
703 fence_state.observe_epoch(engine.recovered_epoch());
706 let (mut exchange, writer) = engine.into_parts();
707 factory.apply_operator_policy(&mut exchange);
708 Ok((Some(exchange), Some(writer), last, hash))
709}
710
711pub(super) const MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(30);
717
718pub(in crate::replication) enum AfterSession<A, W> {
722 Return(ReceiverResult<A, W>),
726 Resync {
731 exchange: Option<A>,
732 journal_writer: Option<W>,
733 last_sequence: u64,
734 chain_hash: [u8; 32],
735 },
736 Reconnect,
740}
741
742#[allow(clippy::too_many_arguments)]
763pub(in crate::replication) fn handle_session_exit<A, W>(
764 result: StreamingResult,
765 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
766 divergence_resyncs: &mut u32,
767 backoff: &mut std::time::Duration,
768 last_sequence: u64,
769 journal_path: &std::path::Path,
770 snapshot_path: &std::path::Path,
771 factory: &dyn melin_app::app_factory::AppFactory<App = A>,
772 fence_state: &melin_transport_core::fence::FenceState,
773 shutdown: &AtomicBool,
774 promote: &crate::promotion::PromotionRequest,
775 mut close: impl FnMut(),
776) -> AfterSession<A, W>
777where
778 A: Application + Send + 'static,
779 W: JournalWrite<A::Event> + Send + 'static,
780{
781 let StreamingResult {
782 exit,
783 heard_from_primary,
784 } = result;
785
786 match exit {
787 SessionExit::Shutdown => {
788 if let Some(p) = pipeline.take() {
789 let _ = teardown_replica_pipeline::<A, W>(p);
790 }
791 AfterSession::Return(Ok(None))
792 }
793
794 SessionExit::Promote => AfterSession::Return(match pipeline.take() {
795 Some(p) => match teardown_replica_pipeline::<A, W>(p) {
796 TeardownOutcome::Clean(ex, wr) => Ok(Some((ex, wr))),
797 _ => Err("pipeline failed during promotion".into()),
798 },
799 None => Err("pipeline missing on promote".into()),
800 }),
801
802 SessionExit::Fatal(e) => {
803 let outcome = match pipeline.take() {
804 Some(p) => teardown_replica_pipeline::<A, W>(p),
805 None => return AfterSession::Return(Err(e)),
808 };
809 let TeardownOutcome::JournalFailed(
819 je @ melin_journal::JournalError::ReplicaChainDivergence { .. },
820 ) = outcome
821 else {
822 return AfterSession::Return(Err(e));
823 };
824
825 *divergence_resyncs += 1;
826 let attempt = *divergence_resyncs;
827 if attempt > MAX_INPROCESS_DIVERGENCE_RESYNCS {
828 return AfterSession::Return(Err(format!(
829 "mid-stream chain divergence recurred {attempt} times — giving up on \
830 in-process resync (each cycle archives the local journal and re-seeds \
831 from the primary; recurrence at this rate means the primary keeps \
832 streaming history that forks from what it announces): {je}"
833 )
834 .into()));
835 }
836 tracing::warn!(
837 error = %je,
838 attempt,
839 max_attempts = MAX_INPROCESS_DIVERGENCE_RESYNCS,
840 "mid-stream chain divergence — re-deriving local state for in-process resync"
841 );
842 close();
844 match recover_replica_state::<A, W>(journal_path, snapshot_path, factory, fence_state) {
845 Ok((exchange, journal_writer, seq, hash)) => AfterSession::Resync {
846 exchange,
847 journal_writer,
848 last_sequence: seq,
849 chain_hash: hash,
850 },
851 Err(e) => AfterSession::Return(Err(e)),
852 }
853 }
854
855 SessionExit::StreamGap(e) => {
856 close();
865 *backoff = std::time::Duration::from_secs(1);
866 tracing::warn!(
867 error = %e,
868 last_sequence,
869 "replication stream broke contiguity — reconnecting from the durable position"
870 );
871 sleep_then_double_backoff(backoff, shutdown, promote);
872 AfterSession::Reconnect
873 }
874
875 SessionExit::Disconnected => {
876 close();
879 if heard_from_primary {
888 *backoff = std::time::Duration::from_secs(1);
889 }
890 tracing::warn!(
891 last_sequence,
892 heard_from_primary,
893 backoff_secs = backoff.as_secs(),
894 "reconnecting to primary"
895 );
896 sleep_then_double_backoff(backoff, shutdown, promote);
897 AfterSession::Reconnect
898 }
899 }
900}
901
902pub(in crate::replication) fn take_pipeline_for_promotion<A, W>(
912 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
913 exchange: &mut Option<A>,
914 journal_writer: &mut Option<W>,
915) -> ReceiverResult<A, W>
916where
917 A: Application + Send + 'static,
918 W: JournalWrite<A::Event> + Send + 'static,
919{
920 if let Some(p) = pipeline.take()
921 && let TeardownOutcome::Clean(e, w) = teardown_replica_pipeline::<A, W>(p)
922 {
923 *exchange = Some(e);
924 *journal_writer = Some(w);
925 }
926 match (exchange.take(), journal_writer.take()) {
927 (Some(e), Some(w)) => Ok(Some((e, w))),
928 _ => Err("promotion requested but no local state available".into()),
929 }
930}
931
932type ResyncTransfer<A> = (A, u64, [u8; 32], u64);
935
936pub(in crate::replication) enum ResyncDecision {
939 Ready {
944 segment_start_sequence: u64,
945 anchor_hash: [u8; 32],
946 resume_sequence: u64,
947 },
948 Retry,
952}
953
954fn receive_resync_transfer<A, S>(
964 source: &mut S,
965 snapshot_path: &std::path::Path,
966 journal_path: &std::path::Path,
967 fence_state: &melin_transport_core::fence::FenceState,
968) -> Result<ResyncTransfer<A>, Box<dyn std::error::Error + Send + Sync>>
969where
970 A: Application,
971 S: ControlFrameSource,
972{
973 let (snap_len, snap_sequence, snap_chain_hash) =
974 match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
975 PrimaryMessage::SnapshotBegin {
976 snapshot_len,
977 snap_sequence,
978 snap_chain_hash,
979 } => (snapshot_len, snap_sequence, snap_chain_hash),
980 other => return Err(format!("expected SnapshotBegin, got {other:?}").into()),
981 };
982
983 tracing::info!(snap_sequence, snap_len, "receiving snapshot");
984 let tmp_path = snapshot_path.with_extension("snapshot.tmp");
985 receive_chunked_body(source, &tmp_path, snap_len, "snapshot")?;
986 std::fs::rename(&tmp_path, snapshot_path)?;
987 tracing::info!(snap_sequence, snap_len, "snapshot received and verified");
988
989 let (snap_exchange, _snap_seq, snap_hash, snap_epoch) =
990 melin_transport_core::snapshot::load::<A>(snapshot_path)?;
991 if snap_hash != snap_chain_hash {
992 return Err(format!(
993 "snapshot chain hash mismatch: primary sent {snap_chain_hash:02x?}, \
994 loaded snapshot has {snap_hash:02x?}"
995 )
996 .into());
997 }
998 fence_state.observe_epoch(snap_epoch);
1001
1002 let seed_len = match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
1007 PrimaryMessage::SegmentSeedBegin { seed_len } => seed_len,
1008 other => {
1009 return Err(format!("expected SegmentSeedBegin after snapshot, got {other:?}").into());
1010 }
1011 };
1012 let seed_tmp = journal_path.with_extension("seed.tmp");
1013 receive_chunked_body(source, &seed_tmp, seed_len, "segment seed")?;
1014 if let Err(e) =
1020 melin_journal::segment::verify_segment_prefix(&seed_tmp, snap_sequence, seed_len)
1021 {
1022 let _ = std::fs::remove_file(&seed_tmp);
1023 return Err(format!("segment seed failed structural verification: {e}").into());
1024 }
1025 std::fs::rename(&seed_tmp, journal_path)?;
1026 melin_journal::segment::fsync_parent_dir(journal_path)?;
1027 Ok((snap_exchange, snap_sequence, snap_chain_hash, seed_len))
1028}
1029
1030#[allow(clippy::too_many_arguments)]
1043pub(in crate::replication) fn handle_resync_verdict<A, W, S>(
1044 divergent: bool,
1045 source: &mut S,
1046 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
1047 exchange: &mut Option<A>,
1048 journal_writer: &mut Option<W>,
1049 journal_path: &std::path::Path,
1050 snapshot_path: &std::path::Path,
1051 fence_state: &melin_transport_core::fence::FenceState,
1052 control: &ReplicaControlPlane,
1053 last_sequence: &mut u64,
1054 chain_hash: &mut [u8; 32],
1055) -> Result<ResyncDecision, Box<dyn std::error::Error + Send + Sync>>
1056where
1057 A: Application + Send + 'static,
1058 W: JournalWrite<A::Event> + Send + 'static,
1059 S: ControlFrameSource,
1060{
1061 if divergent {
1066 tracing::warn!(
1067 last_sequence = *last_sequence,
1068 "primary reports chain divergence — archiving local journal, resyncing from snapshot"
1069 );
1070 } else {
1071 tracing::info!("primary requires snapshot transfer — receiving snapshot");
1072 }
1073
1074 if let Some(p) = pipeline.take() {
1075 let _ = teardown_replica_pipeline::<A, W>(p);
1076 }
1077
1078 *exchange = None;
1085 *journal_writer = None;
1086
1087 let reason = if divergent {
1091 ArchiveReason::Divergent
1092 } else {
1093 ArchiveReason::Resync
1094 };
1095 archive_local_lineage(journal_path, snapshot_path, reason)?;
1096 *last_sequence = 0;
1098 *chain_hash = [0u8; 32];
1099 control
1104 .journal_tip
1105 .reset(melin_transport_core::WireSeq::new(0));
1106
1107 let (snap_exchange, snap_sequence, snap_chain_hash, seed_len) =
1108 match receive_resync_transfer::<A, S>(source, snapshot_path, journal_path, fence_state) {
1109 Ok(v) => v,
1110 Err(e) => {
1111 tracing::warn!(error = %e, "snapshot transfer failed — retrying");
1112 let _ = std::fs::remove_file(snapshot_path);
1116 return Ok(ResyncDecision::Retry);
1117 }
1118 };
1119 *exchange = Some(snap_exchange);
1120
1121 let writer = W::open_append(journal_path, snap_sequence, seed_len)?;
1128 let seeded_chain = writer.chain_hash().unwrap_or(snap_chain_hash);
1129 if snap_chain_hash != [0u8; 32] && seeded_chain != snap_chain_hash {
1130 return Err(format!(
1131 "segment seed chain at {snap_sequence} disagrees with the transferred snapshot's \
1132 hash — inconsistent primary"
1133 )
1134 .into());
1135 }
1136 let seeded_info = melin_journal::segment::read_header_info(journal_path)?;
1137 *journal_writer = Some(writer);
1138
1139 match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
1142 PrimaryMessage::StreamStart {
1143 start_sequence,
1144 segment_start_sequence,
1145 anchor_hash,
1146 epoch,
1147 ack_policy,
1148 } => {
1149 if segment_start_sequence != seeded_info.starting_sequence
1150 || anchor_hash != seeded_info.anchor_hash
1151 {
1152 return Err(format!(
1153 "post-snapshot StreamStart lineage (start {segment_start_sequence}) \
1154 disagrees with the transferred segment seed (start {}) — inconsistent \
1155 primary",
1156 seeded_info.starting_sequence
1157 )
1158 .into());
1159 }
1160 fence_state.observe_epoch(epoch);
1164 control
1165 .primary_ack_policy
1166 .store(ack_policy, Ordering::Release);
1167 tracing::info!(
1168 start_sequence,
1169 epoch,
1170 ack_policy,
1171 "streaming resumed after snapshot transfer"
1172 );
1173 control
1176 .journal_tip
1177 .reset(melin_transport_core::WireSeq::new(snap_sequence));
1178 Ok(ResyncDecision::Ready {
1179 segment_start_sequence,
1180 anchor_hash,
1181 resume_sequence: snap_sequence,
1182 })
1183 }
1184 other => Err(format!("expected StreamStart after snapshot, got {other:?}").into()),
1185 }
1186}
1187
1188#[cfg(test)]
1189mod tests {
1190 use std::io::Write;
1191 use std::sync::Arc;
1192
1193 use super::auth::{authenticate_replica, authenticate_with_primary};
1194 use super::*;
1195 use counter_server::CounterEvent;
1201 type InputSlot = melin_transport_core::pipeline::InputSlot<CounterEvent>;
1202 use melin_transport_core::replication::protocol::{
1203 MAX_CONTROL_FRAME, MAX_DATA_FRAME, MSG_AUTH_OK, MSG_CHALLENGE_RESPONSE, MSG_SNAPSHOT_BEGIN,
1204 MSG_SNAPSHOT_CHUNK, MSG_SNAPSHOT_END, decode_auth_result, decode_challenge,
1205 decode_challenge_response, decode_primary_message, decode_replica_message, encode_ack,
1206 encode_auth_failed, encode_auth_ok, encode_chain_check, encode_challenge,
1207 encode_challenge_response, encode_handshake, encode_hash_mismatch, encode_heartbeat,
1208 encode_input_batch, encode_need_snapshot, encode_rotate, encode_segment_seed_begin,
1209 encode_snapshot_begin, encode_snapshot_chunk, encode_snapshot_end, encode_stream_start,
1210 read_frame, try_decode_input_batch,
1211 };
1212
1213 fn encode_input_batch_with_seq(end_sequence: u64, buf: &mut Vec<u8>) {
1217 let slot = InputSlot {
1218 connection_id: 0,
1219 key_hash: 0,
1220 request_seq: 0,
1221 sequence: end_sequence,
1222 timestamp_ns: 0,
1223 event: melin_journal::JournalEvent::Tick { now_ns: 0 },
1224 publish_ts: Default::default(),
1225 recv_ts: Default::default(),
1226 };
1227 encode_input_batch(&[slot], buf);
1228 }
1229
1230 #[test]
1231 fn handshake_encode_decode_round_trip() {
1232 let handshake = Handshake {
1233 last_sequence: 42,
1234 chain_hash: [0xAB; 32],
1235 epoch: 9,
1237 };
1238 let mut buf = Vec::new();
1239 encode_handshake(&handshake, &mut buf);
1240
1241 let payload = &buf[4..];
1243 let msg = decode_replica_message(payload).unwrap();
1244 match msg {
1245 ReplicaMessage::Handshake(h) => {
1246 assert_eq!(h.last_sequence, 42);
1247 assert_eq!(h.chain_hash, [0xAB; 32]);
1248 assert_eq!(h.epoch, 9);
1249 }
1250 _ => panic!("expected Handshake"),
1251 }
1252 }
1253
1254 #[test]
1255 fn ack_encode_decode_round_trip() {
1256 let ack = Ack {
1257 acked_sequence: 1000,
1258 in_memory_sequence: 1024,
1259 };
1260 let mut buf = Vec::new();
1261 encode_ack(&ack, &mut buf);
1262
1263 let payload = &buf[4..];
1264 let msg = decode_replica_message(payload).unwrap();
1265 match msg {
1266 ReplicaMessage::Ack(a) => {
1267 assert_eq!(a.acked_sequence, 1000);
1268 assert_eq!(a.in_memory_sequence, 1024);
1269 }
1270 _ => panic!("expected Ack"),
1271 }
1272 }
1273
1274 #[test]
1282 fn ack_wire_byte_pattern() {
1283 let ack = Ack {
1284 acked_sequence: 0xDEAD_BEEF_CAFE_F00D,
1285 in_memory_sequence: 0x1122_3344_5566_7788,
1286 };
1287 let mut buf = Vec::new();
1288 encode_ack(&ack, &mut buf);
1289 let expected: &[u8] = &[
1292 0x11, 0x00, 0x00, 0x00, 0x02, 0x0D, 0xF0, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, ];
1297 assert_eq!(buf.as_slice(), expected, "Ack wire layout drifted");
1298 }
1299
1300 #[test]
1301 fn stream_start_encode_decode_round_trip() {
1302 let mut buf = Vec::new();
1303 encode_stream_start(99, 42, [0xAA; 32], 5, 2, &mut buf);
1305
1306 let payload = &buf[4..];
1307 let msg = decode_primary_message(payload).unwrap();
1308 match msg {
1309 PrimaryMessage::StreamStart {
1310 start_sequence,
1311 segment_start_sequence,
1312 anchor_hash,
1313 epoch,
1314 ack_policy,
1315 } => {
1316 assert_eq!(start_sequence, 99);
1317 assert_eq!(segment_start_sequence, 42);
1318 assert_eq!(anchor_hash, [0xAA; 32]);
1319 assert_eq!(ack_policy, 2);
1320 assert_eq!(epoch, 5);
1321 }
1322 _ => panic!("expected StreamStart"),
1323 }
1324 }
1325
1326 #[test]
1327 fn heartbeat_encode_decode_round_trip() {
1328 let mut buf = Vec::new();
1329 encode_heartbeat(123, 2, &mut buf);
1330
1331 let payload = &buf[4..];
1332 let msg = decode_primary_message(payload).unwrap();
1333 match msg {
1334 PrimaryMessage::Heartbeat {
1335 sequence,
1336 ack_policy,
1337 } => {
1338 assert_eq!(sequence, 123);
1339 assert_eq!(ack_policy, 2);
1340 }
1341 _ => panic!("expected Heartbeat"),
1342 }
1343 }
1344
1345 #[test]
1346 fn need_snapshot_encode_decode_round_trip() {
1347 let mut buf = Vec::new();
1348 encode_need_snapshot(&mut buf);
1349
1350 let payload = &buf[4..];
1351 let msg = decode_primary_message(payload).unwrap();
1352 assert!(matches!(msg, PrimaryMessage::NeedSnapshot));
1353 }
1354
1355 #[test]
1356 fn hash_mismatch_encode_decode_round_trip() {
1357 let mut buf = Vec::new();
1358 encode_hash_mismatch(&mut buf);
1359
1360 let payload = &buf[4..];
1361 let msg = decode_primary_message(payload).unwrap();
1362 assert!(matches!(msg, PrimaryMessage::HashMismatch));
1363 }
1364
1365 #[test]
1366 fn rotate_encode_decode_round_trip() {
1367 let mut buf = Vec::new();
1368 encode_rotate(7_000_000, &[0xBB; 32], &mut buf);
1369
1370 let payload = &buf[4..];
1371 let msg = decode_primary_message(payload).unwrap();
1372 match msg {
1373 PrimaryMessage::Rotate {
1374 boundary_seq,
1375 tail_hash,
1376 } => {
1377 assert_eq!(boundary_seq, 7_000_000);
1378 assert_eq!(tail_hash, [0xBB; 32]);
1379 }
1380 _ => panic!("expected Rotate"),
1381 }
1382 }
1383
1384 #[test]
1385 fn chain_check_encode_decode_round_trip() {
1386 let mut buf = Vec::new();
1387 encode_chain_check(555, &[0xCC; 32], &mut buf);
1388
1389 let payload = &buf[4..];
1390 let msg = decode_primary_message(payload).unwrap();
1391 match msg {
1392 PrimaryMessage::ChainCheck {
1393 sequence,
1394 chain_hash,
1395 } => {
1396 assert_eq!(sequence, 555);
1397 assert_eq!(chain_hash, [0xCC; 32]);
1398 }
1399 _ => panic!("expected ChainCheck"),
1400 }
1401 }
1402
1403 #[test]
1407 fn rotate_wire_byte_pattern() {
1408 let mut hash = [0u8; 32];
1409 for (i, b) in hash.iter_mut().enumerate() {
1410 *b = i as u8;
1411 }
1412 let mut buf = Vec::new();
1413 encode_rotate(0xDEAD_BEEF_CAFE_F00D, &hash, &mut buf);
1414 let mut expected = vec![
1417 0x29, 0x00, 0x00, 0x00, 0x16, 0x0D, 0xF0, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE, ];
1421 expected.extend_from_slice(&hash);
1422 assert_eq!(
1423 buf.as_slice(),
1424 expected.as_slice(),
1425 "Rotate wire layout drifted"
1426 );
1427 }
1428
1429 #[test]
1430 fn segment_seed_begin_encode_decode_round_trip() {
1431 let mut buf = Vec::new();
1432 encode_segment_seed_begin(123_456, &mut buf);
1433
1434 let payload = &buf[4..];
1435 let msg = decode_primary_message(payload).unwrap();
1436 match msg {
1437 PrimaryMessage::SegmentSeedBegin { seed_len } => {
1438 assert_eq!(seed_len, 123_456);
1439 }
1440 _ => panic!("expected SegmentSeedBegin"),
1441 }
1442 }
1443
1444 #[test]
1445 fn unknown_replica_message_type_is_error() {
1446 let payload = [0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
1447 let result = decode_replica_message(&payload);
1448 assert!(result.is_err());
1449 }
1450
1451 #[test]
1452 fn unknown_primary_message_type_is_error() {
1453 let payload = [0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
1454 let result = decode_primary_message(&payload);
1455 assert!(result.is_err());
1456 }
1457
1458 #[test]
1459 fn read_frame_enforces_max_size() {
1460 let mut buf = Vec::new();
1462 buf.extend_from_slice(&1000u32.to_le_bytes());
1463 buf.extend_from_slice(&[0u8; 100]); let mut cursor = std::io::Cursor::new(buf);
1466 let result = read_frame(&mut cursor, 64);
1467 assert!(result.is_err());
1468 }
1469
1470 #[test]
1471 fn challenge_encode_decode_round_trip() {
1472 let nonce = [0x42; 32];
1473 let mut buf = Vec::new();
1474 encode_challenge(&nonce, &mut buf);
1475
1476 let payload = &buf[4..];
1477 let decoded = decode_challenge(payload).unwrap();
1478 assert_eq!(decoded, nonce);
1479 }
1480
1481 #[test]
1482 fn challenge_response_encode_decode_round_trip() {
1483 let sig = [0xAA; 64];
1484 let pubkey = [0xBB; 32];
1485 let mut buf = Vec::new();
1486 encode_challenge_response(&sig, &pubkey, &mut buf);
1487
1488 let payload = &buf[4..];
1489 let (decoded_sig, decoded_pubkey) = decode_challenge_response(payload).unwrap();
1490 assert_eq!(decoded_sig, sig);
1491 assert_eq!(decoded_pubkey, pubkey);
1492 }
1493
1494 #[test]
1495 fn auth_ok_encode_decode_round_trip() {
1496 let mut buf = Vec::new();
1497 encode_auth_ok(&mut buf);
1498
1499 let payload = &buf[4..];
1500 assert!(decode_auth_result(payload).unwrap());
1501 }
1502
1503 #[test]
1504 fn auth_failed_encode_decode_round_trip() {
1505 let mut buf = Vec::new();
1506 encode_auth_failed(&mut buf);
1507
1508 let payload = &buf[4..];
1509 assert!(!decode_auth_result(payload).unwrap());
1510 }
1511
1512 #[test]
1513 fn decode_challenge_rejects_wrong_tag() {
1514 let mut payload = [0u8; 33];
1515 payload[0] = MSG_AUTH_OK;
1516 assert!(decode_challenge(&payload).is_err());
1517 }
1518
1519 #[test]
1520 fn decode_challenge_response_rejects_short_payload() {
1521 let payload = [MSG_CHALLENGE_RESPONSE; 10]; assert!(decode_challenge_response(&payload).is_err());
1523 }
1524
1525 #[test]
1526 fn auth_round_trip_valid_key() {
1527 use ed25519_dalek::SigningKey;
1528 use std::os::unix::net::UnixStream;
1529
1530 let repl_key = SigningKey::from_bytes(&[0xFC; 32]);
1531 let pub_b64 = base64::Engine::encode(
1532 &base64::engine::general_purpose::STANDARD,
1533 repl_key.verifying_key().to_bytes(),
1534 );
1535 let keys_content = format!("replication {pub_b64} test-replica\n");
1536 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1537
1538 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1539 primary_stream
1540 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1541 .unwrap();
1542 replica_stream
1543 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1544 .unwrap();
1545
1546 let repl_key_clone = SigningKey::from_bytes(&[0xFC; 32]);
1547 let replica_handle = std::thread::spawn(move || {
1548 let mut conn = replica_stream;
1549 authenticate_with_primary(&mut conn, &repl_key_clone)
1550 });
1551
1552 let mut conn = primary_stream;
1553 authenticate_replica(&mut conn, &authorized_keys).unwrap();
1554
1555 replica_handle.join().unwrap().unwrap();
1556 }
1557
1558 #[test]
1559 fn auth_rejects_unknown_key() {
1560 use ed25519_dalek::SigningKey;
1561 use std::os::unix::net::UnixStream;
1562
1563 let authorized_key = SigningKey::from_bytes(&[0xAA; 32]);
1565 let rogue_key = SigningKey::from_bytes(&[0xBB; 32]);
1566 let pub_b64 = base64::Engine::encode(
1567 &base64::engine::general_purpose::STANDARD,
1568 authorized_key.verifying_key().to_bytes(),
1569 );
1570 let keys_content = format!("replication {pub_b64} authorized-replica\n");
1571 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1572
1573 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1574 primary_stream
1575 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1576 .unwrap();
1577 replica_stream
1578 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1579 .unwrap();
1580
1581 let replica_handle = std::thread::spawn(move || {
1582 let mut conn = replica_stream;
1583 authenticate_with_primary(&mut conn, &rogue_key)
1584 });
1585
1586 let mut conn = primary_stream;
1587 let result = authenticate_replica(&mut conn, &authorized_keys);
1588 assert!(result.is_err());
1589 assert!(result.unwrap_err().to_string().contains("unknown"));
1590
1591 let replica_result = replica_handle.join().unwrap();
1593 assert!(replica_result.is_err());
1594 }
1595
1596 #[test]
1597 fn auth_rejects_wrong_permission() {
1598 use ed25519_dalek::SigningKey;
1599 use std::os::unix::net::UnixStream;
1600
1601 let key = SigningKey::from_bytes(&[0xCC; 32]);
1603 let pub_b64 = base64::Engine::encode(
1604 &base64::engine::general_purpose::STANDARD,
1605 key.verifying_key().to_bytes(),
1606 );
1607 let keys_content = format!("trader {pub_b64} wrong-role\n");
1608 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1609
1610 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1611 primary_stream
1612 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1613 .unwrap();
1614 replica_stream
1615 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1616 .unwrap();
1617
1618 let replica_handle = std::thread::spawn(move || {
1619 let mut conn = replica_stream;
1620 authenticate_with_primary(&mut conn, &key)
1621 });
1622
1623 let mut conn = primary_stream;
1624 let result = authenticate_replica(&mut conn, &authorized_keys);
1625 assert!(result.is_err());
1626 assert!(result.unwrap_err().to_string().contains("Replication"));
1627
1628 let replica_result = replica_handle.join().unwrap();
1629 assert!(replica_result.is_err());
1630 }
1631
1632 #[test]
1635 fn auth_rejects_invalid_signature() {
1636 use ed25519_dalek::SigningKey;
1637 use std::os::unix::net::UnixStream;
1638
1639 let correct_key = SigningKey::from_bytes(&[0xDD; 32]);
1641 let pub_b64 = base64::Engine::encode(
1642 &base64::engine::general_purpose::STANDARD,
1643 correct_key.verifying_key().to_bytes(),
1644 );
1645 let keys_content = format!("replication {pub_b64} test-replica\n");
1646 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1647
1648 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1649 primary_stream
1650 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1651 .unwrap();
1652 replica_stream
1653 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1654 .unwrap();
1655
1656 let replica_handle = std::thread::spawn(move || {
1659 use melin_transport_core::replication::protocol::*;
1660
1661 let mut reader = replica_stream.try_clone().unwrap();
1662 let mut writer = replica_stream;
1663
1664 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1666 let nonce = decode_challenge(&frame).unwrap();
1667
1668 let wrong_key = SigningKey::from_bytes(&[0xEE; 32]);
1670 let bad_signature = ed25519_dalek::Signer::sign(&wrong_key, &nonce);
1671 let correct_pubkey = correct_key.verifying_key();
1672
1673 let mut buf = Vec::with_capacity(128);
1674 encode_challenge_response(
1675 &bad_signature.to_bytes(),
1676 correct_pubkey.as_bytes(),
1677 &mut buf,
1678 );
1679 writer.write_all(&buf).unwrap();
1680 writer.flush().unwrap();
1681
1682 let result_frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1684 let ok = decode_auth_result(&result_frame).unwrap();
1685 assert!(!ok, "should receive auth failure");
1686 });
1687
1688 let mut conn = primary_stream;
1689 let result = authenticate_replica(&mut conn, &authorized_keys);
1690 assert!(result.is_err());
1691 assert!(
1692 result
1693 .unwrap_err()
1694 .to_string()
1695 .contains("signature verification failed"),
1696 "should fail on signature verification"
1697 );
1698
1699 replica_handle.join().unwrap();
1700 }
1701
1702 #[test]
1703 fn sender_receiver_end_to_end() {
1704 use std::os::unix::net::UnixStream;
1705
1706 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1708
1709 let replica_handle = std::thread::spawn(move || {
1711 let mut reader = replica_stream.try_clone().unwrap();
1712 let mut writer = replica_stream;
1713
1714 let mut buf = Vec::new();
1716 let handshake = Handshake {
1717 last_sequence: 0,
1718 chain_hash: [0u8; 32],
1719 epoch: 0,
1720 };
1721 encode_handshake(&handshake, &mut buf);
1722 writer.write_all(&buf).unwrap();
1723 writer.flush().unwrap();
1724 buf.clear();
1725
1726 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1728 let msg = decode_primary_message(&frame).unwrap();
1729 assert!(matches!(msg, PrimaryMessage::StreamStart { .. }));
1730
1731 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1733 let slots: Vec<InputSlot> = try_decode_input_batch(&frame).expect("decode InputBatch");
1734 let end_seq = slots
1735 .last()
1736 .map(|s| s.sequence)
1737 .expect("InputBatch carried at least one slot");
1738
1739 let ack = Ack {
1741 acked_sequence: end_seq,
1742 in_memory_sequence: end_seq,
1743 };
1744 encode_ack(&ack, &mut buf);
1745 writer.write_all(&buf).unwrap();
1746 writer.flush().unwrap();
1747
1748 end_seq
1749 });
1750
1751 let mut p_reader = primary_stream.try_clone().unwrap();
1753 let mut p_writer = primary_stream;
1754
1755 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1757 let handshake = match decode_replica_message(&frame).unwrap() {
1758 ReplicaMessage::Handshake(h) => h,
1759 _ => panic!("expected Handshake"),
1760 };
1761 assert_eq!(handshake.last_sequence, 0);
1762
1763 let mut buf = Vec::new();
1765 encode_stream_start(0, 1, [0u8; 32], 0, 1, &mut buf); p_writer.write_all(&buf).unwrap();
1767 p_writer.flush().unwrap();
1768 buf.clear();
1769
1770 encode_input_batch_with_seq(42, &mut buf);
1772 p_writer.write_all(&buf).unwrap();
1773 p_writer.flush().unwrap();
1774 buf.clear();
1775
1776 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1778 let ack = match decode_replica_message(&frame).unwrap() {
1779 ReplicaMessage::Ack(a) => a,
1780 _ => panic!("expected Ack"),
1781 };
1782 assert_eq!(ack.acked_sequence, 42);
1783
1784 let end_seq = replica_handle.join().unwrap();
1786 assert_eq!(end_seq, 42);
1787 }
1788
1789 #[test]
1790 fn multiple_data_batches_acked_in_order() {
1791 use std::os::unix::net::UnixStream;
1794
1795 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1796
1797 let replica_handle = std::thread::spawn(move || {
1798 let mut reader = replica_stream.try_clone().unwrap();
1799 let mut writer = replica_stream;
1800 let mut buf = Vec::new();
1801
1802 encode_handshake(
1804 &Handshake {
1805 last_sequence: 0,
1806 chain_hash: [0u8; 32],
1807 epoch: 0,
1808 },
1809 &mut buf,
1810 );
1811 writer.write_all(&buf).unwrap();
1812 writer.flush().unwrap();
1813 buf.clear();
1814
1815 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1817 assert!(matches!(
1818 decode_primary_message(&frame).unwrap(),
1819 PrimaryMessage::StreamStart { .. }
1820 ));
1821
1822 let mut acked_seqs = Vec::new();
1824 for _ in 0..3 {
1825 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1826 let slots: Vec<InputSlot> =
1827 try_decode_input_batch(&frame).expect("decode InputBatch");
1828 let end_seq = slots
1829 .last()
1830 .map(|s| s.sequence)
1831 .expect("InputBatch carried at least one slot");
1832 acked_seqs.push(end_seq);
1833
1834 encode_ack(
1835 &Ack {
1836 acked_sequence: end_seq,
1837 in_memory_sequence: end_seq,
1838 },
1839 &mut buf,
1840 );
1841 writer.write_all(&buf).unwrap();
1842 writer.flush().unwrap();
1843 buf.clear();
1844 }
1845
1846 acked_seqs
1847 });
1848
1849 let mut p_reader = primary_stream.try_clone().unwrap();
1851 let mut p_writer = primary_stream;
1852 let mut buf = Vec::new();
1853
1854 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1856 assert!(matches!(
1857 decode_replica_message(&frame).unwrap(),
1858 ReplicaMessage::Handshake(_)
1859 ));
1860
1861 encode_stream_start(0, 1, [0u8; 32], 0, 1, &mut buf);
1863 p_writer.write_all(&buf).unwrap();
1864 p_writer.flush().unwrap();
1865 buf.clear();
1866
1867 for seq in [10u64, 20, 30] {
1869 encode_input_batch_with_seq(seq, &mut buf);
1870 p_writer.write_all(&buf).unwrap();
1871 p_writer.flush().unwrap();
1872 buf.clear();
1873 }
1874
1875 for expected_seq in [10u64, 20, 30] {
1877 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1878 let ack = match decode_replica_message(&frame).unwrap() {
1879 ReplicaMessage::Ack(a) => a,
1880 other => panic!("expected Ack, got {other:?}"),
1881 };
1882 assert_eq!(ack.acked_sequence, expected_seq);
1883 }
1884
1885 let acked = replica_handle.join().unwrap();
1886 assert_eq!(acked, vec![10, 20, 30]);
1887 }
1888
1889 #[test]
1890 fn heartbeat_encode_contains_sequence() {
1891 let mut buf = Vec::new();
1894 encode_heartbeat(999, 1, &mut buf);
1895
1896 let payload = &buf[4..];
1897 match decode_primary_message(payload).unwrap() {
1898 PrimaryMessage::Heartbeat { sequence, .. } => {
1899 assert_eq!(sequence, 999);
1900 }
1901 other => panic!("expected Heartbeat, got {other:?}"),
1902 }
1903 }
1904
1905 #[test]
1906 fn replica_mid_stream_handshake_with_nonzero_sequence() {
1907 use std::os::unix::net::UnixStream;
1912
1913 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1914
1915 let replica_handle = std::thread::spawn(move || {
1916 let mut reader = replica_stream.try_clone().unwrap();
1917 let mut writer = replica_stream;
1918 let mut buf = Vec::new();
1919
1920 encode_handshake(
1922 &Handshake {
1923 last_sequence: 100,
1924 chain_hash: [0xBB; 32],
1925 epoch: 0,
1926 },
1927 &mut buf,
1928 );
1929 writer.write_all(&buf).unwrap();
1930 writer.flush().unwrap();
1931 buf.clear();
1932
1933 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1935 match decode_primary_message(&frame).unwrap() {
1936 PrimaryMessage::StreamStart { start_sequence, .. } => {
1937 assert_eq!(
1938 start_sequence, 100,
1939 "StreamStart should echo replica's last_sequence"
1940 );
1941 }
1942 other => panic!("expected StreamStart, got {other:?}"),
1943 }
1944
1945 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1947 let slots: Vec<InputSlot> = try_decode_input_batch(&frame).expect("decode InputBatch");
1948 let end_sequence = slots
1949 .last()
1950 .map(|s| s.sequence)
1951 .expect("InputBatch carried at least one slot");
1952 assert!(
1953 end_sequence > 100,
1954 "InputBatch should be after replica's last_sequence"
1955 );
1956 });
1957
1958 let mut p_reader = primary_stream.try_clone().unwrap();
1960 let mut p_writer = primary_stream;
1961 let mut buf = Vec::new();
1962
1963 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1965 let handshake = match decode_replica_message(&frame).unwrap() {
1966 ReplicaMessage::Handshake(h) => h,
1967 _ => panic!("expected Handshake"),
1968 };
1969 assert_eq!(handshake.last_sequence, 100);
1970 assert_eq!(handshake.chain_hash, [0xBB; 32]);
1971
1972 encode_stream_start(handshake.last_sequence, 1, [0u8; 32], 0, 1, &mut buf);
1974 p_writer.write_all(&buf).unwrap();
1975 p_writer.flush().unwrap();
1976 buf.clear();
1977
1978 encode_input_batch_with_seq(150, &mut buf);
1980 p_writer.write_all(&buf).unwrap();
1981 p_writer.flush().unwrap();
1982
1983 replica_handle.join().unwrap();
1984 }
1985
1986 #[test]
2001 fn receiver_refuses_stream_start_from_stale_primary() {
2002 use std::os::unix::net::UnixStream;
2003
2004 use melin_transport_core::fence::FenceState;
2005
2006 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2007
2008 const STALE_EPOCH: u64 = 3;
2012 let primary_handle = std::thread::spawn(move || {
2013 let mut reader = primary_stream.try_clone().unwrap();
2014 let mut writer = primary_stream;
2015
2016 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2017 let handshake = match decode_replica_message(&frame).unwrap() {
2018 ReplicaMessage::Handshake(h) => h,
2019 _ => panic!("expected Handshake"),
2020 };
2021 assert_eq!(handshake.epoch, 5, "replica advertises its real epoch");
2024
2025 let mut buf = Vec::new();
2026 encode_stream_start(
2027 handshake.last_sequence,
2028 1,
2029 [0u8; 32],
2030 STALE_EPOCH,
2031 1,
2032 &mut buf,
2033 );
2034 writer.write_all(&buf).unwrap();
2035 writer.flush().unwrap();
2036 });
2037
2038 let fence_state = Arc::new(FenceState::new(5));
2041 let mut r_reader = replica_stream.try_clone().unwrap();
2042 let mut r_writer = replica_stream;
2043
2044 let mut buf = Vec::new();
2045 encode_handshake(
2046 &Handshake {
2047 last_sequence: 0,
2048 chain_hash: [0u8; 32],
2049 epoch: fence_state.epoch(),
2050 },
2051 &mut buf,
2052 );
2053 r_writer.write_all(&buf).unwrap();
2054 r_writer.flush().unwrap();
2055
2056 let frame = read_frame(&mut r_reader, MAX_CONTROL_FRAME).unwrap();
2057 let stream_epoch = match decode_primary_message(&frame).unwrap() {
2058 PrimaryMessage::StreamStart { epoch, .. } => epoch,
2059 other => panic!("expected StreamStart, got {other:?}"),
2060 };
2061 assert_eq!(stream_epoch, STALE_EPOCH, "epoch must survive the wire");
2062
2063 assert!(
2066 fence_state.refuses_primary(stream_epoch),
2067 "replica at epoch 5 must refuse a StreamStart from epoch {STALE_EPOCH}"
2068 );
2069 assert_eq!(fence_state.epoch(), 5, "refusal must not lower our epoch");
2070
2071 assert!(!fence_state.refuses_primary(5), "same tenure is followed");
2074 assert!(!fence_state.refuses_primary(6), "newer primary is followed");
2075
2076 primary_handle.join().unwrap();
2077 }
2078
2079 #[test]
2080 fn snapshot_begin_encode_decode_round_trip() {
2081 let mut buf = Vec::new();
2082 encode_snapshot_begin(1_000_000, 42, &[0xAB; 32], &mut buf);
2083
2084 let payload = &buf[4..];
2085 let msg = decode_primary_message(payload).unwrap();
2086 match msg {
2087 PrimaryMessage::SnapshotBegin {
2088 snapshot_len,
2089 snap_sequence,
2090 snap_chain_hash,
2091 } => {
2092 assert_eq!(snapshot_len, 1_000_000);
2093 assert_eq!(snap_sequence, 42);
2094 assert_eq!(snap_chain_hash, [0xAB; 32]);
2095 }
2096 _ => panic!("expected SnapshotBegin"),
2097 }
2098 }
2099
2100 #[test]
2101 fn snapshot_chunk_encode_decode_round_trip() {
2102 let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
2103 let mut buf = Vec::new();
2104 encode_snapshot_chunk(&data, &mut buf);
2105
2106 let payload = &buf[4..];
2107 let msg = decode_primary_message(payload).unwrap();
2108 match msg {
2109 PrimaryMessage::SnapshotChunk(chunk) => {
2110 assert_eq!(chunk, data);
2111 }
2112 _ => panic!("expected SnapshotChunk"),
2113 }
2114 }
2115
2116 #[test]
2117 fn snapshot_end_encode_decode_round_trip() {
2118 let mut buf = Vec::new();
2119 encode_snapshot_end(0xDEADBEEF, &mut buf);
2120
2121 let payload = &buf[4..];
2122 let msg = decode_primary_message(payload).unwrap();
2123 match msg {
2124 PrimaryMessage::SnapshotEnd { crc32c } => {
2125 assert_eq!(crc32c, 0xDEADBEEF);
2126 }
2127 _ => panic!("expected SnapshotEnd"),
2128 }
2129 }
2130
2131 #[test]
2135 fn snapshot_receiver_detects_length_mismatch() {
2136 use std::os::unix::net::UnixStream;
2137
2138 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2139
2140 let receiver = std::thread::spawn(move || -> String {
2142 let mut reader = replica_stream.try_clone().unwrap();
2143
2144 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2146 assert!(matches!(
2147 decode_primary_message(&frame).unwrap(),
2148 PrimaryMessage::NeedSnapshot,
2149 ));
2150
2151 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2153 let (snap_len, _snap_sequence, _snap_chain_hash) =
2154 match decode_primary_message(&frame).unwrap() {
2155 PrimaryMessage::SnapshotBegin {
2156 snapshot_len,
2157 snap_sequence,
2158 snap_chain_hash,
2159 } => (snapshot_len, snap_sequence, snap_chain_hash),
2160 other => panic!("expected SnapshotBegin, got {other:?}"),
2161 };
2162
2163 let mut received: u64 = 0;
2165 loop {
2166 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
2167 match decode_primary_message(&frame).unwrap() {
2168 PrimaryMessage::SnapshotChunk(data) => {
2169 received += data.len() as u64;
2170 }
2171 PrimaryMessage::SnapshotEnd { .. } => {
2172 if received != snap_len {
2173 return format!(
2174 "snapshot length mismatch: expected {snap_len} bytes, got {received}"
2175 );
2176 }
2177 return String::new(); }
2179 other => panic!("unexpected message: {other:?}"),
2180 }
2181 }
2182 });
2183
2184 let mut writer = primary_stream;
2186 let mut buf = Vec::new();
2187
2188 let actual_data = vec![0xAA; 100];
2189 let wrong_len = 999u64; encode_need_snapshot(&mut buf);
2192 std::io::Write::write_all(&mut writer, &buf).unwrap();
2193 buf.clear();
2194
2195 encode_snapshot_begin(wrong_len, 42, &[0xBB; 32], &mut buf);
2196 std::io::Write::write_all(&mut writer, &buf).unwrap();
2197 buf.clear();
2198
2199 encode_snapshot_chunk(&actual_data, &mut buf);
2200 std::io::Write::write_all(&mut writer, &buf).unwrap();
2201 buf.clear();
2202
2203 let crc = crc32c::crc32c(&actual_data);
2204 encode_snapshot_end(crc, &mut buf);
2205 std::io::Write::write_all(&mut writer, &buf).unwrap();
2206 std::io::Write::flush(&mut writer).unwrap();
2207
2208 let error_msg = receiver.join().unwrap();
2209 assert!(
2210 error_msg.contains("length mismatch"),
2211 "expected length mismatch error, got: {error_msg:?}"
2212 );
2213 }
2214
2215 #[test]
2219 fn snapshot_receiver_detects_crc_mismatch() {
2220 use std::os::unix::net::UnixStream;
2221
2222 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2223
2224 let receiver = std::thread::spawn(move || -> String {
2225 let mut reader = replica_stream.try_clone().unwrap();
2226
2227 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2229 assert!(matches!(
2230 decode_primary_message(&frame).unwrap(),
2231 PrimaryMessage::NeedSnapshot,
2232 ));
2233
2234 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2236 let snap_len = match decode_primary_message(&frame).unwrap() {
2237 PrimaryMessage::SnapshotBegin { snapshot_len, .. } => snapshot_len,
2238 other => panic!("expected SnapshotBegin, got {other:?}"),
2239 };
2240
2241 let mut received_data = Vec::new();
2243 let mut received: u64 = 0;
2244 loop {
2245 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
2246 match decode_primary_message(&frame).unwrap() {
2247 PrimaryMessage::SnapshotChunk(data) => {
2248 received += data.len() as u64;
2249 received_data.extend_from_slice(&data);
2250 }
2251 PrimaryMessage::SnapshotEnd {
2252 crc32c: expected_crc,
2253 } => {
2254 if received != snap_len {
2255 return format!("length mismatch: {snap_len} vs {received}");
2256 }
2257 let actual_crc = crc32c::crc32c(&received_data);
2258 if actual_crc != expected_crc {
2259 return format!(
2260 "CRC mismatch: expected {expected_crc:#x}, got {actual_crc:#x}"
2261 );
2262 }
2263 return String::new();
2264 }
2265 other => panic!("unexpected message: {other:?}"),
2266 }
2267 }
2268 });
2269
2270 let mut writer = primary_stream;
2272 let mut buf = Vec::new();
2273
2274 let data = vec![0xAA; 100];
2275
2276 encode_need_snapshot(&mut buf);
2277 std::io::Write::write_all(&mut writer, &buf).unwrap();
2278 buf.clear();
2279
2280 encode_snapshot_begin(data.len() as u64, 42, &[0xBB; 32], &mut buf);
2281 std::io::Write::write_all(&mut writer, &buf).unwrap();
2282 buf.clear();
2283
2284 encode_snapshot_chunk(&data, &mut buf);
2285 std::io::Write::write_all(&mut writer, &buf).unwrap();
2286 buf.clear();
2287
2288 let wrong_crc = !crc32c::crc32c(&data);
2290 encode_snapshot_end(wrong_crc, &mut buf);
2291 std::io::Write::write_all(&mut writer, &buf).unwrap();
2292 std::io::Write::flush(&mut writer).unwrap();
2293
2294 let error_msg = receiver.join().unwrap();
2295 assert!(
2296 error_msg.contains("CRC mismatch"),
2297 "expected CRC mismatch error, got: {error_msg:?}"
2298 );
2299 }
2300
2301 #[test]
2304 fn snapshot_receiver_detects_chain_hash_mismatch() {
2305 use std::os::unix::net::UnixStream;
2306
2307 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2308
2309 let receiver = std::thread::spawn(move || -> String {
2310 let mut reader = replica_stream.try_clone().unwrap();
2311
2312 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2313 assert!(matches!(
2314 decode_primary_message(&frame).unwrap(),
2315 PrimaryMessage::NeedSnapshot,
2316 ));
2317
2318 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2319 let (snap_len, _snap_sequence, snap_chain_hash) =
2320 match decode_primary_message(&frame).unwrap() {
2321 PrimaryMessage::SnapshotBegin {
2322 snapshot_len,
2323 snap_sequence,
2324 snap_chain_hash,
2325 } => (snapshot_len, snap_sequence, snap_chain_hash),
2326 other => panic!("expected SnapshotBegin, got {other:?}"),
2327 };
2328
2329 let mut received_data = Vec::new();
2331 let mut received: u64 = 0;
2332 loop {
2333 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
2334 match decode_primary_message(&frame).unwrap() {
2335 PrimaryMessage::SnapshotChunk(data) => {
2336 received += data.len() as u64;
2337 received_data.extend_from_slice(&data);
2338 }
2339 PrimaryMessage::SnapshotEnd {
2340 crc32c: expected_crc,
2341 } => {
2342 assert_eq!(received, snap_len, "length should match");
2343 let actual_crc = crc32c::crc32c(&received_data);
2344 assert_eq!(actual_crc, expected_crc, "CRC should match");
2345 break;
2346 }
2347 other => panic!("unexpected message: {other:?}"),
2348 }
2349 }
2350
2351 let loaded_hash = [0xFF; 32]; if loaded_hash != snap_chain_hash {
2355 return format!(
2356 "snapshot chain hash mismatch: primary sent {snap_chain_hash:02x?}, \
2357 loaded snapshot has {loaded_hash:02x?}"
2358 );
2359 }
2360 String::new()
2361 });
2362
2363 let mut writer = primary_stream;
2366 let mut buf = Vec::new();
2367
2368 let data = vec![0xAA; 64];
2369 let advertised_hash = [0xBB; 32];
2371
2372 encode_need_snapshot(&mut buf);
2373 std::io::Write::write_all(&mut writer, &buf).unwrap();
2374 buf.clear();
2375
2376 encode_snapshot_begin(data.len() as u64, 10, &advertised_hash, &mut buf);
2377 std::io::Write::write_all(&mut writer, &buf).unwrap();
2378 buf.clear();
2379
2380 encode_snapshot_chunk(&data, &mut buf);
2381 std::io::Write::write_all(&mut writer, &buf).unwrap();
2382 buf.clear();
2383
2384 let crc = crc32c::crc32c(&data);
2385 encode_snapshot_end(crc, &mut buf);
2386 std::io::Write::write_all(&mut writer, &buf).unwrap();
2387 std::io::Write::flush(&mut writer).unwrap();
2388
2389 let error_msg = receiver.join().unwrap();
2390 assert!(
2391 error_msg.contains("chain hash mismatch"),
2392 "expected chain hash mismatch error, got: {error_msg:?}"
2393 );
2394 }
2395
2396 #[test]
2399 fn primary_rejects_snapshot_with_invalid_magic() {
2400 let tmp = tempfile::tempdir().unwrap();
2401 let snap_path = tmp.path().join("test.snapshot");
2402
2403 let mut bad_snap = vec![0u8; 64];
2405 bad_snap[0..4].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes()); std::fs::write(&snap_path, &bad_snap).unwrap();
2408
2409 let snap_data = std::fs::read(&snap_path).unwrap();
2411 assert!(
2412 snap_data.len() >= 48,
2413 "file should be big enough for header"
2414 );
2415
2416 let magic = u32::from_le_bytes(snap_data[0..4].try_into().unwrap());
2417 assert_ne!(magic, 0x534E_4150);
2418 assert_eq!(magic, 0xDEAD_BEEF);
2419 }
2420
2421 #[test]
2424 fn primary_rejects_snapshot_too_small_for_header() {
2425 let tmp = tempfile::tempdir().unwrap();
2426 let snap_path = tmp.path().join("test.snapshot");
2427
2428 std::fs::write(&snap_path, [0u8; 20]).unwrap();
2430
2431 let snap_data = std::fs::read(&snap_path).unwrap();
2432 assert!(
2433 snap_data.len() < 48,
2434 "file must be too small for header validation"
2435 );
2436 }
2437
2438 #[test]
2439 fn decode_snapshot_begin_too_short() {
2440 let payload = [MSG_SNAPSHOT_BEGIN, 0x01, 0x02, 0x03];
2443 let err = decode_primary_message(&payload).unwrap_err();
2444 assert!(
2445 err.to_string().contains("SnapshotBegin too short"),
2446 "expected 'SnapshotBegin too short', got: {err}"
2447 );
2448 }
2449
2450 #[test]
2451 fn decode_snapshot_end_too_short() {
2452 let payload = [MSG_SNAPSHOT_END];
2454 let err = decode_primary_message(&payload).unwrap_err();
2455 assert!(
2456 err.to_string().contains("SnapshotEnd too short"),
2457 "expected 'SnapshotEnd too short', got: {err}"
2458 );
2459 }
2460
2461 #[test]
2462 fn decode_snapshot_chunk_empty_data() {
2463 let payload = [MSG_SNAPSHOT_CHUNK];
2465 let msg = decode_primary_message(&payload).unwrap();
2466 match msg {
2467 PrimaryMessage::SnapshotChunk(data) => {
2468 assert!(data.is_empty());
2469 }
2470 _ => panic!("expected SnapshotChunk"),
2471 }
2472 }
2473
2474 fn make_journal_cursor(val: u64) -> melin_pipeline::padding::Sequence {
2477 melin_pipeline::padding::Sequence::new(AtomicU64::new(val))
2478 }
2479
2480 #[test]
2481 fn pending_ack_queue_push_and_pop_ready() {
2482 let mut q = PendingAckQueue::new(8);
2483 assert!(q.is_empty());
2484 assert!(!q.is_full());
2485
2486 q.push(10, 100);
2487 q.push(20, 200);
2488 assert!(!q.is_empty());
2489
2490 let cursor = make_journal_cursor(5);
2492 assert!(q.pop_ready(&cursor).is_none());
2493
2494 cursor.get().store(15, Ordering::Relaxed);
2496 assert_eq!(q.pop_ready(&cursor), Some(100));
2497 assert!(!q.is_empty());
2499
2500 cursor.get().store(25, Ordering::Relaxed);
2502 assert_eq!(q.pop_ready(&cursor), Some(200));
2503 assert!(q.is_empty());
2504 }
2505
2506 #[test]
2507 fn pending_ack_queue_pop_ready_returns_highest_sequence() {
2508 let mut q = PendingAckQueue::new(8);
2512 q.push(10, 100);
2513 q.push(20, 200);
2514 q.push(30, 300);
2515
2516 let cursor = make_journal_cursor(30);
2517 assert_eq!(q.pop_ready(&cursor), Some(300));
2518 assert!(q.is_empty());
2519 }
2520
2521 #[test]
2522 fn pending_ack_queue_capacity_and_full() {
2523 let mut q = PendingAckQueue::new(8);
2524 for i in 0..8 {
2525 assert!(!q.is_full());
2526 q.push(i as u64 + 1, (i + 1) as u64 * 100);
2527 }
2528 assert!(q.is_full());
2529 }
2530
2531 #[test]
2536 fn pending_ack_queue_merges_into_the_newest_entry_when_full() {
2537 let mut q = PendingAckQueue::new(4);
2538 for i in 1..=4u64 {
2539 q.push(i * 10, i * 100);
2540 }
2541 assert!(q.is_full());
2542 assert_eq!(q.merged(), 0);
2543
2544 q.push(50, 500);
2546 assert_eq!(q.merged(), 1);
2547 assert_eq!(q.len(), 4, "merge must not grow the queue");
2548
2549 let cursor = make_journal_cursor(40);
2552 assert_eq!(q.pop_ready(&cursor), Some(300));
2553 assert!(!q.is_empty(), "merged entry is not durable yet");
2554
2555 cursor.get().store(50, Ordering::Relaxed);
2558 assert_eq!(q.pop_ready(&cursor), Some(500));
2559 assert!(q.is_empty());
2560 }
2561
2562 #[test]
2565 fn pending_ack_queue_merges_repeatedly_without_growing() {
2566 let mut q = PendingAckQueue::new(2);
2567 for i in 1..=100u64 {
2568 q.push(i, i * 10);
2569 }
2570 assert_eq!(q.len(), 2);
2571 assert_eq!(q.merged(), 98);
2572
2573 let cursor = make_journal_cursor(99);
2575 assert_eq!(q.pop_ready(&cursor), Some(10), "only the first entry");
2576 cursor.get().store(100, Ordering::Relaxed);
2578 assert_eq!(q.pop_ready(&cursor), Some(1000));
2579 }
2580
2581 #[test]
2582 fn pending_ack_queue_pop_oldest_blocking() {
2583 let mut q = PendingAckQueue::new(8);
2584 q.push(10, 100);
2585 q.push(20, 200);
2586
2587 let cursor = make_journal_cursor(25);
2590 let seq = q.pop_oldest_blocking(
2591 &cursor,
2592 melin_pipeline::wait::WaitStrategy::BusySpin,
2593 &AtomicBool::new(false),
2594 );
2595 assert_eq!(seq, Some(200));
2597 assert!(q.is_empty());
2598 }
2599
2600 #[test]
2603 fn pending_ack_queue_blocking_wait_aborts_on_journal_failure() {
2604 let mut q = PendingAckQueue::new(8);
2605 q.push(10, 100);
2606
2607 let cursor = make_journal_cursor(5);
2609 let abort = AtomicBool::new(true);
2610 assert_eq!(
2611 q.pop_oldest_blocking(
2612 &cursor,
2613 melin_pipeline::wait::WaitStrategy::BusySpin,
2614 &abort
2615 ),
2616 None
2617 );
2618 assert!(!q.is_empty(), "aborted wait must not pop the entry");
2619 assert_eq!(
2620 q.pop_all_blocking(
2621 &cursor,
2622 melin_pipeline::wait::WaitStrategy::BusySpin,
2623 &abort
2624 ),
2625 None
2626 );
2627 }
2628
2629 #[test]
2638 fn shutdown_pipeline_surfaces_journal_error_kind() {
2639 let flag = AtomicBool::new(false);
2640 let matching_joined = Arc::new(AtomicBool::new(false));
2641 let mj = Arc::clone(&matching_joined);
2642
2643 let journal = std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> {
2644 Err(melin_journal::JournalError::ReplicaChainDivergence {
2645 sequence: 42,
2646 expected: [1u8; 32],
2647 actual: [2u8; 32],
2648 })
2649 });
2650 let matching = std::thread::spawn(move || {
2651 mj.store(true, Ordering::Release);
2652 7u64
2653 });
2654 let drain = std::thread::spawn(|| {});
2655
2656 let outcome = shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None);
2657 assert!(flag.load(Ordering::Acquire), "shutdown flag must be set");
2658 assert!(
2659 matching_joined.load(Ordering::Acquire),
2660 "matching thread must be joined even when the journal failed"
2661 );
2662 match outcome {
2663 TeardownOutcome::JournalFailed(
2664 melin_journal::JournalError::ReplicaChainDivergence { sequence, .. },
2665 ) => assert_eq!(sequence, 42),
2666 _ => panic!("expected JournalFailed(ReplicaChainDivergence)"),
2667 }
2668 }
2669
2670 #[test]
2671 fn shutdown_pipeline_clean_returns_both_states() {
2672 let flag = AtomicBool::new(false);
2673 let journal =
2674 std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> { Ok(11u32) });
2675 let matching = std::thread::spawn(|| 7u64);
2676 let drain = std::thread::spawn(|| {});
2677
2678 match shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None) {
2679 TeardownOutcome::Clean(app, writer) => {
2680 assert_eq!(app, 7);
2681 assert_eq!(writer, 11);
2682 }
2683 _ => panic!("expected Clean"),
2684 }
2685 }
2686
2687 #[test]
2688 fn shutdown_pipeline_panicked_stage_reports_panicked() {
2689 let flag = AtomicBool::new(false);
2690 let journal =
2691 std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> { Ok(11u32) });
2692 let matching = std::thread::spawn(|| -> u64 { panic!("matching stage died") });
2693 let drain = std::thread::spawn(|| {});
2694
2695 assert!(matches!(
2696 shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None),
2697 TeardownOutcome::Panicked
2698 ));
2699 }
2700
2701 fn teardown_fixture(
2706 capacity: usize,
2707 ) -> (
2708 ReplicaPipelineHandles<counter_server::Counter, u32>,
2709 melin_pipeline::ring::Consumer<InputSlot>,
2710 ) {
2711 use melin_app::app_factory::AppFactory;
2712 let (input_producer, mut consumers) =
2713 melin_pipeline::ring::DisruptorBuilder::<InputSlot>::new(capacity)
2714 .add_consumer()
2715 .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
2716 let consumer = consumers.pop().expect("one consumer");
2717 let handles = ReplicaPipelineHandles {
2718 input_producer,
2719 journal_cursor: Arc::new(make_journal_cursor(0)),
2720 last_seq: melin_transport_core::DurableWireSeqCursor::detached(
2721 melin_transport_core::WireSeq::new(0),
2722 ),
2723 chain_hash_lock: None,
2724 stream_marks: Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new())),
2725 journal_failed: Arc::new(AtomicBool::new(false)),
2726 pipeline_shutdown: Arc::new(AtomicBool::new(false)),
2727 journal_handle: std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> {
2728 Ok(11)
2729 }),
2730 matching_handle: std::thread::spawn(|| counter_server::CounterFactory.empty()),
2731 drain_handle: std::thread::spawn(|| {}),
2732 shadow_handle: None,
2733 };
2734 (handles, consumer)
2735 }
2736
2737 #[test]
2743 fn teardown_publishes_shutdown_sentinel() {
2744 let (handles, mut consumer) = teardown_fixture(8);
2745 assert!(matches!(
2746 teardown_replica_pipeline::<counter_server::Counter, u32>(handles),
2747 TeardownOutcome::Clean(..)
2748 ));
2749 let (_, slot) = consumer.try_consume().expect("sentinel on the ring");
2750 assert!(
2751 slot.event.is_shutdown(),
2752 "published slot must be the sentinel"
2753 );
2754 assert!(consumer.try_consume().is_none(), "exactly one sentinel");
2755 }
2756
2757 fn backoff_after_disconnect(heard_from_primary: bool) -> std::time::Duration {
2764 let dir = tempfile::tempdir().expect("tempdir");
2765 type Writer = melin_journal::BufferedWriter<CounterEvent>;
2766 let mut pipeline: Option<ReplicaPipelineHandles<counter_server::Counter, Writer>> = None;
2767 let mut divergence_resyncs = 0u32;
2768 let mut backoff = MAX_BACKOFF;
2769 let shutdown = AtomicBool::new(true);
2770 let promote = crate::promotion::PromotionRequest::new();
2771 let after = handle_session_exit::<counter_server::Counter, Writer>(
2772 StreamingResult {
2773 exit: SessionExit::Disconnected,
2774 heard_from_primary,
2775 },
2776 &mut pipeline,
2777 &mut divergence_resyncs,
2778 &mut backoff,
2779 0,
2780 &dir.path().join("r.journal"),
2781 &dir.path().join("r.snapshot"),
2782 &counter_server::CounterFactory,
2783 &melin_transport_core::fence::FenceState::new(0),
2784 &shutdown,
2785 &promote,
2786 || {},
2787 );
2788 assert!(matches!(after, AfterSession::Reconnect));
2789 backoff
2790 }
2791
2792 #[test]
2797 fn disconnect_resets_backoff_when_primary_spoke() {
2798 assert_eq!(
2801 backoff_after_disconnect(true),
2802 std::time::Duration::from_secs(2)
2803 );
2804 }
2805
2806 #[test]
2811 fn disconnect_keeps_escalated_backoff_when_primary_silent() {
2812 assert_eq!(backoff_after_disconnect(false), MAX_BACKOFF);
2813 }
2814
2815 #[test]
2824 fn stream_gap_reconnects_and_keeps_pipeline() {
2825 use melin_app::app_factory::AppFactory;
2826 let dir = tempfile::tempdir().expect("tempdir");
2827 type Writer = melin_journal::BufferedWriter<CounterEvent>;
2828 let (input_producer, mut consumers) =
2832 melin_pipeline::ring::DisruptorBuilder::<InputSlot>::new(4)
2833 .add_consumer()
2834 .build(melin_pipeline::wait::WaitStrategy::SpinThenYield);
2835 let _consumer = consumers.pop().expect("one consumer");
2836 let writer_path = dir.path().join("w.journal");
2837 let handles = ReplicaPipelineHandles {
2838 input_producer,
2839 journal_cursor: Arc::new(make_journal_cursor(0)),
2840 last_seq: melin_transport_core::DurableWireSeqCursor::detached(
2841 melin_transport_core::WireSeq::new(0),
2842 ),
2843 chain_hash_lock: None,
2844 stream_marks: Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new())),
2845 journal_failed: Arc::new(AtomicBool::new(false)),
2846 pipeline_shutdown: Arc::new(AtomicBool::new(false)),
2847 journal_handle: std::thread::spawn(move || Writer::create(&writer_path)),
2848 matching_handle: std::thread::spawn(|| counter_server::CounterFactory.empty()),
2849 drain_handle: std::thread::spawn(|| {}),
2850 shadow_handle: None,
2851 };
2852 let mut pipeline: Option<ReplicaPipelineHandles<counter_server::Counter, Writer>> =
2853 Some(handles);
2854 let mut divergence_resyncs = 0u32;
2855 let mut backoff = MAX_BACKOFF;
2856 let shutdown = AtomicBool::new(true);
2857 let promote = crate::promotion::PromotionRequest::new();
2858 let mut closed = false;
2859 let after = handle_session_exit::<counter_server::Counter, Writer>(
2860 StreamingResult {
2861 exit: SessionExit::StreamGap("sequence gap: expected 3, got 5".into()),
2862 heard_from_primary: true,
2863 },
2864 &mut pipeline,
2865 &mut divergence_resyncs,
2866 &mut backoff,
2867 2,
2868 &dir.path().join("r.journal"),
2869 &dir.path().join("r.snapshot"),
2870 &counter_server::CounterFactory,
2871 &melin_transport_core::fence::FenceState::new(0),
2872 &shutdown,
2873 &promote,
2874 || closed = true,
2875 );
2876 assert!(
2877 matches!(after, AfterSession::Reconnect),
2878 "a stream gap must reconnect, not return an error"
2879 );
2880 assert!(
2881 pipeline.is_some(),
2882 "the pipeline must survive for the reconnect"
2883 );
2884 assert!(closed, "the transport must be closed before redialing");
2885 assert_eq!(
2886 backoff,
2887 std::time::Duration::from_secs(2),
2888 "backoff reset to 1s then doubled by the shared helper"
2889 );
2890 assert_eq!(divergence_resyncs, 0, "not a resync");
2891 if let Some(p) = pipeline.take() {
2892 let _ = teardown_replica_pipeline::<counter_server::Counter, Writer>(p);
2893 }
2894 }
2895
2896 #[test]
2901 fn teardown_skips_sentinel_when_journal_failed_and_ring_full() {
2902 let (mut handles, mut consumer) = teardown_fixture(4);
2903 for seq in 0..4 {
2904 assert!(
2905 handles
2906 .input_producer
2907 .try_publish(InputSlot {
2908 sequence: seq,
2909 ..InputSlot::default()
2910 })
2911 .is_ok(),
2912 "ring must have space while filling"
2913 );
2914 }
2915 assert!(
2916 handles
2917 .input_producer
2918 .try_publish(InputSlot::default())
2919 .is_err(),
2920 "ring must be full"
2921 );
2922 handles.journal_failed.store(true, Ordering::Release);
2923 assert!(matches!(
2925 teardown_replica_pipeline::<counter_server::Counter, u32>(handles),
2926 TeardownOutcome::Clean(..)
2927 ));
2928 while let Some((_, slot)) = consumer.try_consume() {
2929 assert!(!slot.event.is_shutdown(), "no sentinel may be published");
2930 }
2931 }
2932
2933 #[test]
2934 fn pending_ack_queue_wraps_around() {
2935 let mut q = PendingAckQueue::new(8);
2936 let cursor = make_journal_cursor(100);
2937
2938 for round in 0..3 {
2940 for i in 0..8 {
2941 let target = (round * 8 + i) as u64 + 1;
2942 q.push(target, target * 10);
2943 }
2944 assert!(q.is_full());
2945 let seq = q.pop_ready(&cursor).expect("should be ready");
2946 assert_eq!(seq, (round * 8 + 8) as u64 * 10);
2947 assert!(q.is_empty());
2948 }
2949 }
2950
2951 #[test]
2952 fn pending_ack_queue_pop_all_blocking_empty() {
2953 let mut q = PendingAckQueue::new(8);
2954 let cursor = make_journal_cursor(0);
2955 assert!(
2956 q.pop_all_blocking(
2957 &cursor,
2958 melin_pipeline::wait::WaitStrategy::BusySpin,
2959 &AtomicBool::new(false)
2960 )
2961 .is_none()
2962 );
2963 }
2964
2965 #[test]
2968 fn dual_track_returns_none_when_both_tracks_idle() {
2969 let mut q = PendingAckQueue::new(8);
2970 let cursor = make_journal_cursor(0);
2971 assert!(
2972 try_flush_dual_track(&mut q, &cursor, 0, 0, 0).is_none(),
2973 "no advance on either track → no ack"
2974 );
2975 }
2976
2977 #[test]
2978 fn dual_track_fires_on_persisted_advance_only() {
2979 let mut q = PendingAckQueue::new(8);
2984 q.push(10, 100);
2985 let cursor = make_journal_cursor(20);
2986 let ack = try_flush_dual_track(&mut q, &cursor, 50, 0, 50).expect("persisted advanced");
2987 assert_eq!(ack.acked_sequence, 100);
2988 assert_eq!(ack.in_memory_sequence, 50);
2989 }
2990
2991 #[test]
2992 fn dual_track_fires_on_in_memory_advance_only() {
2993 let mut q = PendingAckQueue::new(8);
2996 let cursor = make_journal_cursor(0);
2997 let ack = try_flush_dual_track(&mut q, &cursor, 200, 100, 100).expect("in-memory advanced");
2998 assert_eq!(ack.acked_sequence, 100);
2999 assert_eq!(ack.in_memory_sequence, 200);
3000 }
3001
3002 #[test]
3003 fn dual_track_coalesces_until_caller_updates_trackers() {
3004 let mut q = PendingAckQueue::new(8);
3010 q.push(10, 100);
3011 let cursor = make_journal_cursor(20);
3012 let ack1 = try_flush_dual_track(&mut q, &cursor, 50, 0, 50).expect("call 1 fires");
3013 assert_eq!(ack1.acked_sequence, 100);
3014 let ack2 = try_flush_dual_track(&mut q, &cursor, 80, 0, 50)
3016 .expect("call 2 fires on in-memory advance");
3017 assert_eq!(
3018 ack2.acked_sequence, 0,
3019 "no new persisted pop → unwrap_or(last_sent)"
3020 );
3021 assert_eq!(ack2.in_memory_sequence, 80);
3022 }
3023
3024 #[test]
3025 fn dual_track_no_duplicate_ack_after_backpressure_drain() {
3026 let mut q = PendingAckQueue::new(4);
3044 for i in 1..=4u64 {
3047 q.push(i * 10, i * 100);
3048 }
3049 assert!(q.is_full());
3050
3051 let cursor = make_journal_cursor(40);
3054 let drained = q.pop_ready(&cursor).expect("all entries durable");
3055 assert_eq!(drained, 400);
3056 assert!(q.is_empty());
3057
3058 let mut last_sent_acked = drained;
3061 let mut last_sent_in_mem = 450u64;
3062
3063 assert!(
3068 try_flush_dual_track(
3069 &mut q,
3070 &cursor,
3071 last_sent_in_mem,
3072 last_sent_acked,
3073 last_sent_in_mem,
3074 )
3075 .is_none(),
3076 "no advance on either track after backpressure drain → no duplicate ack",
3077 );
3078
3079 q.push(50, 500);
3082 let cursor2 = make_journal_cursor(50);
3083 let ack = try_flush_dual_track(&mut q, &cursor2, 500, last_sent_acked, last_sent_in_mem)
3084 .expect("fresh batch after drain fires a new ack");
3085 assert!(
3086 ack.acked_sequence >= last_sent_acked,
3087 "regression: drain sent {last_sent_acked} but next ack carries {}",
3088 ack.acked_sequence,
3089 );
3090 assert_eq!(ack.acked_sequence, 500);
3091 assert_eq!(ack.in_memory_sequence, 500);
3092
3093 last_sent_acked = ack.acked_sequence;
3094 last_sent_in_mem = ack.in_memory_sequence;
3095
3096 assert!(
3100 try_flush_dual_track(
3101 &mut q,
3102 &cursor2,
3103 last_sent_in_mem,
3104 last_sent_acked,
3105 last_sent_in_mem,
3106 )
3107 .is_none(),
3108 "post-resume idle → no further ack",
3109 );
3110 }
3111}