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::server::PipelineCores,
439 snapshot_interval_ms: u64,
440 snapshot_path: std::path::PathBuf,
441 group_commit_delay: std::time::Duration,
442 busy_spin: bool,
443 fence_state: Arc<melin_transport_core::fence::FenceState>,
444 pipeline_healthy: Arc<AtomicBool>,
450) -> Result<ReplicaHandles<A>, Box<dyn std::error::Error>>
451where
452 A: Application + Send + 'static,
453 A::Event: Send + Sync + 'static,
454 A::Report: Send + 'static,
455 A::QueryResponse: Send + 'static,
456{
457 let shadow_exchange = <A as Application>::clone_via_snapshot(&exchange)?;
458
459 let enable_shadow = snapshot_interval_ms > 0;
460 let shadow_initial_epoch = fence_state.epoch();
463 let pipeline = melin_transport_core::pipeline::build_replica_pipeline(
464 exchange,
465 writer,
466 4096, group_commit_delay,
468 busy_spin,
469 enable_shadow,
470 fence_state,
471 );
472
473 let pipeline_shutdown = Arc::new(AtomicBool::new(false));
474
475 let ps = Arc::clone(&pipeline_shutdown);
476 let journal_core = cores.journal;
477 let mut journal_stage = pipeline.journal_stage;
478 let stream_marks: melin_transport_core::pipeline::StreamMarkQueue =
483 Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new()));
484 journal_stage.set_stream_marks(Arc::clone(&stream_marks));
485 journal_stage.set_preparer_core(cores.journal_prep);
486 journal_stage.set_disk_core(cores.journal_disk);
487 let journal_failed = Arc::new(AtomicBool::new(false));
488 let journal_failed_latch = Arc::clone(&journal_failed);
489 pipeline_healthy.store(true, Ordering::Release);
492 let journal_handle = std::thread::Builder::new()
493 .name("journal".into())
494 .spawn(move || {
495 melin_app::affinity::pin_thread("journal", journal_core);
496 let result = journal_stage.run(&ps);
497 if let Err(ref e) = result {
498 journal_failed_latch.store(true, Ordering::Release);
505 pipeline_healthy.store(false, Ordering::Release);
508 tracing::error!(error = %e, "replica journal stage failed — session teardown");
509 }
510 result
511 })
512 .expect("spawn journal thread");
513
514 let ps = Arc::clone(&pipeline_shutdown);
515 let matching_core = cores.matching;
516 let matching_stage = pipeline.matching_stage;
517 let matching_handle = std::thread::Builder::new()
518 .name("matching".into())
519 .spawn(move || {
520 melin_app::affinity::pin_thread("matching", matching_core);
521 matching_stage.run(&ps)
522 })
523 .expect("spawn matching thread");
524
525 let ps = Arc::clone(&pipeline_shutdown);
528 let drain_core = cores.response;
529 let drain_consumer = pipeline.drain_consumer;
530 let drain_handle = std::thread::Builder::new()
531 .name("drain".into())
532 .spawn(move || {
533 melin_app::affinity::pin_thread("drain", drain_core);
534 let mut consumer = drain_consumer;
535 let mut batch = vec![OutputSlot::<A::Report, A::QueryResponse>::default(); 256];
536 loop {
537 if ps.load(Ordering::Relaxed) {
538 return;
539 }
540 let count = consumer.consume_batch(&mut batch, 256);
541 if count == 0 {
542 if busy_spin {
543 std::hint::spin_loop();
544 } else {
545 std::thread::yield_now();
546 }
547 }
548 }
549 })
550 .expect("spawn drain thread");
551
552 let shadow_handle = if let Some(shadow_cons) = pipeline.shadow_consumer {
553 let snap_path = snapshot_path;
554 let chain_lock = pipeline
555 .chain_hash_lock
556 .as_ref()
557 .expect("chain hash lock with shadow")
558 .clone();
559 let ps = Arc::clone(&pipeline_shutdown);
560 let shadow_core = cores.shadow;
561 Some(
562 std::thread::Builder::new()
563 .name("replica-shadow".into())
564 .spawn(move || {
565 melin_app::affinity::pin_thread("replica-shadow", shadow_core);
566 melin_transport_core::shadow::run(
567 shadow_cons,
568 shadow_exchange,
569 snap_path,
570 std::time::Duration::from_millis(snapshot_interval_ms),
571 chain_lock,
572 &ps,
573 false,
574 shadow_initial_epoch,
575 );
576 })
577 .expect("spawn shadow thread"),
578 )
579 } else {
580 None
581 };
582
583 Ok(ReplicaPipelineHandles {
584 input_producer: pipeline.input_producer,
585 journal_cursor: pipeline.cursors.journal_ring_arc(),
586 last_seq: pipeline.cursors.durable_wire_seq(),
587 chain_hash_lock: pipeline.chain_hash_lock,
588 stream_marks,
589 journal_failed,
590 pipeline_shutdown,
591 journal_handle,
592 matching_handle,
593 drain_handle,
594 shadow_handle,
595 })
596}
597
598pub(super) fn teardown_replica_pipeline<A: Application + Send + 'static, W: Send + 'static>(
603 mut handles: ReplicaPipelineHandles<A, W>,
604) -> TeardownOutcome<A, W> {
605 while !handles.journal_failed.load(Ordering::Acquire) {
617 match handles
618 .input_producer
619 .try_publish(InputSlot::<A::Event>::shutdown_sentinel())
620 {
621 Ok(_) => break,
622 Err(_) => std::thread::yield_now(),
624 }
625 }
626 shutdown_pipeline::<A, W>(
627 &handles.pipeline_shutdown,
628 handles.journal_handle,
629 handles.matching_handle,
630 handles.drain_handle,
631 handles.shadow_handle,
632 )
633}
634
635pub(super) const MAX_INPROCESS_DIVERGENCE_RESYNCS: u32 = 1;
646
647#[allow(clippy::type_complexity)]
664pub(super) fn recover_replica_state<A, W>(
665 journal_path: &std::path::Path,
666 snapshot_path: &std::path::Path,
667 factory: &dyn melin_app::app_factory::AppFactory<App = A>,
668 fence_state: &melin_transport_core::fence::FenceState,
669) -> Result<(Option<A>, Option<W>, u64, [u8; 32]), Box<dyn std::error::Error>>
670where
671 A: Application,
672 W: melin_journal::JournalWrite<A::Event>,
673{
674 let lineage_exists =
675 journal_path.exists() || !melin_journal::segment::list_archives(journal_path)?.is_empty();
676 if !lineage_exists {
677 return Ok((None, None, 0u64, [0u8; 32]));
678 }
679 let engine = if snapshot_path.exists() {
680 tracing::info!("recovering replica from snapshot + journal");
681 melin_transport_core::JournaledApp::<A, W>::recover_from_snapshot(
682 snapshot_path,
683 journal_path,
684 )?
685 } else {
686 melin_transport_core::JournaledApp::<A, W>::recover(factory.empty(), journal_path)?
687 };
688 let next = engine.next_sequence();
689 let last = next.saturating_sub(1);
690 let hash = engine.chain_hash().unwrap_or([0u8; 32]);
691 fence_state.observe_epoch(engine.recovered_epoch());
694 let (mut exchange, writer) = engine.into_parts();
695 factory.apply_operator_policy(&mut exchange);
696 Ok((Some(exchange), Some(writer), last, hash))
697}
698
699pub(super) const MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(30);
705
706pub(in crate::replication) enum AfterSession<A, W> {
710 Return(ReceiverResult<A, W>),
714 Resync {
719 exchange: Option<A>,
720 journal_writer: Option<W>,
721 last_sequence: u64,
722 chain_hash: [u8; 32],
723 },
724 Reconnect,
728}
729
730#[allow(clippy::too_many_arguments)]
751pub(in crate::replication) fn handle_session_exit<A, W>(
752 result: StreamingResult,
753 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
754 divergence_resyncs: &mut u32,
755 backoff: &mut std::time::Duration,
756 last_sequence: u64,
757 journal_path: &std::path::Path,
758 snapshot_path: &std::path::Path,
759 factory: &dyn melin_app::app_factory::AppFactory<App = A>,
760 fence_state: &melin_transport_core::fence::FenceState,
761 shutdown: &AtomicBool,
762 promote: &crate::promotion::PromotionRequest,
763 mut close: impl FnMut(),
764) -> AfterSession<A, W>
765where
766 A: Application + Send + 'static,
767 W: JournalWrite<A::Event> + Send + 'static,
768{
769 let StreamingResult {
770 exit,
771 heard_from_primary,
772 } = result;
773
774 match exit {
775 SessionExit::Shutdown => {
776 if let Some(p) = pipeline.take() {
777 let _ = teardown_replica_pipeline::<A, W>(p);
778 }
779 AfterSession::Return(Ok(None))
780 }
781
782 SessionExit::Promote => AfterSession::Return(match pipeline.take() {
783 Some(p) => match teardown_replica_pipeline::<A, W>(p) {
784 TeardownOutcome::Clean(ex, wr) => Ok(Some((ex, wr))),
785 _ => Err("pipeline failed during promotion".into()),
786 },
787 None => Err("pipeline missing on promote".into()),
788 }),
789
790 SessionExit::Fatal(e) => {
791 let outcome = match pipeline.take() {
792 Some(p) => teardown_replica_pipeline::<A, W>(p),
793 None => return AfterSession::Return(Err(e)),
796 };
797 let TeardownOutcome::JournalFailed(
807 je @ melin_journal::JournalError::ReplicaChainDivergence { .. },
808 ) = outcome
809 else {
810 return AfterSession::Return(Err(e));
811 };
812
813 *divergence_resyncs += 1;
814 let attempt = *divergence_resyncs;
815 if attempt > MAX_INPROCESS_DIVERGENCE_RESYNCS {
816 return AfterSession::Return(Err(format!(
817 "mid-stream chain divergence recurred {attempt} times — giving up on \
818 in-process resync (each cycle archives the local journal and re-seeds \
819 from the primary; recurrence at this rate means the primary keeps \
820 streaming history that forks from what it announces): {je}"
821 )
822 .into()));
823 }
824 tracing::warn!(
825 error = %je,
826 attempt,
827 max_attempts = MAX_INPROCESS_DIVERGENCE_RESYNCS,
828 "mid-stream chain divergence — re-deriving local state for in-process resync"
829 );
830 close();
832 match recover_replica_state::<A, W>(journal_path, snapshot_path, factory, fence_state) {
833 Ok((exchange, journal_writer, seq, hash)) => AfterSession::Resync {
834 exchange,
835 journal_writer,
836 last_sequence: seq,
837 chain_hash: hash,
838 },
839 Err(e) => AfterSession::Return(Err(e)),
840 }
841 }
842
843 SessionExit::StreamGap(e) => {
844 close();
853 *backoff = std::time::Duration::from_secs(1);
854 tracing::warn!(
855 error = %e,
856 last_sequence,
857 "replication stream broke contiguity — reconnecting from the durable position"
858 );
859 sleep_then_double_backoff(backoff, shutdown, promote);
860 AfterSession::Reconnect
861 }
862
863 SessionExit::Disconnected => {
864 close();
867 if heard_from_primary {
876 *backoff = std::time::Duration::from_secs(1);
877 }
878 tracing::warn!(
879 last_sequence,
880 heard_from_primary,
881 backoff_secs = backoff.as_secs(),
882 "reconnecting to primary"
883 );
884 sleep_then_double_backoff(backoff, shutdown, promote);
885 AfterSession::Reconnect
886 }
887 }
888}
889
890pub(in crate::replication) fn take_pipeline_for_promotion<A, W>(
900 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
901 exchange: &mut Option<A>,
902 journal_writer: &mut Option<W>,
903) -> ReceiverResult<A, W>
904where
905 A: Application + Send + 'static,
906 W: JournalWrite<A::Event> + Send + 'static,
907{
908 if let Some(p) = pipeline.take()
909 && let TeardownOutcome::Clean(e, w) = teardown_replica_pipeline::<A, W>(p)
910 {
911 *exchange = Some(e);
912 *journal_writer = Some(w);
913 }
914 match (exchange.take(), journal_writer.take()) {
915 (Some(e), Some(w)) => Ok(Some((e, w))),
916 _ => Err("promotion requested but no local state available".into()),
917 }
918}
919
920type ResyncTransfer<A> = (A, u64, [u8; 32], u64);
923
924pub(in crate::replication) enum ResyncDecision {
927 Ready {
932 segment_start_sequence: u64,
933 anchor_hash: [u8; 32],
934 resume_sequence: u64,
935 },
936 Retry,
940}
941
942fn receive_resync_transfer<A, S>(
952 source: &mut S,
953 snapshot_path: &std::path::Path,
954 journal_path: &std::path::Path,
955 fence_state: &melin_transport_core::fence::FenceState,
956) -> Result<ResyncTransfer<A>, Box<dyn std::error::Error + Send + Sync>>
957where
958 A: Application,
959 S: ControlFrameSource,
960{
961 let (snap_len, snap_sequence, snap_chain_hash) =
962 match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
963 PrimaryMessage::SnapshotBegin {
964 snapshot_len,
965 snap_sequence,
966 snap_chain_hash,
967 } => (snapshot_len, snap_sequence, snap_chain_hash),
968 other => return Err(format!("expected SnapshotBegin, got {other:?}").into()),
969 };
970
971 tracing::info!(snap_sequence, snap_len, "receiving snapshot");
972 let tmp_path = snapshot_path.with_extension("snapshot.tmp");
973 receive_chunked_body(source, &tmp_path, snap_len, "snapshot")?;
974 std::fs::rename(&tmp_path, snapshot_path)?;
975 tracing::info!(snap_sequence, snap_len, "snapshot received and verified");
976
977 let (snap_exchange, _snap_seq, snap_hash, snap_epoch) =
978 melin_transport_core::snapshot::load::<A>(snapshot_path)?;
979 if snap_hash != snap_chain_hash {
980 return Err(format!(
981 "snapshot chain hash mismatch: primary sent {snap_chain_hash:02x?}, \
982 loaded snapshot has {snap_hash:02x?}"
983 )
984 .into());
985 }
986 fence_state.observe_epoch(snap_epoch);
989
990 let seed_len = match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
995 PrimaryMessage::SegmentSeedBegin { seed_len } => seed_len,
996 other => {
997 return Err(format!("expected SegmentSeedBegin after snapshot, got {other:?}").into());
998 }
999 };
1000 let seed_tmp = journal_path.with_extension("seed.tmp");
1001 receive_chunked_body(source, &seed_tmp, seed_len, "segment seed")?;
1002 if let Err(e) =
1008 melin_journal::segment::verify_segment_prefix(&seed_tmp, snap_sequence, seed_len)
1009 {
1010 let _ = std::fs::remove_file(&seed_tmp);
1011 return Err(format!("segment seed failed structural verification: {e}").into());
1012 }
1013 std::fs::rename(&seed_tmp, journal_path)?;
1014 melin_journal::segment::fsync_parent_dir(journal_path)?;
1015 Ok((snap_exchange, snap_sequence, snap_chain_hash, seed_len))
1016}
1017
1018#[allow(clippy::too_many_arguments)]
1031pub(in crate::replication) fn handle_resync_verdict<A, W, S>(
1032 divergent: bool,
1033 source: &mut S,
1034 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
1035 exchange: &mut Option<A>,
1036 journal_writer: &mut Option<W>,
1037 journal_path: &std::path::Path,
1038 snapshot_path: &std::path::Path,
1039 fence_state: &melin_transport_core::fence::FenceState,
1040 control: &ReplicaControlPlane,
1041 last_sequence: &mut u64,
1042 chain_hash: &mut [u8; 32],
1043) -> Result<ResyncDecision, Box<dyn std::error::Error + Send + Sync>>
1044where
1045 A: Application + Send + 'static,
1046 W: JournalWrite<A::Event> + Send + 'static,
1047 S: ControlFrameSource,
1048{
1049 if divergent {
1054 tracing::warn!(
1055 last_sequence = *last_sequence,
1056 "primary reports chain divergence — archiving local journal, resyncing from snapshot"
1057 );
1058 } else {
1059 tracing::info!("primary requires snapshot transfer — receiving snapshot");
1060 }
1061
1062 if let Some(p) = pipeline.take() {
1063 let _ = teardown_replica_pipeline::<A, W>(p);
1064 }
1065
1066 *exchange = None;
1073 *journal_writer = None;
1074
1075 let reason = if divergent {
1079 ArchiveReason::Divergent
1080 } else {
1081 ArchiveReason::Resync
1082 };
1083 archive_local_lineage(journal_path, snapshot_path, reason)?;
1084 *last_sequence = 0;
1086 *chain_hash = [0u8; 32];
1087 control
1092 .journal_tip
1093 .reset(melin_transport_core::WireSeq::new(0));
1094
1095 let (snap_exchange, snap_sequence, snap_chain_hash, seed_len) =
1096 match receive_resync_transfer::<A, S>(source, snapshot_path, journal_path, fence_state) {
1097 Ok(v) => v,
1098 Err(e) => {
1099 tracing::warn!(error = %e, "snapshot transfer failed — retrying");
1100 let _ = std::fs::remove_file(snapshot_path);
1104 return Ok(ResyncDecision::Retry);
1105 }
1106 };
1107 *exchange = Some(snap_exchange);
1108
1109 let writer = W::open_append(journal_path, snap_sequence, seed_len)?;
1116 let seeded_chain = writer.chain_hash().unwrap_or(snap_chain_hash);
1117 if snap_chain_hash != [0u8; 32] && seeded_chain != snap_chain_hash {
1118 return Err(format!(
1119 "segment seed chain at {snap_sequence} disagrees with the transferred snapshot's \
1120 hash — inconsistent primary"
1121 )
1122 .into());
1123 }
1124 let seeded_info = melin_journal::segment::read_header_info(journal_path)?;
1125 *journal_writer = Some(writer);
1126
1127 match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
1130 PrimaryMessage::StreamStart {
1131 start_sequence,
1132 segment_start_sequence,
1133 anchor_hash,
1134 epoch,
1135 ack_policy,
1136 } => {
1137 if segment_start_sequence != seeded_info.starting_sequence
1138 || anchor_hash != seeded_info.anchor_hash
1139 {
1140 return Err(format!(
1141 "post-snapshot StreamStart lineage (start {segment_start_sequence}) \
1142 disagrees with the transferred segment seed (start {}) — inconsistent \
1143 primary",
1144 seeded_info.starting_sequence
1145 )
1146 .into());
1147 }
1148 fence_state.observe_epoch(epoch);
1152 control
1153 .primary_ack_policy
1154 .store(ack_policy, Ordering::Release);
1155 tracing::info!(
1156 start_sequence,
1157 epoch,
1158 ack_policy,
1159 "streaming resumed after snapshot transfer"
1160 );
1161 control
1164 .journal_tip
1165 .reset(melin_transport_core::WireSeq::new(snap_sequence));
1166 Ok(ResyncDecision::Ready {
1167 segment_start_sequence,
1168 anchor_hash,
1169 resume_sequence: snap_sequence,
1170 })
1171 }
1172 other => Err(format!("expected StreamStart after snapshot, got {other:?}").into()),
1173 }
1174}
1175
1176#[cfg(test)]
1177mod tests {
1178 use std::io::Write;
1179 use std::sync::Arc;
1180
1181 use super::auth::{authenticate_replica, authenticate_with_primary};
1182 use super::*;
1183 use counter_server::CounterEvent;
1189 type InputSlot = melin_transport_core::pipeline::InputSlot<CounterEvent>;
1190 use melin_transport_core::replication::protocol::{
1191 MAX_CONTROL_FRAME, MAX_DATA_FRAME, MSG_AUTH_OK, MSG_CHALLENGE_RESPONSE, MSG_SNAPSHOT_BEGIN,
1192 MSG_SNAPSHOT_CHUNK, MSG_SNAPSHOT_END, decode_auth_result, decode_challenge,
1193 decode_challenge_response, decode_primary_message, decode_replica_message, encode_ack,
1194 encode_auth_failed, encode_auth_ok, encode_chain_check, encode_challenge,
1195 encode_challenge_response, encode_handshake, encode_hash_mismatch, encode_heartbeat,
1196 encode_input_batch, encode_need_snapshot, encode_rotate, encode_segment_seed_begin,
1197 encode_snapshot_begin, encode_snapshot_chunk, encode_snapshot_end, encode_stream_start,
1198 read_frame, try_decode_input_batch,
1199 };
1200
1201 fn encode_input_batch_with_seq(end_sequence: u64, buf: &mut Vec<u8>) {
1205 let slot = InputSlot {
1206 connection_id: 0,
1207 key_hash: 0,
1208 request_seq: 0,
1209 sequence: end_sequence,
1210 timestamp_ns: 0,
1211 event: melin_journal::JournalEvent::Tick { now_ns: 0 },
1212 publish_ts: Default::default(),
1213 recv_ts: Default::default(),
1214 };
1215 encode_input_batch(&[slot], buf);
1216 }
1217
1218 #[test]
1219 fn handshake_encode_decode_round_trip() {
1220 let handshake = Handshake {
1221 last_sequence: 42,
1222 chain_hash: [0xAB; 32],
1223 epoch: 9,
1225 };
1226 let mut buf = Vec::new();
1227 encode_handshake(&handshake, &mut buf);
1228
1229 let payload = &buf[4..];
1231 let msg = decode_replica_message(payload).unwrap();
1232 match msg {
1233 ReplicaMessage::Handshake(h) => {
1234 assert_eq!(h.last_sequence, 42);
1235 assert_eq!(h.chain_hash, [0xAB; 32]);
1236 assert_eq!(h.epoch, 9);
1237 }
1238 _ => panic!("expected Handshake"),
1239 }
1240 }
1241
1242 #[test]
1243 fn ack_encode_decode_round_trip() {
1244 let ack = Ack {
1245 acked_sequence: 1000,
1246 in_memory_sequence: 1024,
1247 };
1248 let mut buf = Vec::new();
1249 encode_ack(&ack, &mut buf);
1250
1251 let payload = &buf[4..];
1252 let msg = decode_replica_message(payload).unwrap();
1253 match msg {
1254 ReplicaMessage::Ack(a) => {
1255 assert_eq!(a.acked_sequence, 1000);
1256 assert_eq!(a.in_memory_sequence, 1024);
1257 }
1258 _ => panic!("expected Ack"),
1259 }
1260 }
1261
1262 #[test]
1270 fn ack_wire_byte_pattern() {
1271 let ack = Ack {
1272 acked_sequence: 0xDEAD_BEEF_CAFE_F00D,
1273 in_memory_sequence: 0x1122_3344_5566_7788,
1274 };
1275 let mut buf = Vec::new();
1276 encode_ack(&ack, &mut buf);
1277 let expected: &[u8] = &[
1280 0x11, 0x00, 0x00, 0x00, 0x02, 0x0D, 0xF0, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, ];
1285 assert_eq!(buf.as_slice(), expected, "Ack wire layout drifted");
1286 }
1287
1288 #[test]
1289 fn stream_start_encode_decode_round_trip() {
1290 let mut buf = Vec::new();
1291 encode_stream_start(99, 42, [0xAA; 32], 5, 2, &mut buf);
1293
1294 let payload = &buf[4..];
1295 let msg = decode_primary_message(payload).unwrap();
1296 match msg {
1297 PrimaryMessage::StreamStart {
1298 start_sequence,
1299 segment_start_sequence,
1300 anchor_hash,
1301 epoch,
1302 ack_policy,
1303 } => {
1304 assert_eq!(start_sequence, 99);
1305 assert_eq!(segment_start_sequence, 42);
1306 assert_eq!(anchor_hash, [0xAA; 32]);
1307 assert_eq!(ack_policy, 2);
1308 assert_eq!(epoch, 5);
1309 }
1310 _ => panic!("expected StreamStart"),
1311 }
1312 }
1313
1314 #[test]
1315 fn heartbeat_encode_decode_round_trip() {
1316 let mut buf = Vec::new();
1317 encode_heartbeat(123, 2, &mut buf);
1318
1319 let payload = &buf[4..];
1320 let msg = decode_primary_message(payload).unwrap();
1321 match msg {
1322 PrimaryMessage::Heartbeat {
1323 sequence,
1324 ack_policy,
1325 } => {
1326 assert_eq!(sequence, 123);
1327 assert_eq!(ack_policy, 2);
1328 }
1329 _ => panic!("expected Heartbeat"),
1330 }
1331 }
1332
1333 #[test]
1334 fn need_snapshot_encode_decode_round_trip() {
1335 let mut buf = Vec::new();
1336 encode_need_snapshot(&mut buf);
1337
1338 let payload = &buf[4..];
1339 let msg = decode_primary_message(payload).unwrap();
1340 assert!(matches!(msg, PrimaryMessage::NeedSnapshot));
1341 }
1342
1343 #[test]
1344 fn hash_mismatch_encode_decode_round_trip() {
1345 let mut buf = Vec::new();
1346 encode_hash_mismatch(&mut buf);
1347
1348 let payload = &buf[4..];
1349 let msg = decode_primary_message(payload).unwrap();
1350 assert!(matches!(msg, PrimaryMessage::HashMismatch));
1351 }
1352
1353 #[test]
1354 fn rotate_encode_decode_round_trip() {
1355 let mut buf = Vec::new();
1356 encode_rotate(7_000_000, &[0xBB; 32], &mut buf);
1357
1358 let payload = &buf[4..];
1359 let msg = decode_primary_message(payload).unwrap();
1360 match msg {
1361 PrimaryMessage::Rotate {
1362 boundary_seq,
1363 tail_hash,
1364 } => {
1365 assert_eq!(boundary_seq, 7_000_000);
1366 assert_eq!(tail_hash, [0xBB; 32]);
1367 }
1368 _ => panic!("expected Rotate"),
1369 }
1370 }
1371
1372 #[test]
1373 fn chain_check_encode_decode_round_trip() {
1374 let mut buf = Vec::new();
1375 encode_chain_check(555, &[0xCC; 32], &mut buf);
1376
1377 let payload = &buf[4..];
1378 let msg = decode_primary_message(payload).unwrap();
1379 match msg {
1380 PrimaryMessage::ChainCheck {
1381 sequence,
1382 chain_hash,
1383 } => {
1384 assert_eq!(sequence, 555);
1385 assert_eq!(chain_hash, [0xCC; 32]);
1386 }
1387 _ => panic!("expected ChainCheck"),
1388 }
1389 }
1390
1391 #[test]
1395 fn rotate_wire_byte_pattern() {
1396 let mut hash = [0u8; 32];
1397 for (i, b) in hash.iter_mut().enumerate() {
1398 *b = i as u8;
1399 }
1400 let mut buf = Vec::new();
1401 encode_rotate(0xDEAD_BEEF_CAFE_F00D, &hash, &mut buf);
1402 let mut expected = vec![
1405 0x29, 0x00, 0x00, 0x00, 0x16, 0x0D, 0xF0, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE, ];
1409 expected.extend_from_slice(&hash);
1410 assert_eq!(
1411 buf.as_slice(),
1412 expected.as_slice(),
1413 "Rotate wire layout drifted"
1414 );
1415 }
1416
1417 #[test]
1418 fn segment_seed_begin_encode_decode_round_trip() {
1419 let mut buf = Vec::new();
1420 encode_segment_seed_begin(123_456, &mut buf);
1421
1422 let payload = &buf[4..];
1423 let msg = decode_primary_message(payload).unwrap();
1424 match msg {
1425 PrimaryMessage::SegmentSeedBegin { seed_len } => {
1426 assert_eq!(seed_len, 123_456);
1427 }
1428 _ => panic!("expected SegmentSeedBegin"),
1429 }
1430 }
1431
1432 #[test]
1433 fn unknown_replica_message_type_is_error() {
1434 let payload = [0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
1435 let result = decode_replica_message(&payload);
1436 assert!(result.is_err());
1437 }
1438
1439 #[test]
1440 fn unknown_primary_message_type_is_error() {
1441 let payload = [0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
1442 let result = decode_primary_message(&payload);
1443 assert!(result.is_err());
1444 }
1445
1446 #[test]
1447 fn read_frame_enforces_max_size() {
1448 let mut buf = Vec::new();
1450 buf.extend_from_slice(&1000u32.to_le_bytes());
1451 buf.extend_from_slice(&[0u8; 100]); let mut cursor = std::io::Cursor::new(buf);
1454 let result = read_frame(&mut cursor, 64);
1455 assert!(result.is_err());
1456 }
1457
1458 #[test]
1459 fn challenge_encode_decode_round_trip() {
1460 let nonce = [0x42; 32];
1461 let mut buf = Vec::new();
1462 encode_challenge(&nonce, &mut buf);
1463
1464 let payload = &buf[4..];
1465 let decoded = decode_challenge(payload).unwrap();
1466 assert_eq!(decoded, nonce);
1467 }
1468
1469 #[test]
1470 fn challenge_response_encode_decode_round_trip() {
1471 let sig = [0xAA; 64];
1472 let pubkey = [0xBB; 32];
1473 let mut buf = Vec::new();
1474 encode_challenge_response(&sig, &pubkey, &mut buf);
1475
1476 let payload = &buf[4..];
1477 let (decoded_sig, decoded_pubkey) = decode_challenge_response(payload).unwrap();
1478 assert_eq!(decoded_sig, sig);
1479 assert_eq!(decoded_pubkey, pubkey);
1480 }
1481
1482 #[test]
1483 fn auth_ok_encode_decode_round_trip() {
1484 let mut buf = Vec::new();
1485 encode_auth_ok(&mut buf);
1486
1487 let payload = &buf[4..];
1488 assert!(decode_auth_result(payload).unwrap());
1489 }
1490
1491 #[test]
1492 fn auth_failed_encode_decode_round_trip() {
1493 let mut buf = Vec::new();
1494 encode_auth_failed(&mut buf);
1495
1496 let payload = &buf[4..];
1497 assert!(!decode_auth_result(payload).unwrap());
1498 }
1499
1500 #[test]
1501 fn decode_challenge_rejects_wrong_tag() {
1502 let mut payload = [0u8; 33];
1503 payload[0] = MSG_AUTH_OK;
1504 assert!(decode_challenge(&payload).is_err());
1505 }
1506
1507 #[test]
1508 fn decode_challenge_response_rejects_short_payload() {
1509 let payload = [MSG_CHALLENGE_RESPONSE; 10]; assert!(decode_challenge_response(&payload).is_err());
1511 }
1512
1513 #[test]
1514 fn auth_round_trip_valid_key() {
1515 use ed25519_dalek::SigningKey;
1516 use std::os::unix::net::UnixStream;
1517
1518 let repl_key = SigningKey::from_bytes(&[0xFC; 32]);
1519 let pub_b64 = base64::Engine::encode(
1520 &base64::engine::general_purpose::STANDARD,
1521 repl_key.verifying_key().to_bytes(),
1522 );
1523 let keys_content = format!("replication {pub_b64} test-replica\n");
1524 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1525
1526 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1527 primary_stream
1528 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1529 .unwrap();
1530 replica_stream
1531 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1532 .unwrap();
1533
1534 let repl_key_clone = SigningKey::from_bytes(&[0xFC; 32]);
1535 let replica_handle = std::thread::spawn(move || {
1536 let mut conn = replica_stream;
1537 authenticate_with_primary(&mut conn, &repl_key_clone)
1538 });
1539
1540 let mut conn = primary_stream;
1541 authenticate_replica(&mut conn, &authorized_keys).unwrap();
1542
1543 replica_handle.join().unwrap().unwrap();
1544 }
1545
1546 #[test]
1547 fn auth_rejects_unknown_key() {
1548 use ed25519_dalek::SigningKey;
1549 use std::os::unix::net::UnixStream;
1550
1551 let authorized_key = SigningKey::from_bytes(&[0xAA; 32]);
1553 let rogue_key = SigningKey::from_bytes(&[0xBB; 32]);
1554 let pub_b64 = base64::Engine::encode(
1555 &base64::engine::general_purpose::STANDARD,
1556 authorized_key.verifying_key().to_bytes(),
1557 );
1558 let keys_content = format!("replication {pub_b64} authorized-replica\n");
1559 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1560
1561 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1562 primary_stream
1563 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1564 .unwrap();
1565 replica_stream
1566 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1567 .unwrap();
1568
1569 let replica_handle = std::thread::spawn(move || {
1570 let mut conn = replica_stream;
1571 authenticate_with_primary(&mut conn, &rogue_key)
1572 });
1573
1574 let mut conn = primary_stream;
1575 let result = authenticate_replica(&mut conn, &authorized_keys);
1576 assert!(result.is_err());
1577 assert!(result.unwrap_err().to_string().contains("unknown"));
1578
1579 let replica_result = replica_handle.join().unwrap();
1581 assert!(replica_result.is_err());
1582 }
1583
1584 #[test]
1585 fn auth_rejects_wrong_permission() {
1586 use ed25519_dalek::SigningKey;
1587 use std::os::unix::net::UnixStream;
1588
1589 let key = SigningKey::from_bytes(&[0xCC; 32]);
1591 let pub_b64 = base64::Engine::encode(
1592 &base64::engine::general_purpose::STANDARD,
1593 key.verifying_key().to_bytes(),
1594 );
1595 let keys_content = format!("trader {pub_b64} wrong-role\n");
1596 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1597
1598 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1599 primary_stream
1600 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1601 .unwrap();
1602 replica_stream
1603 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1604 .unwrap();
1605
1606 let replica_handle = std::thread::spawn(move || {
1607 let mut conn = replica_stream;
1608 authenticate_with_primary(&mut conn, &key)
1609 });
1610
1611 let mut conn = primary_stream;
1612 let result = authenticate_replica(&mut conn, &authorized_keys);
1613 assert!(result.is_err());
1614 assert!(result.unwrap_err().to_string().contains("Replication"));
1615
1616 let replica_result = replica_handle.join().unwrap();
1617 assert!(replica_result.is_err());
1618 }
1619
1620 #[test]
1623 fn auth_rejects_invalid_signature() {
1624 use ed25519_dalek::SigningKey;
1625 use std::os::unix::net::UnixStream;
1626
1627 let correct_key = SigningKey::from_bytes(&[0xDD; 32]);
1629 let pub_b64 = base64::Engine::encode(
1630 &base64::engine::general_purpose::STANDARD,
1631 correct_key.verifying_key().to_bytes(),
1632 );
1633 let keys_content = format!("replication {pub_b64} test-replica\n");
1634 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1635
1636 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1637 primary_stream
1638 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1639 .unwrap();
1640 replica_stream
1641 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1642 .unwrap();
1643
1644 let replica_handle = std::thread::spawn(move || {
1647 use melin_transport_core::replication::protocol::*;
1648
1649 let mut reader = replica_stream.try_clone().unwrap();
1650 let mut writer = replica_stream;
1651
1652 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1654 let nonce = decode_challenge(&frame).unwrap();
1655
1656 let wrong_key = SigningKey::from_bytes(&[0xEE; 32]);
1658 let bad_signature = ed25519_dalek::Signer::sign(&wrong_key, &nonce);
1659 let correct_pubkey = correct_key.verifying_key();
1660
1661 let mut buf = Vec::with_capacity(128);
1662 encode_challenge_response(
1663 &bad_signature.to_bytes(),
1664 correct_pubkey.as_bytes(),
1665 &mut buf,
1666 );
1667 writer.write_all(&buf).unwrap();
1668 writer.flush().unwrap();
1669
1670 let result_frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1672 let ok = decode_auth_result(&result_frame).unwrap();
1673 assert!(!ok, "should receive auth failure");
1674 });
1675
1676 let mut conn = primary_stream;
1677 let result = authenticate_replica(&mut conn, &authorized_keys);
1678 assert!(result.is_err());
1679 assert!(
1680 result
1681 .unwrap_err()
1682 .to_string()
1683 .contains("signature verification failed"),
1684 "should fail on signature verification"
1685 );
1686
1687 replica_handle.join().unwrap();
1688 }
1689
1690 #[test]
1691 fn sender_receiver_end_to_end() {
1692 use std::os::unix::net::UnixStream;
1693
1694 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1696
1697 let replica_handle = std::thread::spawn(move || {
1699 let mut reader = replica_stream.try_clone().unwrap();
1700 let mut writer = replica_stream;
1701
1702 let mut buf = Vec::new();
1704 let handshake = Handshake {
1705 last_sequence: 0,
1706 chain_hash: [0u8; 32],
1707 epoch: 0,
1708 };
1709 encode_handshake(&handshake, &mut buf);
1710 writer.write_all(&buf).unwrap();
1711 writer.flush().unwrap();
1712 buf.clear();
1713
1714 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1716 let msg = decode_primary_message(&frame).unwrap();
1717 assert!(matches!(msg, PrimaryMessage::StreamStart { .. }));
1718
1719 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1721 let slots: Vec<InputSlot> = try_decode_input_batch(&frame).expect("decode InputBatch");
1722 let end_seq = slots
1723 .last()
1724 .map(|s| s.sequence)
1725 .expect("InputBatch carried at least one slot");
1726
1727 let ack = Ack {
1729 acked_sequence: end_seq,
1730 in_memory_sequence: end_seq,
1731 };
1732 encode_ack(&ack, &mut buf);
1733 writer.write_all(&buf).unwrap();
1734 writer.flush().unwrap();
1735
1736 end_seq
1737 });
1738
1739 let mut p_reader = primary_stream.try_clone().unwrap();
1741 let mut p_writer = primary_stream;
1742
1743 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1745 let handshake = match decode_replica_message(&frame).unwrap() {
1746 ReplicaMessage::Handshake(h) => h,
1747 _ => panic!("expected Handshake"),
1748 };
1749 assert_eq!(handshake.last_sequence, 0);
1750
1751 let mut buf = Vec::new();
1753 encode_stream_start(0, 1, [0u8; 32], 0, 1, &mut buf); p_writer.write_all(&buf).unwrap();
1755 p_writer.flush().unwrap();
1756 buf.clear();
1757
1758 encode_input_batch_with_seq(42, &mut buf);
1760 p_writer.write_all(&buf).unwrap();
1761 p_writer.flush().unwrap();
1762 buf.clear();
1763
1764 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1766 let ack = match decode_replica_message(&frame).unwrap() {
1767 ReplicaMessage::Ack(a) => a,
1768 _ => panic!("expected Ack"),
1769 };
1770 assert_eq!(ack.acked_sequence, 42);
1771
1772 let end_seq = replica_handle.join().unwrap();
1774 assert_eq!(end_seq, 42);
1775 }
1776
1777 #[test]
1778 fn multiple_data_batches_acked_in_order() {
1779 use std::os::unix::net::UnixStream;
1782
1783 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1784
1785 let replica_handle = std::thread::spawn(move || {
1786 let mut reader = replica_stream.try_clone().unwrap();
1787 let mut writer = replica_stream;
1788 let mut buf = Vec::new();
1789
1790 encode_handshake(
1792 &Handshake {
1793 last_sequence: 0,
1794 chain_hash: [0u8; 32],
1795 epoch: 0,
1796 },
1797 &mut buf,
1798 );
1799 writer.write_all(&buf).unwrap();
1800 writer.flush().unwrap();
1801 buf.clear();
1802
1803 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1805 assert!(matches!(
1806 decode_primary_message(&frame).unwrap(),
1807 PrimaryMessage::StreamStart { .. }
1808 ));
1809
1810 let mut acked_seqs = Vec::new();
1812 for _ in 0..3 {
1813 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1814 let slots: Vec<InputSlot> =
1815 try_decode_input_batch(&frame).expect("decode InputBatch");
1816 let end_seq = slots
1817 .last()
1818 .map(|s| s.sequence)
1819 .expect("InputBatch carried at least one slot");
1820 acked_seqs.push(end_seq);
1821
1822 encode_ack(
1823 &Ack {
1824 acked_sequence: end_seq,
1825 in_memory_sequence: end_seq,
1826 },
1827 &mut buf,
1828 );
1829 writer.write_all(&buf).unwrap();
1830 writer.flush().unwrap();
1831 buf.clear();
1832 }
1833
1834 acked_seqs
1835 });
1836
1837 let mut p_reader = primary_stream.try_clone().unwrap();
1839 let mut p_writer = primary_stream;
1840 let mut buf = Vec::new();
1841
1842 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1844 assert!(matches!(
1845 decode_replica_message(&frame).unwrap(),
1846 ReplicaMessage::Handshake(_)
1847 ));
1848
1849 encode_stream_start(0, 1, [0u8; 32], 0, 1, &mut buf);
1851 p_writer.write_all(&buf).unwrap();
1852 p_writer.flush().unwrap();
1853 buf.clear();
1854
1855 for seq in [10u64, 20, 30] {
1857 encode_input_batch_with_seq(seq, &mut buf);
1858 p_writer.write_all(&buf).unwrap();
1859 p_writer.flush().unwrap();
1860 buf.clear();
1861 }
1862
1863 for expected_seq in [10u64, 20, 30] {
1865 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1866 let ack = match decode_replica_message(&frame).unwrap() {
1867 ReplicaMessage::Ack(a) => a,
1868 other => panic!("expected Ack, got {other:?}"),
1869 };
1870 assert_eq!(ack.acked_sequence, expected_seq);
1871 }
1872
1873 let acked = replica_handle.join().unwrap();
1874 assert_eq!(acked, vec![10, 20, 30]);
1875 }
1876
1877 #[test]
1878 fn heartbeat_encode_contains_sequence() {
1879 let mut buf = Vec::new();
1882 encode_heartbeat(999, 1, &mut buf);
1883
1884 let payload = &buf[4..];
1885 match decode_primary_message(payload).unwrap() {
1886 PrimaryMessage::Heartbeat { sequence, .. } => {
1887 assert_eq!(sequence, 999);
1888 }
1889 other => panic!("expected Heartbeat, got {other:?}"),
1890 }
1891 }
1892
1893 #[test]
1894 fn replica_mid_stream_handshake_with_nonzero_sequence() {
1895 use std::os::unix::net::UnixStream;
1900
1901 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1902
1903 let replica_handle = std::thread::spawn(move || {
1904 let mut reader = replica_stream.try_clone().unwrap();
1905 let mut writer = replica_stream;
1906 let mut buf = Vec::new();
1907
1908 encode_handshake(
1910 &Handshake {
1911 last_sequence: 100,
1912 chain_hash: [0xBB; 32],
1913 epoch: 0,
1914 },
1915 &mut buf,
1916 );
1917 writer.write_all(&buf).unwrap();
1918 writer.flush().unwrap();
1919 buf.clear();
1920
1921 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1923 match decode_primary_message(&frame).unwrap() {
1924 PrimaryMessage::StreamStart { start_sequence, .. } => {
1925 assert_eq!(
1926 start_sequence, 100,
1927 "StreamStart should echo replica's last_sequence"
1928 );
1929 }
1930 other => panic!("expected StreamStart, got {other:?}"),
1931 }
1932
1933 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1935 let slots: Vec<InputSlot> = try_decode_input_batch(&frame).expect("decode InputBatch");
1936 let end_sequence = slots
1937 .last()
1938 .map(|s| s.sequence)
1939 .expect("InputBatch carried at least one slot");
1940 assert!(
1941 end_sequence > 100,
1942 "InputBatch should be after replica's last_sequence"
1943 );
1944 });
1945
1946 let mut p_reader = primary_stream.try_clone().unwrap();
1948 let mut p_writer = primary_stream;
1949 let mut buf = Vec::new();
1950
1951 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1953 let handshake = match decode_replica_message(&frame).unwrap() {
1954 ReplicaMessage::Handshake(h) => h,
1955 _ => panic!("expected Handshake"),
1956 };
1957 assert_eq!(handshake.last_sequence, 100);
1958 assert_eq!(handshake.chain_hash, [0xBB; 32]);
1959
1960 encode_stream_start(handshake.last_sequence, 1, [0u8; 32], 0, 1, &mut buf);
1962 p_writer.write_all(&buf).unwrap();
1963 p_writer.flush().unwrap();
1964 buf.clear();
1965
1966 encode_input_batch_with_seq(150, &mut buf);
1968 p_writer.write_all(&buf).unwrap();
1969 p_writer.flush().unwrap();
1970
1971 replica_handle.join().unwrap();
1972 }
1973
1974 #[test]
1989 fn receiver_refuses_stream_start_from_stale_primary() {
1990 use std::os::unix::net::UnixStream;
1991
1992 use melin_transport_core::fence::FenceState;
1993
1994 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1995
1996 const STALE_EPOCH: u64 = 3;
2000 let primary_handle = std::thread::spawn(move || {
2001 let mut reader = primary_stream.try_clone().unwrap();
2002 let mut writer = primary_stream;
2003
2004 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2005 let handshake = match decode_replica_message(&frame).unwrap() {
2006 ReplicaMessage::Handshake(h) => h,
2007 _ => panic!("expected Handshake"),
2008 };
2009 assert_eq!(handshake.epoch, 5, "replica advertises its real epoch");
2012
2013 let mut buf = Vec::new();
2014 encode_stream_start(
2015 handshake.last_sequence,
2016 1,
2017 [0u8; 32],
2018 STALE_EPOCH,
2019 1,
2020 &mut buf,
2021 );
2022 writer.write_all(&buf).unwrap();
2023 writer.flush().unwrap();
2024 });
2025
2026 let fence_state = Arc::new(FenceState::new(5));
2029 let mut r_reader = replica_stream.try_clone().unwrap();
2030 let mut r_writer = replica_stream;
2031
2032 let mut buf = Vec::new();
2033 encode_handshake(
2034 &Handshake {
2035 last_sequence: 0,
2036 chain_hash: [0u8; 32],
2037 epoch: fence_state.epoch(),
2038 },
2039 &mut buf,
2040 );
2041 r_writer.write_all(&buf).unwrap();
2042 r_writer.flush().unwrap();
2043
2044 let frame = read_frame(&mut r_reader, MAX_CONTROL_FRAME).unwrap();
2045 let stream_epoch = match decode_primary_message(&frame).unwrap() {
2046 PrimaryMessage::StreamStart { epoch, .. } => epoch,
2047 other => panic!("expected StreamStart, got {other:?}"),
2048 };
2049 assert_eq!(stream_epoch, STALE_EPOCH, "epoch must survive the wire");
2050
2051 assert!(
2054 fence_state.refuses_primary(stream_epoch),
2055 "replica at epoch 5 must refuse a StreamStart from epoch {STALE_EPOCH}"
2056 );
2057 assert_eq!(fence_state.epoch(), 5, "refusal must not lower our epoch");
2058
2059 assert!(!fence_state.refuses_primary(5), "same tenure is followed");
2062 assert!(!fence_state.refuses_primary(6), "newer primary is followed");
2063
2064 primary_handle.join().unwrap();
2065 }
2066
2067 #[test]
2068 fn snapshot_begin_encode_decode_round_trip() {
2069 let mut buf = Vec::new();
2070 encode_snapshot_begin(1_000_000, 42, &[0xAB; 32], &mut buf);
2071
2072 let payload = &buf[4..];
2073 let msg = decode_primary_message(payload).unwrap();
2074 match msg {
2075 PrimaryMessage::SnapshotBegin {
2076 snapshot_len,
2077 snap_sequence,
2078 snap_chain_hash,
2079 } => {
2080 assert_eq!(snapshot_len, 1_000_000);
2081 assert_eq!(snap_sequence, 42);
2082 assert_eq!(snap_chain_hash, [0xAB; 32]);
2083 }
2084 _ => panic!("expected SnapshotBegin"),
2085 }
2086 }
2087
2088 #[test]
2089 fn snapshot_chunk_encode_decode_round_trip() {
2090 let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
2091 let mut buf = Vec::new();
2092 encode_snapshot_chunk(&data, &mut buf);
2093
2094 let payload = &buf[4..];
2095 let msg = decode_primary_message(payload).unwrap();
2096 match msg {
2097 PrimaryMessage::SnapshotChunk(chunk) => {
2098 assert_eq!(chunk, data);
2099 }
2100 _ => panic!("expected SnapshotChunk"),
2101 }
2102 }
2103
2104 #[test]
2105 fn snapshot_end_encode_decode_round_trip() {
2106 let mut buf = Vec::new();
2107 encode_snapshot_end(0xDEADBEEF, &mut buf);
2108
2109 let payload = &buf[4..];
2110 let msg = decode_primary_message(payload).unwrap();
2111 match msg {
2112 PrimaryMessage::SnapshotEnd { crc32c } => {
2113 assert_eq!(crc32c, 0xDEADBEEF);
2114 }
2115 _ => panic!("expected SnapshotEnd"),
2116 }
2117 }
2118
2119 #[test]
2123 fn snapshot_receiver_detects_length_mismatch() {
2124 use std::os::unix::net::UnixStream;
2125
2126 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2127
2128 let receiver = std::thread::spawn(move || -> String {
2130 let mut reader = replica_stream.try_clone().unwrap();
2131
2132 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2134 assert!(matches!(
2135 decode_primary_message(&frame).unwrap(),
2136 PrimaryMessage::NeedSnapshot,
2137 ));
2138
2139 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2141 let (snap_len, _snap_sequence, _snap_chain_hash) =
2142 match decode_primary_message(&frame).unwrap() {
2143 PrimaryMessage::SnapshotBegin {
2144 snapshot_len,
2145 snap_sequence,
2146 snap_chain_hash,
2147 } => (snapshot_len, snap_sequence, snap_chain_hash),
2148 other => panic!("expected SnapshotBegin, got {other:?}"),
2149 };
2150
2151 let mut received: u64 = 0;
2153 loop {
2154 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
2155 match decode_primary_message(&frame).unwrap() {
2156 PrimaryMessage::SnapshotChunk(data) => {
2157 received += data.len() as u64;
2158 }
2159 PrimaryMessage::SnapshotEnd { .. } => {
2160 if received != snap_len {
2161 return format!(
2162 "snapshot length mismatch: expected {snap_len} bytes, got {received}"
2163 );
2164 }
2165 return String::new(); }
2167 other => panic!("unexpected message: {other:?}"),
2168 }
2169 }
2170 });
2171
2172 let mut writer = primary_stream;
2174 let mut buf = Vec::new();
2175
2176 let actual_data = vec![0xAA; 100];
2177 let wrong_len = 999u64; encode_need_snapshot(&mut buf);
2180 std::io::Write::write_all(&mut writer, &buf).unwrap();
2181 buf.clear();
2182
2183 encode_snapshot_begin(wrong_len, 42, &[0xBB; 32], &mut buf);
2184 std::io::Write::write_all(&mut writer, &buf).unwrap();
2185 buf.clear();
2186
2187 encode_snapshot_chunk(&actual_data, &mut buf);
2188 std::io::Write::write_all(&mut writer, &buf).unwrap();
2189 buf.clear();
2190
2191 let crc = crc32c::crc32c(&actual_data);
2192 encode_snapshot_end(crc, &mut buf);
2193 std::io::Write::write_all(&mut writer, &buf).unwrap();
2194 std::io::Write::flush(&mut writer).unwrap();
2195
2196 let error_msg = receiver.join().unwrap();
2197 assert!(
2198 error_msg.contains("length mismatch"),
2199 "expected length mismatch error, got: {error_msg:?}"
2200 );
2201 }
2202
2203 #[test]
2207 fn snapshot_receiver_detects_crc_mismatch() {
2208 use std::os::unix::net::UnixStream;
2209
2210 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2211
2212 let receiver = std::thread::spawn(move || -> String {
2213 let mut reader = replica_stream.try_clone().unwrap();
2214
2215 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2217 assert!(matches!(
2218 decode_primary_message(&frame).unwrap(),
2219 PrimaryMessage::NeedSnapshot,
2220 ));
2221
2222 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2224 let snap_len = match decode_primary_message(&frame).unwrap() {
2225 PrimaryMessage::SnapshotBegin { snapshot_len, .. } => snapshot_len,
2226 other => panic!("expected SnapshotBegin, got {other:?}"),
2227 };
2228
2229 let mut received_data = Vec::new();
2231 let mut received: u64 = 0;
2232 loop {
2233 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
2234 match decode_primary_message(&frame).unwrap() {
2235 PrimaryMessage::SnapshotChunk(data) => {
2236 received += data.len() as u64;
2237 received_data.extend_from_slice(&data);
2238 }
2239 PrimaryMessage::SnapshotEnd {
2240 crc32c: expected_crc,
2241 } => {
2242 if received != snap_len {
2243 return format!("length mismatch: {snap_len} vs {received}");
2244 }
2245 let actual_crc = crc32c::crc32c(&received_data);
2246 if actual_crc != expected_crc {
2247 return format!(
2248 "CRC mismatch: expected {expected_crc:#x}, got {actual_crc:#x}"
2249 );
2250 }
2251 return String::new();
2252 }
2253 other => panic!("unexpected message: {other:?}"),
2254 }
2255 }
2256 });
2257
2258 let mut writer = primary_stream;
2260 let mut buf = Vec::new();
2261
2262 let data = vec![0xAA; 100];
2263
2264 encode_need_snapshot(&mut buf);
2265 std::io::Write::write_all(&mut writer, &buf).unwrap();
2266 buf.clear();
2267
2268 encode_snapshot_begin(data.len() as u64, 42, &[0xBB; 32], &mut buf);
2269 std::io::Write::write_all(&mut writer, &buf).unwrap();
2270 buf.clear();
2271
2272 encode_snapshot_chunk(&data, &mut buf);
2273 std::io::Write::write_all(&mut writer, &buf).unwrap();
2274 buf.clear();
2275
2276 let wrong_crc = !crc32c::crc32c(&data);
2278 encode_snapshot_end(wrong_crc, &mut buf);
2279 std::io::Write::write_all(&mut writer, &buf).unwrap();
2280 std::io::Write::flush(&mut writer).unwrap();
2281
2282 let error_msg = receiver.join().unwrap();
2283 assert!(
2284 error_msg.contains("CRC mismatch"),
2285 "expected CRC mismatch error, got: {error_msg:?}"
2286 );
2287 }
2288
2289 #[test]
2292 fn snapshot_receiver_detects_chain_hash_mismatch() {
2293 use std::os::unix::net::UnixStream;
2294
2295 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2296
2297 let receiver = std::thread::spawn(move || -> String {
2298 let mut reader = replica_stream.try_clone().unwrap();
2299
2300 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2301 assert!(matches!(
2302 decode_primary_message(&frame).unwrap(),
2303 PrimaryMessage::NeedSnapshot,
2304 ));
2305
2306 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2307 let (snap_len, _snap_sequence, snap_chain_hash) =
2308 match decode_primary_message(&frame).unwrap() {
2309 PrimaryMessage::SnapshotBegin {
2310 snapshot_len,
2311 snap_sequence,
2312 snap_chain_hash,
2313 } => (snapshot_len, snap_sequence, snap_chain_hash),
2314 other => panic!("expected SnapshotBegin, got {other:?}"),
2315 };
2316
2317 let mut received_data = Vec::new();
2319 let mut received: u64 = 0;
2320 loop {
2321 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
2322 match decode_primary_message(&frame).unwrap() {
2323 PrimaryMessage::SnapshotChunk(data) => {
2324 received += data.len() as u64;
2325 received_data.extend_from_slice(&data);
2326 }
2327 PrimaryMessage::SnapshotEnd {
2328 crc32c: expected_crc,
2329 } => {
2330 assert_eq!(received, snap_len, "length should match");
2331 let actual_crc = crc32c::crc32c(&received_data);
2332 assert_eq!(actual_crc, expected_crc, "CRC should match");
2333 break;
2334 }
2335 other => panic!("unexpected message: {other:?}"),
2336 }
2337 }
2338
2339 let loaded_hash = [0xFF; 32]; if loaded_hash != snap_chain_hash {
2343 return format!(
2344 "snapshot chain hash mismatch: primary sent {snap_chain_hash:02x?}, \
2345 loaded snapshot has {loaded_hash:02x?}"
2346 );
2347 }
2348 String::new()
2349 });
2350
2351 let mut writer = primary_stream;
2354 let mut buf = Vec::new();
2355
2356 let data = vec![0xAA; 64];
2357 let advertised_hash = [0xBB; 32];
2359
2360 encode_need_snapshot(&mut buf);
2361 std::io::Write::write_all(&mut writer, &buf).unwrap();
2362 buf.clear();
2363
2364 encode_snapshot_begin(data.len() as u64, 10, &advertised_hash, &mut buf);
2365 std::io::Write::write_all(&mut writer, &buf).unwrap();
2366 buf.clear();
2367
2368 encode_snapshot_chunk(&data, &mut buf);
2369 std::io::Write::write_all(&mut writer, &buf).unwrap();
2370 buf.clear();
2371
2372 let crc = crc32c::crc32c(&data);
2373 encode_snapshot_end(crc, &mut buf);
2374 std::io::Write::write_all(&mut writer, &buf).unwrap();
2375 std::io::Write::flush(&mut writer).unwrap();
2376
2377 let error_msg = receiver.join().unwrap();
2378 assert!(
2379 error_msg.contains("chain hash mismatch"),
2380 "expected chain hash mismatch error, got: {error_msg:?}"
2381 );
2382 }
2383
2384 #[test]
2387 fn primary_rejects_snapshot_with_invalid_magic() {
2388 let tmp = tempfile::tempdir().unwrap();
2389 let snap_path = tmp.path().join("test.snapshot");
2390
2391 let mut bad_snap = vec![0u8; 64];
2393 bad_snap[0..4].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes()); std::fs::write(&snap_path, &bad_snap).unwrap();
2396
2397 let snap_data = std::fs::read(&snap_path).unwrap();
2399 assert!(
2400 snap_data.len() >= 48,
2401 "file should be big enough for header"
2402 );
2403
2404 let magic = u32::from_le_bytes(snap_data[0..4].try_into().unwrap());
2405 assert_ne!(magic, 0x534E_4150);
2406 assert_eq!(magic, 0xDEAD_BEEF);
2407 }
2408
2409 #[test]
2412 fn primary_rejects_snapshot_too_small_for_header() {
2413 let tmp = tempfile::tempdir().unwrap();
2414 let snap_path = tmp.path().join("test.snapshot");
2415
2416 std::fs::write(&snap_path, [0u8; 20]).unwrap();
2418
2419 let snap_data = std::fs::read(&snap_path).unwrap();
2420 assert!(
2421 snap_data.len() < 48,
2422 "file must be too small for header validation"
2423 );
2424 }
2425
2426 #[test]
2427 fn decode_snapshot_begin_too_short() {
2428 let payload = [MSG_SNAPSHOT_BEGIN, 0x01, 0x02, 0x03];
2431 let err = decode_primary_message(&payload).unwrap_err();
2432 assert!(
2433 err.to_string().contains("SnapshotBegin too short"),
2434 "expected 'SnapshotBegin too short', got: {err}"
2435 );
2436 }
2437
2438 #[test]
2439 fn decode_snapshot_end_too_short() {
2440 let payload = [MSG_SNAPSHOT_END];
2442 let err = decode_primary_message(&payload).unwrap_err();
2443 assert!(
2444 err.to_string().contains("SnapshotEnd too short"),
2445 "expected 'SnapshotEnd too short', got: {err}"
2446 );
2447 }
2448
2449 #[test]
2450 fn decode_snapshot_chunk_empty_data() {
2451 let payload = [MSG_SNAPSHOT_CHUNK];
2453 let msg = decode_primary_message(&payload).unwrap();
2454 match msg {
2455 PrimaryMessage::SnapshotChunk(data) => {
2456 assert!(data.is_empty());
2457 }
2458 _ => panic!("expected SnapshotChunk"),
2459 }
2460 }
2461
2462 fn make_journal_cursor(val: u64) -> melin_pipeline::padding::Sequence {
2465 melin_pipeline::padding::Sequence::new(AtomicU64::new(val))
2466 }
2467
2468 #[test]
2469 fn pending_ack_queue_push_and_pop_ready() {
2470 let mut q = PendingAckQueue::new(8);
2471 assert!(q.is_empty());
2472 assert!(!q.is_full());
2473
2474 q.push(10, 100);
2475 q.push(20, 200);
2476 assert!(!q.is_empty());
2477
2478 let cursor = make_journal_cursor(5);
2480 assert!(q.pop_ready(&cursor).is_none());
2481
2482 cursor.get().store(15, Ordering::Relaxed);
2484 assert_eq!(q.pop_ready(&cursor), Some(100));
2485 assert!(!q.is_empty());
2487
2488 cursor.get().store(25, Ordering::Relaxed);
2490 assert_eq!(q.pop_ready(&cursor), Some(200));
2491 assert!(q.is_empty());
2492 }
2493
2494 #[test]
2495 fn pending_ack_queue_pop_ready_returns_highest_sequence() {
2496 let mut q = PendingAckQueue::new(8);
2500 q.push(10, 100);
2501 q.push(20, 200);
2502 q.push(30, 300);
2503
2504 let cursor = make_journal_cursor(30);
2505 assert_eq!(q.pop_ready(&cursor), Some(300));
2506 assert!(q.is_empty());
2507 }
2508
2509 #[test]
2510 fn pending_ack_queue_capacity_and_full() {
2511 let mut q = PendingAckQueue::new(8);
2512 for i in 0..8 {
2513 assert!(!q.is_full());
2514 q.push(i as u64 + 1, (i + 1) as u64 * 100);
2515 }
2516 assert!(q.is_full());
2517 }
2518
2519 #[test]
2524 fn pending_ack_queue_merges_into_the_newest_entry_when_full() {
2525 let mut q = PendingAckQueue::new(4);
2526 for i in 1..=4u64 {
2527 q.push(i * 10, i * 100);
2528 }
2529 assert!(q.is_full());
2530 assert_eq!(q.merged(), 0);
2531
2532 q.push(50, 500);
2534 assert_eq!(q.merged(), 1);
2535 assert_eq!(q.len(), 4, "merge must not grow the queue");
2536
2537 let cursor = make_journal_cursor(40);
2540 assert_eq!(q.pop_ready(&cursor), Some(300));
2541 assert!(!q.is_empty(), "merged entry is not durable yet");
2542
2543 cursor.get().store(50, Ordering::Relaxed);
2546 assert_eq!(q.pop_ready(&cursor), Some(500));
2547 assert!(q.is_empty());
2548 }
2549
2550 #[test]
2553 fn pending_ack_queue_merges_repeatedly_without_growing() {
2554 let mut q = PendingAckQueue::new(2);
2555 for i in 1..=100u64 {
2556 q.push(i, i * 10);
2557 }
2558 assert_eq!(q.len(), 2);
2559 assert_eq!(q.merged(), 98);
2560
2561 let cursor = make_journal_cursor(99);
2563 assert_eq!(q.pop_ready(&cursor), Some(10), "only the first entry");
2564 cursor.get().store(100, Ordering::Relaxed);
2566 assert_eq!(q.pop_ready(&cursor), Some(1000));
2567 }
2568
2569 #[test]
2570 fn pending_ack_queue_pop_oldest_blocking() {
2571 let mut q = PendingAckQueue::new(8);
2572 q.push(10, 100);
2573 q.push(20, 200);
2574
2575 let cursor = make_journal_cursor(25);
2578 let seq = q.pop_oldest_blocking(&cursor, true, &AtomicBool::new(false));
2579 assert_eq!(seq, Some(200));
2581 assert!(q.is_empty());
2582 }
2583
2584 #[test]
2587 fn pending_ack_queue_blocking_wait_aborts_on_journal_failure() {
2588 let mut q = PendingAckQueue::new(8);
2589 q.push(10, 100);
2590
2591 let cursor = make_journal_cursor(5);
2593 let abort = AtomicBool::new(true);
2594 assert_eq!(q.pop_oldest_blocking(&cursor, true, &abort), None);
2595 assert!(!q.is_empty(), "aborted wait must not pop the entry");
2596 assert_eq!(q.pop_all_blocking(&cursor, true, &abort), None);
2597 }
2598
2599 #[test]
2608 fn shutdown_pipeline_surfaces_journal_error_kind() {
2609 let flag = AtomicBool::new(false);
2610 let matching_joined = Arc::new(AtomicBool::new(false));
2611 let mj = Arc::clone(&matching_joined);
2612
2613 let journal = std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> {
2614 Err(melin_journal::JournalError::ReplicaChainDivergence {
2615 sequence: 42,
2616 expected: [1u8; 32],
2617 actual: [2u8; 32],
2618 })
2619 });
2620 let matching = std::thread::spawn(move || {
2621 mj.store(true, Ordering::Release);
2622 7u64
2623 });
2624 let drain = std::thread::spawn(|| {});
2625
2626 let outcome = shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None);
2627 assert!(flag.load(Ordering::Acquire), "shutdown flag must be set");
2628 assert!(
2629 matching_joined.load(Ordering::Acquire),
2630 "matching thread must be joined even when the journal failed"
2631 );
2632 match outcome {
2633 TeardownOutcome::JournalFailed(
2634 melin_journal::JournalError::ReplicaChainDivergence { sequence, .. },
2635 ) => assert_eq!(sequence, 42),
2636 _ => panic!("expected JournalFailed(ReplicaChainDivergence)"),
2637 }
2638 }
2639
2640 #[test]
2641 fn shutdown_pipeline_clean_returns_both_states() {
2642 let flag = AtomicBool::new(false);
2643 let journal =
2644 std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> { Ok(11u32) });
2645 let matching = std::thread::spawn(|| 7u64);
2646 let drain = std::thread::spawn(|| {});
2647
2648 match shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None) {
2649 TeardownOutcome::Clean(app, writer) => {
2650 assert_eq!(app, 7);
2651 assert_eq!(writer, 11);
2652 }
2653 _ => panic!("expected Clean"),
2654 }
2655 }
2656
2657 #[test]
2658 fn shutdown_pipeline_panicked_stage_reports_panicked() {
2659 let flag = AtomicBool::new(false);
2660 let journal =
2661 std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> { Ok(11u32) });
2662 let matching = std::thread::spawn(|| -> u64 { panic!("matching stage died") });
2663 let drain = std::thread::spawn(|| {});
2664
2665 assert!(matches!(
2666 shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None),
2667 TeardownOutcome::Panicked
2668 ));
2669 }
2670
2671 fn teardown_fixture(
2676 capacity: usize,
2677 ) -> (
2678 ReplicaPipelineHandles<counter_server::Counter, u32>,
2679 melin_pipeline::ring::Consumer<InputSlot>,
2680 ) {
2681 use melin_app::app_factory::AppFactory;
2682 let (input_producer, mut consumers) =
2683 melin_pipeline::ring::DisruptorBuilder::<InputSlot>::new(capacity)
2684 .add_consumer()
2685 .build();
2686 let consumer = consumers.pop().expect("one consumer");
2687 let handles = ReplicaPipelineHandles {
2688 input_producer,
2689 journal_cursor: Arc::new(make_journal_cursor(0)),
2690 last_seq: melin_transport_core::DurableWireSeqCursor::detached(
2691 melin_transport_core::WireSeq::new(0),
2692 ),
2693 chain_hash_lock: None,
2694 stream_marks: Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new())),
2695 journal_failed: Arc::new(AtomicBool::new(false)),
2696 pipeline_shutdown: Arc::new(AtomicBool::new(false)),
2697 journal_handle: std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> {
2698 Ok(11)
2699 }),
2700 matching_handle: std::thread::spawn(|| counter_server::CounterFactory.empty()),
2701 drain_handle: std::thread::spawn(|| {}),
2702 shadow_handle: None,
2703 };
2704 (handles, consumer)
2705 }
2706
2707 #[test]
2713 fn teardown_publishes_shutdown_sentinel() {
2714 let (handles, mut consumer) = teardown_fixture(8);
2715 assert!(matches!(
2716 teardown_replica_pipeline::<counter_server::Counter, u32>(handles),
2717 TeardownOutcome::Clean(..)
2718 ));
2719 let (_, slot) = consumer.try_consume().expect("sentinel on the ring");
2720 assert!(
2721 slot.event.is_shutdown(),
2722 "published slot must be the sentinel"
2723 );
2724 assert!(consumer.try_consume().is_none(), "exactly one sentinel");
2725 }
2726
2727 fn backoff_after_disconnect(heard_from_primary: bool) -> std::time::Duration {
2734 let dir = tempfile::tempdir().expect("tempdir");
2735 type Writer = melin_journal::BufferedWriter<CounterEvent>;
2736 let mut pipeline: Option<ReplicaPipelineHandles<counter_server::Counter, Writer>> = None;
2737 let mut divergence_resyncs = 0u32;
2738 let mut backoff = MAX_BACKOFF;
2739 let shutdown = AtomicBool::new(true);
2740 let promote = crate::promotion::PromotionRequest::new();
2741 let after = handle_session_exit::<counter_server::Counter, Writer>(
2742 StreamingResult {
2743 exit: SessionExit::Disconnected,
2744 heard_from_primary,
2745 },
2746 &mut pipeline,
2747 &mut divergence_resyncs,
2748 &mut backoff,
2749 0,
2750 &dir.path().join("r.journal"),
2751 &dir.path().join("r.snapshot"),
2752 &counter_server::CounterFactory,
2753 &melin_transport_core::fence::FenceState::new(0),
2754 &shutdown,
2755 &promote,
2756 || {},
2757 );
2758 assert!(matches!(after, AfterSession::Reconnect));
2759 backoff
2760 }
2761
2762 #[test]
2767 fn disconnect_resets_backoff_when_primary_spoke() {
2768 assert_eq!(
2771 backoff_after_disconnect(true),
2772 std::time::Duration::from_secs(2)
2773 );
2774 }
2775
2776 #[test]
2781 fn disconnect_keeps_escalated_backoff_when_primary_silent() {
2782 assert_eq!(backoff_after_disconnect(false), MAX_BACKOFF);
2783 }
2784
2785 #[test]
2794 fn stream_gap_reconnects_and_keeps_pipeline() {
2795 use melin_app::app_factory::AppFactory;
2796 let dir = tempfile::tempdir().expect("tempdir");
2797 type Writer = melin_journal::BufferedWriter<CounterEvent>;
2798 let (input_producer, mut consumers) =
2802 melin_pipeline::ring::DisruptorBuilder::<InputSlot>::new(4)
2803 .add_consumer()
2804 .build();
2805 let _consumer = consumers.pop().expect("one consumer");
2806 let writer_path = dir.path().join("w.journal");
2807 let handles = ReplicaPipelineHandles {
2808 input_producer,
2809 journal_cursor: Arc::new(make_journal_cursor(0)),
2810 last_seq: melin_transport_core::DurableWireSeqCursor::detached(
2811 melin_transport_core::WireSeq::new(0),
2812 ),
2813 chain_hash_lock: None,
2814 stream_marks: Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new())),
2815 journal_failed: Arc::new(AtomicBool::new(false)),
2816 pipeline_shutdown: Arc::new(AtomicBool::new(false)),
2817 journal_handle: std::thread::spawn(move || Writer::create(&writer_path)),
2818 matching_handle: std::thread::spawn(|| counter_server::CounterFactory.empty()),
2819 drain_handle: std::thread::spawn(|| {}),
2820 shadow_handle: None,
2821 };
2822 let mut pipeline: Option<ReplicaPipelineHandles<counter_server::Counter, Writer>> =
2823 Some(handles);
2824 let mut divergence_resyncs = 0u32;
2825 let mut backoff = MAX_BACKOFF;
2826 let shutdown = AtomicBool::new(true);
2827 let promote = crate::promotion::PromotionRequest::new();
2828 let mut closed = false;
2829 let after = handle_session_exit::<counter_server::Counter, Writer>(
2830 StreamingResult {
2831 exit: SessionExit::StreamGap("sequence gap: expected 3, got 5".into()),
2832 heard_from_primary: true,
2833 },
2834 &mut pipeline,
2835 &mut divergence_resyncs,
2836 &mut backoff,
2837 2,
2838 &dir.path().join("r.journal"),
2839 &dir.path().join("r.snapshot"),
2840 &counter_server::CounterFactory,
2841 &melin_transport_core::fence::FenceState::new(0),
2842 &shutdown,
2843 &promote,
2844 || closed = true,
2845 );
2846 assert!(
2847 matches!(after, AfterSession::Reconnect),
2848 "a stream gap must reconnect, not return an error"
2849 );
2850 assert!(
2851 pipeline.is_some(),
2852 "the pipeline must survive for the reconnect"
2853 );
2854 assert!(closed, "the transport must be closed before redialing");
2855 assert_eq!(
2856 backoff,
2857 std::time::Duration::from_secs(2),
2858 "backoff reset to 1s then doubled by the shared helper"
2859 );
2860 assert_eq!(divergence_resyncs, 0, "not a resync");
2861 if let Some(p) = pipeline.take() {
2862 let _ = teardown_replica_pipeline::<counter_server::Counter, Writer>(p);
2863 }
2864 }
2865
2866 #[test]
2871 fn teardown_skips_sentinel_when_journal_failed_and_ring_full() {
2872 let (mut handles, mut consumer) = teardown_fixture(4);
2873 for seq in 0..4 {
2874 assert!(
2875 handles
2876 .input_producer
2877 .try_publish(InputSlot {
2878 sequence: seq,
2879 ..InputSlot::default()
2880 })
2881 .is_ok(),
2882 "ring must have space while filling"
2883 );
2884 }
2885 assert!(
2886 handles
2887 .input_producer
2888 .try_publish(InputSlot::default())
2889 .is_err(),
2890 "ring must be full"
2891 );
2892 handles.journal_failed.store(true, Ordering::Release);
2893 assert!(matches!(
2895 teardown_replica_pipeline::<counter_server::Counter, u32>(handles),
2896 TeardownOutcome::Clean(..)
2897 ));
2898 while let Some((_, slot)) = consumer.try_consume() {
2899 assert!(!slot.event.is_shutdown(), "no sentinel may be published");
2900 }
2901 }
2902
2903 #[test]
2904 fn pending_ack_queue_wraps_around() {
2905 let mut q = PendingAckQueue::new(8);
2906 let cursor = make_journal_cursor(100);
2907
2908 for round in 0..3 {
2910 for i in 0..8 {
2911 let target = (round * 8 + i) as u64 + 1;
2912 q.push(target, target * 10);
2913 }
2914 assert!(q.is_full());
2915 let seq = q.pop_ready(&cursor).expect("should be ready");
2916 assert_eq!(seq, (round * 8 + 8) as u64 * 10);
2917 assert!(q.is_empty());
2918 }
2919 }
2920
2921 #[test]
2922 fn pending_ack_queue_pop_all_blocking_empty() {
2923 let mut q = PendingAckQueue::new(8);
2924 let cursor = make_journal_cursor(0);
2925 assert!(
2926 q.pop_all_blocking(&cursor, true, &AtomicBool::new(false))
2927 .is_none()
2928 );
2929 }
2930
2931 #[test]
2934 fn dual_track_returns_none_when_both_tracks_idle() {
2935 let mut q = PendingAckQueue::new(8);
2936 let cursor = make_journal_cursor(0);
2937 assert!(
2938 try_flush_dual_track(&mut q, &cursor, 0, 0, 0).is_none(),
2939 "no advance on either track → no ack"
2940 );
2941 }
2942
2943 #[test]
2944 fn dual_track_fires_on_persisted_advance_only() {
2945 let mut q = PendingAckQueue::new(8);
2950 q.push(10, 100);
2951 let cursor = make_journal_cursor(20);
2952 let ack = try_flush_dual_track(&mut q, &cursor, 50, 0, 50).expect("persisted advanced");
2953 assert_eq!(ack.acked_sequence, 100);
2954 assert_eq!(ack.in_memory_sequence, 50);
2955 }
2956
2957 #[test]
2958 fn dual_track_fires_on_in_memory_advance_only() {
2959 let mut q = PendingAckQueue::new(8);
2962 let cursor = make_journal_cursor(0);
2963 let ack = try_flush_dual_track(&mut q, &cursor, 200, 100, 100).expect("in-memory advanced");
2964 assert_eq!(ack.acked_sequence, 100);
2965 assert_eq!(ack.in_memory_sequence, 200);
2966 }
2967
2968 #[test]
2969 fn dual_track_coalesces_until_caller_updates_trackers() {
2970 let mut q = PendingAckQueue::new(8);
2976 q.push(10, 100);
2977 let cursor = make_journal_cursor(20);
2978 let ack1 = try_flush_dual_track(&mut q, &cursor, 50, 0, 50).expect("call 1 fires");
2979 assert_eq!(ack1.acked_sequence, 100);
2980 let ack2 = try_flush_dual_track(&mut q, &cursor, 80, 0, 50)
2982 .expect("call 2 fires on in-memory advance");
2983 assert_eq!(
2984 ack2.acked_sequence, 0,
2985 "no new persisted pop → unwrap_or(last_sent)"
2986 );
2987 assert_eq!(ack2.in_memory_sequence, 80);
2988 }
2989
2990 #[test]
2991 fn dual_track_no_duplicate_ack_after_backpressure_drain() {
2992 let mut q = PendingAckQueue::new(4);
3010 for i in 1..=4u64 {
3013 q.push(i * 10, i * 100);
3014 }
3015 assert!(q.is_full());
3016
3017 let cursor = make_journal_cursor(40);
3020 let drained = q.pop_ready(&cursor).expect("all entries durable");
3021 assert_eq!(drained, 400);
3022 assert!(q.is_empty());
3023
3024 let mut last_sent_acked = drained;
3027 let mut last_sent_in_mem = 450u64;
3028
3029 assert!(
3034 try_flush_dual_track(
3035 &mut q,
3036 &cursor,
3037 last_sent_in_mem,
3038 last_sent_acked,
3039 last_sent_in_mem,
3040 )
3041 .is_none(),
3042 "no advance on either track after backpressure drain → no duplicate ack",
3043 );
3044
3045 q.push(50, 500);
3048 let cursor2 = make_journal_cursor(50);
3049 let ack = try_flush_dual_track(&mut q, &cursor2, 500, last_sent_acked, last_sent_in_mem)
3050 .expect("fresh batch after drain fires a new ack");
3051 assert!(
3052 ack.acked_sequence >= last_sent_acked,
3053 "regression: drain sent {last_sent_acked} but next ack carries {}",
3054 ack.acked_sequence,
3055 );
3056 assert_eq!(ack.acked_sequence, 500);
3057 assert_eq!(ack.in_memory_sequence, 500);
3058
3059 last_sent_acked = ack.acked_sequence;
3060 last_sent_in_mem = ack.in_memory_sequence;
3061
3062 assert!(
3066 try_flush_dual_track(
3067 &mut q,
3068 &cursor2,
3069 last_sent_in_mem,
3070 last_sent_acked,
3071 last_sent_in_mem,
3072 )
3073 .is_none(),
3074 "post-resume idle → no further ack",
3075 );
3076 }
3077}