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
77use receiver_transport::{ControlFrameSource, SessionExit, StreamingResult, receive_chunked_body};
78
79pub(crate) struct ReplicaGate<'a> {
92 count: &'a AtomicU32,
93}
94
95impl<'a> ReplicaGate<'a> {
96 pub(crate) fn new(count: &'a AtomicU32) -> Self {
97 Self { count }
98 }
99
100 pub(crate) fn lift(&self) {
103 self.count.fetch_add(1, Ordering::Release);
104 }
105
106 pub(crate) fn lower(&self) -> bool {
113 let was_last = self.count.fetch_sub(1, Ordering::Release) == 1;
114 if was_last {
115 tracing::warn!("all replicas disconnected — trading halted");
116 }
117 was_last
118 }
119}
120
121pub use melin_transport_core::replication::ack_queue::{
128 PendingAck, PendingAckQueue, try_flush_dual_track, wait_for_journal_cursor,
129};
130pub use melin_transport_core::replication::protocol::{
131 Ack, Handshake, PrimaryMessage, ReplicaMessage,
132};
133pub use melin_transport_core::replication::{ReplicaCursors, ReplicationMetrics, SentHighWater};
134
135#[cfg(feature = "dpdk")]
136pub use dpdk::{DpdkReplicationDriver, run_receiver_dpdk};
137pub use tcp_receiver::{ReceiverResult, run_receiver};
138pub use tcp_sender::{ReplicationListener, Sender, run_sender};
139
140#[derive(Clone)]
147pub struct ReplicaControlPlane {
148 pub promote: crate::promotion::PromotionRequest,
151 pub tip_ready: std::sync::Arc<AtomicBool>,
155 pub journal_tip: melin_transport_core::AdvertisedJournalTip,
158 pub primary_link_up: std::sync::Arc<AtomicBool>,
165 pub primary_acking_mode: std::sync::Arc<std::sync::atomic::AtomicU8>,
175 pub pipeline_healthy: std::sync::Arc<AtomicBool>,
183}
184
185impl ReplicaControlPlane {
186 pub fn new() -> Self {
190 Self {
191 promote: crate::promotion::PromotionRequest::new(),
192 tip_ready: std::sync::Arc::new(AtomicBool::new(false)),
193 journal_tip: melin_transport_core::AdvertisedJournalTip::new(
194 melin_transport_core::WireSeq::new(0),
195 ),
196 primary_link_up: std::sync::Arc::new(AtomicBool::new(false)),
197 primary_acking_mode: std::sync::Arc::new(std::sync::atomic::AtomicU8::new(
198 crate::durability_policy::ACKING_MODE_UNKNOWN,
199 )),
200 pipeline_healthy: std::sync::Arc::new(AtomicBool::new(true)),
201 }
202 }
203}
204
205impl Default for ReplicaControlPlane {
206 fn default() -> Self {
207 Self::new()
208 }
209}
210
211pub(super) fn log_tcp_info(fd: std::os::unix::io::RawFd, tag: &str, slot: usize) {
220 if !tracing::enabled!(tracing::Level::DEBUG) {
221 return;
222 }
223 let mut info: libc::tcp_info = unsafe { std::mem::zeroed() };
227 let mut len = std::mem::size_of::<libc::tcp_info>() as libc::socklen_t;
228 let rc = unsafe {
229 libc::getsockopt(
230 fd,
231 libc::IPPROTO_TCP,
232 libc::TCP_INFO,
233 &mut info as *mut _ as *mut libc::c_void,
234 &mut len,
235 )
236 };
237 if rc != 0 {
238 tracing::debug!(
239 slot,
240 tag,
241 errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
242 "tcp_info getsockopt failed"
243 );
244 return;
245 }
246 tracing::debug!(
247 slot,
248 tag,
249 state = info.tcpi_state,
250 ca_state = info.tcpi_ca_state,
251 rtt_us = info.tcpi_rtt,
252 rttvar_us = info.tcpi_rttvar,
253 snd_cwnd = info.tcpi_snd_cwnd,
254 snd_ssthresh = info.tcpi_snd_ssthresh,
255 snd_mss = info.tcpi_snd_mss,
256 rcv_mss = info.tcpi_rcv_mss,
257 unacked = info.tcpi_unacked,
258 retrans = info.tcpi_retrans,
259 total_retrans = info.tcpi_total_retrans,
260 lost = info.tcpi_lost,
261 rcv_space = info.tcpi_rcv_space,
262 rto_us = info.tcpi_rto,
263 "tcp_info"
264 );
265}
266
267pub(super) fn sleep_checking_flags(
271 duration: std::time::Duration,
272 shutdown: &AtomicBool,
273 promote: &crate::promotion::PromotionRequest,
274) {
275 let deadline = std::time::Instant::now() + duration;
276 while std::time::Instant::now() < deadline {
277 if shutdown.load(Ordering::Relaxed) || promote.is_requested() {
278 return;
279 }
280 std::thread::sleep(std::time::Duration::from_millis(100));
281 }
282}
283
284pub(super) fn sleep_then_double_backoff(
291 backoff: &mut std::time::Duration,
292 shutdown: &AtomicBool,
293 promote: &crate::promotion::PromotionRequest,
294) {
295 sleep_checking_flags(*backoff, shutdown, promote);
296 *backoff = (*backoff * 2).min(MAX_BACKOFF);
297}
298
299pub(super) enum TeardownOutcome<A, W> {
303 Clean(A, W),
306 JournalFailed(melin_journal::JournalError),
312 Panicked,
314}
315
316pub(super) fn shutdown_pipeline<A: Send + 'static, W: Send + 'static>(
331 shutdown_flag: &AtomicBool,
332 journal_handle: std::thread::JoinHandle<Result<W, melin_journal::JournalError>>,
333 matching_handle: std::thread::JoinHandle<A>,
334 drain_handle: std::thread::JoinHandle<()>,
335 shadow_handle: Option<std::thread::JoinHandle<()>>,
336) -> TeardownOutcome<A, W> {
337 shutdown_flag.store(true, Ordering::Release);
351 let journal_result = journal_handle.join();
356 let matching_result = matching_handle.join();
357 let _ = drain_handle.join();
358 if let Some(h) = shadow_handle {
359 let _ = h.join();
360 }
361 let writer = match journal_result {
362 Ok(Ok(w)) => w,
363 Ok(Err(e)) => return TeardownOutcome::JournalFailed(e),
365 Err(_) => return TeardownOutcome::Panicked,
366 };
367 match matching_result {
368 Ok(exchange) => TeardownOutcome::Clean(exchange, writer),
369 Err(_) => TeardownOutcome::Panicked,
370 }
371}
372
373pub(super) struct ReplicaPipelineHandles<A: Application, W: Send + 'static> {
382 pub(super) input_producer: melin_pipeline::ring::Producer<InputSlot<A::Event>>,
383 pub(super) journal_cursor: Arc<melin_pipeline::padding::Sequence>,
384 pub(super) last_seq: melin_transport_core::DurableWireSeqCursor,
390 pub(super) chain_hash_lock:
394 Option<melin_pipeline::seqlock::SeqLockReader<melin_transport_core::pipeline::FsyncState>>,
395 pub(super) stream_marks: melin_transport_core::pipeline::StreamMarkQueue,
400 pub(super) journal_failed: Arc<AtomicBool>,
407 pub(super) pipeline_shutdown: Arc<AtomicBool>,
410 pub(super) journal_handle: std::thread::JoinHandle<Result<W, melin_journal::JournalError>>,
411 pub(super) matching_handle: std::thread::JoinHandle<A>,
412 pub(super) drain_handle: std::thread::JoinHandle<()>,
413 pub(super) shadow_handle: Option<std::thread::JoinHandle<()>>,
414}
415
416pub(super) type ReplicaHandles<A> =
421 ReplicaPipelineHandles<A, BufferedWriter<<A as Application>::Event>>;
422
423#[allow(clippy::too_many_arguments)]
427pub(super) fn build_replica_pipeline_with_threads<A>(
428 exchange: A,
429 writer: BufferedWriter<A::Event>,
430 cores: crate::server::PipelineCores,
431 snapshot_interval_ms: u64,
432 snapshot_path: std::path::PathBuf,
433 group_commit_delay: std::time::Duration,
434 busy_spin: bool,
435 fence_state: Arc<melin_transport_core::fence::FenceState>,
436 pipeline_healthy: Arc<AtomicBool>,
442) -> Result<ReplicaHandles<A>, Box<dyn std::error::Error>>
443where
444 A: Application + Send + 'static,
445 A::Event: Send + Sync + 'static,
446 A::Report: Send + 'static,
447 A::QueryResponse: Send + 'static,
448{
449 let shadow_exchange = <A as Application>::clone_via_snapshot(&exchange)?;
450
451 let enable_shadow = snapshot_interval_ms > 0;
452 let shadow_initial_epoch = fence_state.epoch();
455 let pipeline = melin_transport_core::pipeline::build_replica_pipeline(
456 exchange,
457 writer,
458 4096, group_commit_delay,
460 busy_spin,
461 enable_shadow,
462 fence_state,
463 );
464
465 let pipeline_shutdown = Arc::new(AtomicBool::new(false));
466
467 let ps = Arc::clone(&pipeline_shutdown);
468 let journal_core = cores.journal;
469 let mut journal_stage = pipeline.journal_stage;
470 let stream_marks: melin_transport_core::pipeline::StreamMarkQueue =
475 Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new()));
476 journal_stage.set_stream_marks(Arc::clone(&stream_marks));
477 journal_stage.set_preparer_core(cores.journal_prep);
478 journal_stage.set_disk_core(cores.journal_disk);
479 let journal_failed = Arc::new(AtomicBool::new(false));
480 let journal_failed_latch = Arc::clone(&journal_failed);
481 pipeline_healthy.store(true, Ordering::Release);
484 let journal_handle = std::thread::Builder::new()
485 .name("journal".into())
486 .spawn(move || {
487 melin_app::affinity::pin_thread("journal", journal_core);
488 let result = journal_stage.run(&ps);
489 if let Err(ref e) = result {
490 journal_failed_latch.store(true, Ordering::Release);
497 pipeline_healthy.store(false, Ordering::Release);
500 tracing::error!(error = %e, "replica journal stage failed — session teardown");
501 }
502 result
503 })
504 .expect("spawn journal thread");
505
506 let ps = Arc::clone(&pipeline_shutdown);
507 let matching_core = cores.matching;
508 let matching_stage = pipeline.matching_stage;
509 let matching_handle = std::thread::Builder::new()
510 .name("matching".into())
511 .spawn(move || {
512 melin_app::affinity::pin_thread("matching", matching_core);
513 matching_stage.run(&ps)
514 })
515 .expect("spawn matching thread");
516
517 let ps = Arc::clone(&pipeline_shutdown);
520 let drain_core = cores.response;
521 let drain_consumer = pipeline.drain_consumer;
522 let drain_handle = std::thread::Builder::new()
523 .name("drain".into())
524 .spawn(move || {
525 melin_app::affinity::pin_thread("drain", drain_core);
526 let mut consumer = drain_consumer;
527 let mut batch = vec![OutputSlot::<A::Report, A::QueryResponse>::default(); 256];
528 loop {
529 if ps.load(Ordering::Relaxed) {
530 return;
531 }
532 let count = consumer.consume_batch(&mut batch, 256);
533 if count == 0 {
534 if busy_spin {
535 std::hint::spin_loop();
536 } else {
537 std::thread::yield_now();
538 }
539 }
540 }
541 })
542 .expect("spawn drain thread");
543
544 let shadow_handle = if let Some(shadow_cons) = pipeline.shadow_consumer {
545 let snap_path = snapshot_path;
546 let chain_lock = pipeline
547 .chain_hash_lock
548 .as_ref()
549 .expect("chain hash lock with shadow")
550 .clone();
551 let ps = Arc::clone(&pipeline_shutdown);
552 let shadow_core = cores.shadow;
553 Some(
554 std::thread::Builder::new()
555 .name("replica-shadow".into())
556 .spawn(move || {
557 melin_app::affinity::pin_thread("replica-shadow", shadow_core);
558 melin_transport_core::shadow::run(
559 shadow_cons,
560 shadow_exchange,
561 snap_path,
562 std::time::Duration::from_millis(snapshot_interval_ms),
563 chain_lock,
564 &ps,
565 false,
566 shadow_initial_epoch,
567 );
568 })
569 .expect("spawn shadow thread"),
570 )
571 } else {
572 None
573 };
574
575 Ok(ReplicaPipelineHandles {
576 input_producer: pipeline.input_producer,
577 journal_cursor: pipeline.cursors.journal_ring_arc(),
578 last_seq: pipeline.cursors.durable_wire_seq(),
579 chain_hash_lock: pipeline.chain_hash_lock,
580 stream_marks,
581 journal_failed,
582 pipeline_shutdown,
583 journal_handle,
584 matching_handle,
585 drain_handle,
586 shadow_handle,
587 })
588}
589
590pub(super) fn teardown_replica_pipeline<A: Application + Send + 'static, W: Send + 'static>(
595 mut handles: ReplicaPipelineHandles<A, W>,
596) -> TeardownOutcome<A, W> {
597 while !handles.journal_failed.load(Ordering::Acquire) {
609 match handles
610 .input_producer
611 .try_publish(InputSlot::<A::Event>::shutdown_sentinel())
612 {
613 Ok(_) => break,
614 Err(_) => std::thread::yield_now(),
616 }
617 }
618 shutdown_pipeline::<A, W>(
619 &handles.pipeline_shutdown,
620 handles.journal_handle,
621 handles.matching_handle,
622 handles.drain_handle,
623 handles.shadow_handle,
624 )
625}
626
627pub(super) const MAX_INPROCESS_DIVERGENCE_RESYNCS: u32 = 1;
638
639#[allow(clippy::type_complexity)]
656pub(super) fn recover_replica_state<A, W>(
657 journal_path: &std::path::Path,
658 snapshot_path: &std::path::Path,
659 factory: &dyn melin_app::app_factory::AppFactory<App = A>,
660 fence_state: &melin_transport_core::fence::FenceState,
661) -> Result<(Option<A>, Option<W>, u64, [u8; 32]), Box<dyn std::error::Error>>
662where
663 A: Application,
664 W: melin_journal::JournalWrite<A::Event>,
665{
666 let lineage_exists =
667 journal_path.exists() || !melin_journal::segment::list_archives(journal_path)?.is_empty();
668 if !lineage_exists {
669 return Ok((None, None, 0u64, [0u8; 32]));
670 }
671 let engine = if snapshot_path.exists() {
672 tracing::info!("recovering replica from snapshot + journal");
673 melin_transport_core::JournaledApp::<A, W>::recover_from_snapshot(
674 snapshot_path,
675 journal_path,
676 )?
677 } else {
678 melin_transport_core::JournaledApp::<A, W>::recover(factory.empty(), journal_path)?
679 };
680 let next = engine.next_sequence();
681 let last = next.saturating_sub(1);
682 let hash = engine.chain_hash().unwrap_or([0u8; 32]);
683 fence_state.observe_epoch(engine.recovered_epoch());
686 let (mut exchange, writer) = engine.into_parts();
687 factory.apply_operator_policy(&mut exchange);
688 Ok((Some(exchange), Some(writer), last, hash))
689}
690
691pub(super) const MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(30);
697
698pub(in crate::replication) enum AfterSession<A, W> {
702 Return(ReceiverResult<A, W>),
706 Resync {
711 exchange: Option<A>,
712 journal_writer: Option<W>,
713 last_sequence: u64,
714 chain_hash: [u8; 32],
715 },
716 Reconnect,
720}
721
722#[allow(clippy::too_many_arguments)]
743pub(in crate::replication) fn handle_session_exit<A, W>(
744 result: StreamingResult,
745 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
746 divergence_resyncs: &mut u32,
747 backoff: &mut std::time::Duration,
748 last_sequence: u64,
749 journal_path: &std::path::Path,
750 snapshot_path: &std::path::Path,
751 factory: &dyn melin_app::app_factory::AppFactory<App = A>,
752 fence_state: &melin_transport_core::fence::FenceState,
753 shutdown: &AtomicBool,
754 promote: &crate::promotion::PromotionRequest,
755 mut close: impl FnMut(),
756) -> AfterSession<A, W>
757where
758 A: Application + Send + 'static,
759 W: JournalWrite<A::Event> + Send + 'static,
760{
761 let StreamingResult {
762 exit,
763 heard_from_primary,
764 } = result;
765
766 match exit {
767 SessionExit::Shutdown => {
768 if let Some(p) = pipeline.take() {
769 let _ = teardown_replica_pipeline::<A, W>(p);
770 }
771 AfterSession::Return(Ok(None))
772 }
773
774 SessionExit::Promote => AfterSession::Return(match pipeline.take() {
775 Some(p) => match teardown_replica_pipeline::<A, W>(p) {
776 TeardownOutcome::Clean(ex, wr) => Ok(Some((ex, wr))),
777 _ => Err("pipeline failed during promotion".into()),
778 },
779 None => Err("pipeline missing on promote".into()),
780 }),
781
782 SessionExit::Fatal(e) => {
783 let outcome = match pipeline.take() {
784 Some(p) => teardown_replica_pipeline::<A, W>(p),
785 None => return AfterSession::Return(Err(e)),
788 };
789 let TeardownOutcome::JournalFailed(
799 je @ melin_journal::JournalError::ReplicaChainDivergence { .. },
800 ) = outcome
801 else {
802 return AfterSession::Return(Err(e));
803 };
804
805 *divergence_resyncs += 1;
806 let attempt = *divergence_resyncs;
807 if attempt > MAX_INPROCESS_DIVERGENCE_RESYNCS {
808 return AfterSession::Return(Err(format!(
809 "mid-stream chain divergence recurred {attempt} times — giving up on \
810 in-process resync (each cycle archives the local journal and re-seeds \
811 from the primary; recurrence at this rate means the primary keeps \
812 streaming history that forks from what it announces): {je}"
813 )
814 .into()));
815 }
816 tracing::warn!(
817 error = %je,
818 attempt,
819 max_attempts = MAX_INPROCESS_DIVERGENCE_RESYNCS,
820 "mid-stream chain divergence — re-deriving local state for in-process resync"
821 );
822 close();
824 match recover_replica_state::<A, W>(journal_path, snapshot_path, factory, fence_state) {
825 Ok((exchange, journal_writer, seq, hash)) => AfterSession::Resync {
826 exchange,
827 journal_writer,
828 last_sequence: seq,
829 chain_hash: hash,
830 },
831 Err(e) => AfterSession::Return(Err(e)),
832 }
833 }
834
835 SessionExit::StreamGap(e) => {
836 close();
845 *backoff = std::time::Duration::from_secs(1);
846 tracing::warn!(
847 error = %e,
848 last_sequence,
849 "replication stream broke contiguity — reconnecting from the durable position"
850 );
851 sleep_then_double_backoff(backoff, shutdown, promote);
852 AfterSession::Reconnect
853 }
854
855 SessionExit::Disconnected => {
856 close();
859 if heard_from_primary {
868 *backoff = std::time::Duration::from_secs(1);
869 }
870 tracing::warn!(
871 last_sequence,
872 heard_from_primary,
873 backoff_secs = backoff.as_secs(),
874 "reconnecting to primary"
875 );
876 sleep_then_double_backoff(backoff, shutdown, promote);
877 AfterSession::Reconnect
878 }
879 }
880}
881
882pub(in crate::replication) fn take_pipeline_for_promotion<A, W>(
892 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
893 exchange: &mut Option<A>,
894 journal_writer: &mut Option<W>,
895) -> ReceiverResult<A, W>
896where
897 A: Application + Send + 'static,
898 W: JournalWrite<A::Event> + Send + 'static,
899{
900 if let Some(p) = pipeline.take()
901 && let TeardownOutcome::Clean(e, w) = teardown_replica_pipeline::<A, W>(p)
902 {
903 *exchange = Some(e);
904 *journal_writer = Some(w);
905 }
906 match (exchange.take(), journal_writer.take()) {
907 (Some(e), Some(w)) => Ok(Some((e, w))),
908 _ => Err("promotion requested but no local state available".into()),
909 }
910}
911
912type ResyncTransfer<A> = (A, u64, [u8; 32], u64);
915
916pub(in crate::replication) enum ResyncDecision {
919 Ready {
924 segment_start_sequence: u64,
925 anchor_hash: [u8; 32],
926 resume_sequence: u64,
927 },
928 Retry,
932}
933
934fn receive_resync_transfer<A, S>(
944 source: &mut S,
945 snapshot_path: &std::path::Path,
946 journal_path: &std::path::Path,
947 fence_state: &melin_transport_core::fence::FenceState,
948) -> Result<ResyncTransfer<A>, Box<dyn std::error::Error + Send + Sync>>
949where
950 A: Application,
951 S: ControlFrameSource,
952{
953 let (snap_len, snap_sequence, snap_chain_hash) =
954 match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
955 PrimaryMessage::SnapshotBegin {
956 snapshot_len,
957 snap_sequence,
958 snap_chain_hash,
959 } => (snapshot_len, snap_sequence, snap_chain_hash),
960 other => return Err(format!("expected SnapshotBegin, got {other:?}").into()),
961 };
962
963 tracing::info!(snap_sequence, snap_len, "receiving snapshot");
964 let tmp_path = snapshot_path.with_extension("snapshot.tmp");
965 receive_chunked_body(source, &tmp_path, snap_len, "snapshot")?;
966 std::fs::rename(&tmp_path, snapshot_path)?;
967 tracing::info!(snap_sequence, snap_len, "snapshot received and verified");
968
969 let (snap_exchange, _snap_seq, snap_hash, snap_epoch) =
970 melin_transport_core::snapshot::load::<A>(snapshot_path)?;
971 if snap_hash != snap_chain_hash {
972 return Err(format!(
973 "snapshot chain hash mismatch: primary sent {snap_chain_hash:02x?}, \
974 loaded snapshot has {snap_hash:02x?}"
975 )
976 .into());
977 }
978 fence_state.observe_epoch(snap_epoch);
981
982 let seed_len = match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
987 PrimaryMessage::SegmentSeedBegin { seed_len } => seed_len,
988 other => {
989 return Err(format!("expected SegmentSeedBegin after snapshot, got {other:?}").into());
990 }
991 };
992 let seed_tmp = journal_path.with_extension("seed.tmp");
993 receive_chunked_body(source, &seed_tmp, seed_len, "segment seed")?;
994 if let Err(e) =
1000 melin_journal::segment::verify_segment_prefix(&seed_tmp, snap_sequence, seed_len)
1001 {
1002 let _ = std::fs::remove_file(&seed_tmp);
1003 return Err(format!("segment seed failed structural verification: {e}").into());
1004 }
1005 std::fs::rename(&seed_tmp, journal_path)?;
1006 melin_journal::segment::fsync_parent_dir(journal_path)?;
1007 Ok((snap_exchange, snap_sequence, snap_chain_hash, seed_len))
1008}
1009
1010#[allow(clippy::too_many_arguments)]
1023pub(in crate::replication) fn handle_resync_verdict<A, W, S>(
1024 divergent: bool,
1025 source: &mut S,
1026 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
1027 exchange: &mut Option<A>,
1028 journal_writer: &mut Option<W>,
1029 journal_path: &std::path::Path,
1030 snapshot_path: &std::path::Path,
1031 fence_state: &melin_transport_core::fence::FenceState,
1032 control: &ReplicaControlPlane,
1033 last_sequence: &mut u64,
1034 chain_hash: &mut [u8; 32],
1035) -> Result<ResyncDecision, Box<dyn std::error::Error + Send + Sync>>
1036where
1037 A: Application + Send + 'static,
1038 W: JournalWrite<A::Event> + Send + 'static,
1039 S: ControlFrameSource,
1040{
1041 if divergent {
1046 tracing::warn!(
1047 last_sequence = *last_sequence,
1048 "primary reports chain divergence — archiving local journal, resyncing from snapshot"
1049 );
1050 } else {
1051 tracing::info!("primary requires snapshot transfer — receiving snapshot");
1052 }
1053
1054 if let Some(p) = pipeline.take() {
1055 let _ = teardown_replica_pipeline::<A, W>(p);
1056 }
1057
1058 *exchange = None;
1065 *journal_writer = None;
1066
1067 let reason = if divergent {
1071 ArchiveReason::Divergent
1072 } else {
1073 ArchiveReason::Resync
1074 };
1075 archive_local_lineage(journal_path, snapshot_path, reason)?;
1076 *last_sequence = 0;
1078 *chain_hash = [0u8; 32];
1079 control
1084 .journal_tip
1085 .reset(melin_transport_core::WireSeq::new(0));
1086
1087 let (snap_exchange, snap_sequence, snap_chain_hash, seed_len) =
1088 match receive_resync_transfer::<A, S>(source, snapshot_path, journal_path, fence_state) {
1089 Ok(v) => v,
1090 Err(e) => {
1091 tracing::warn!(error = %e, "snapshot transfer failed — retrying");
1092 let _ = std::fs::remove_file(snapshot_path);
1096 return Ok(ResyncDecision::Retry);
1097 }
1098 };
1099 *exchange = Some(snap_exchange);
1100
1101 let writer = W::open_append(journal_path, snap_sequence, seed_len)?;
1108 let seeded_chain = writer.chain_hash().unwrap_or(snap_chain_hash);
1109 if snap_chain_hash != [0u8; 32] && seeded_chain != snap_chain_hash {
1110 return Err(format!(
1111 "segment seed chain at {snap_sequence} disagrees with the transferred snapshot's \
1112 hash — inconsistent primary"
1113 )
1114 .into());
1115 }
1116 let seeded_info = melin_journal::segment::read_header_info(journal_path)?;
1117 *journal_writer = Some(writer);
1118
1119 match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
1122 PrimaryMessage::StreamStart {
1123 start_sequence,
1124 segment_start_sequence,
1125 anchor_hash,
1126 epoch,
1127 durability_mode,
1128 } => {
1129 if segment_start_sequence != seeded_info.starting_sequence
1130 || anchor_hash != seeded_info.anchor_hash
1131 {
1132 return Err(format!(
1133 "post-snapshot StreamStart lineage (start {segment_start_sequence}) \
1134 disagrees with the transferred segment seed (start {}) — inconsistent \
1135 primary",
1136 seeded_info.starting_sequence
1137 )
1138 .into());
1139 }
1140 fence_state.observe_epoch(epoch);
1144 control
1145 .primary_acking_mode
1146 .store(durability_mode, Ordering::Release);
1147 tracing::info!(
1148 start_sequence,
1149 epoch,
1150 durability_mode,
1151 "streaming resumed after snapshot transfer"
1152 );
1153 control
1156 .journal_tip
1157 .reset(melin_transport_core::WireSeq::new(snap_sequence));
1158 Ok(ResyncDecision::Ready {
1159 segment_start_sequence,
1160 anchor_hash,
1161 resume_sequence: snap_sequence,
1162 })
1163 }
1164 other => Err(format!("expected StreamStart after snapshot, got {other:?}").into()),
1165 }
1166}
1167
1168#[cfg(test)]
1169mod tests {
1170 use std::io::Write;
1171 use std::sync::Arc;
1172
1173 use super::auth::{authenticate_replica, authenticate_with_primary};
1174 use super::*;
1175 use counter_server::CounterEvent;
1181 type InputSlot = melin_transport_core::pipeline::InputSlot<CounterEvent>;
1182 use melin_transport_core::replication::protocol::{
1183 MAX_CONTROL_FRAME, MAX_DATA_FRAME, MSG_AUTH_OK, MSG_CHALLENGE_RESPONSE, MSG_SNAPSHOT_BEGIN,
1184 MSG_SNAPSHOT_CHUNK, MSG_SNAPSHOT_END, decode_auth_result, decode_challenge,
1185 decode_challenge_response, decode_primary_message, decode_replica_message, encode_ack,
1186 encode_auth_failed, encode_auth_ok, encode_chain_check, encode_challenge,
1187 encode_challenge_response, encode_handshake, encode_hash_mismatch, encode_heartbeat,
1188 encode_input_batch, encode_need_snapshot, encode_rotate, encode_segment_seed_begin,
1189 encode_snapshot_begin, encode_snapshot_chunk, encode_snapshot_end, encode_stream_start,
1190 read_frame, try_decode_input_batch,
1191 };
1192
1193 fn encode_input_batch_with_seq(end_sequence: u64, buf: &mut Vec<u8>) {
1197 let slot = InputSlot {
1198 connection_id: 0,
1199 key_hash: 0,
1200 request_seq: 0,
1201 sequence: end_sequence,
1202 timestamp_ns: 0,
1203 event: melin_journal::JournalEvent::Tick { now_ns: 0 },
1204 publish_ts: Default::default(),
1205 recv_ts: Default::default(),
1206 };
1207 encode_input_batch(&[slot], buf);
1208 }
1209
1210 #[test]
1211 fn handshake_encode_decode_round_trip() {
1212 let handshake = Handshake {
1213 last_sequence: 42,
1214 chain_hash: [0xAB; 32],
1215 epoch: 9,
1217 };
1218 let mut buf = Vec::new();
1219 encode_handshake(&handshake, &mut buf);
1220
1221 let payload = &buf[4..];
1223 let msg = decode_replica_message(payload).unwrap();
1224 match msg {
1225 ReplicaMessage::Handshake(h) => {
1226 assert_eq!(h.last_sequence, 42);
1227 assert_eq!(h.chain_hash, [0xAB; 32]);
1228 assert_eq!(h.epoch, 9);
1229 }
1230 _ => panic!("expected Handshake"),
1231 }
1232 }
1233
1234 #[test]
1235 fn ack_encode_decode_round_trip() {
1236 let ack = Ack {
1237 acked_sequence: 1000,
1238 in_memory_sequence: 1024,
1239 };
1240 let mut buf = Vec::new();
1241 encode_ack(&ack, &mut buf);
1242
1243 let payload = &buf[4..];
1244 let msg = decode_replica_message(payload).unwrap();
1245 match msg {
1246 ReplicaMessage::Ack(a) => {
1247 assert_eq!(a.acked_sequence, 1000);
1248 assert_eq!(a.in_memory_sequence, 1024);
1249 }
1250 _ => panic!("expected Ack"),
1251 }
1252 }
1253
1254 #[test]
1262 fn ack_wire_byte_pattern() {
1263 let ack = Ack {
1264 acked_sequence: 0xDEAD_BEEF_CAFE_F00D,
1265 in_memory_sequence: 0x1122_3344_5566_7788,
1266 };
1267 let mut buf = Vec::new();
1268 encode_ack(&ack, &mut buf);
1269 let expected: &[u8] = &[
1272 0x11, 0x00, 0x00, 0x00, 0x02, 0x0D, 0xF0, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, ];
1277 assert_eq!(buf.as_slice(), expected, "Ack wire layout drifted");
1278 }
1279
1280 #[test]
1281 fn stream_start_encode_decode_round_trip() {
1282 let mut buf = Vec::new();
1283 encode_stream_start(99, 42, [0xAA; 32], 5, 2, &mut buf);
1286
1287 let payload = &buf[4..];
1288 let msg = decode_primary_message(payload).unwrap();
1289 match msg {
1290 PrimaryMessage::StreamStart {
1291 start_sequence,
1292 segment_start_sequence,
1293 anchor_hash,
1294 epoch,
1295 durability_mode,
1296 } => {
1297 assert_eq!(start_sequence, 99);
1298 assert_eq!(segment_start_sequence, 42);
1299 assert_eq!(anchor_hash, [0xAA; 32]);
1300 assert_eq!(durability_mode, 2);
1301 assert_eq!(epoch, 5);
1302 }
1303 _ => panic!("expected StreamStart"),
1304 }
1305 }
1306
1307 #[test]
1308 fn heartbeat_encode_decode_round_trip() {
1309 let mut buf = Vec::new();
1310 encode_heartbeat(123, 2, &mut buf);
1311
1312 let payload = &buf[4..];
1313 let msg = decode_primary_message(payload).unwrap();
1314 match msg {
1315 PrimaryMessage::Heartbeat {
1316 sequence,
1317 durability_mode,
1318 } => {
1319 assert_eq!(sequence, 123);
1320 assert_eq!(durability_mode, 2);
1321 }
1322 _ => panic!("expected Heartbeat"),
1323 }
1324 }
1325
1326 #[test]
1327 fn need_snapshot_encode_decode_round_trip() {
1328 let mut buf = Vec::new();
1329 encode_need_snapshot(&mut buf);
1330
1331 let payload = &buf[4..];
1332 let msg = decode_primary_message(payload).unwrap();
1333 assert!(matches!(msg, PrimaryMessage::NeedSnapshot));
1334 }
1335
1336 #[test]
1337 fn hash_mismatch_encode_decode_round_trip() {
1338 let mut buf = Vec::new();
1339 encode_hash_mismatch(&mut buf);
1340
1341 let payload = &buf[4..];
1342 let msg = decode_primary_message(payload).unwrap();
1343 assert!(matches!(msg, PrimaryMessage::HashMismatch));
1344 }
1345
1346 #[test]
1347 fn rotate_encode_decode_round_trip() {
1348 let mut buf = Vec::new();
1349 encode_rotate(7_000_000, &[0xBB; 32], &mut buf);
1350
1351 let payload = &buf[4..];
1352 let msg = decode_primary_message(payload).unwrap();
1353 match msg {
1354 PrimaryMessage::Rotate {
1355 boundary_seq,
1356 tail_hash,
1357 } => {
1358 assert_eq!(boundary_seq, 7_000_000);
1359 assert_eq!(tail_hash, [0xBB; 32]);
1360 }
1361 _ => panic!("expected Rotate"),
1362 }
1363 }
1364
1365 #[test]
1366 fn chain_check_encode_decode_round_trip() {
1367 let mut buf = Vec::new();
1368 encode_chain_check(555, &[0xCC; 32], &mut buf);
1369
1370 let payload = &buf[4..];
1371 let msg = decode_primary_message(payload).unwrap();
1372 match msg {
1373 PrimaryMessage::ChainCheck {
1374 sequence,
1375 chain_hash,
1376 } => {
1377 assert_eq!(sequence, 555);
1378 assert_eq!(chain_hash, [0xCC; 32]);
1379 }
1380 _ => panic!("expected ChainCheck"),
1381 }
1382 }
1383
1384 #[test]
1388 fn rotate_wire_byte_pattern() {
1389 let mut hash = [0u8; 32];
1390 for (i, b) in hash.iter_mut().enumerate() {
1391 *b = i as u8;
1392 }
1393 let mut buf = Vec::new();
1394 encode_rotate(0xDEAD_BEEF_CAFE_F00D, &hash, &mut buf);
1395 let mut expected = vec![
1398 0x29, 0x00, 0x00, 0x00, 0x16, 0x0D, 0xF0, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE, ];
1402 expected.extend_from_slice(&hash);
1403 assert_eq!(
1404 buf.as_slice(),
1405 expected.as_slice(),
1406 "Rotate wire layout drifted"
1407 );
1408 }
1409
1410 #[test]
1411 fn segment_seed_begin_encode_decode_round_trip() {
1412 let mut buf = Vec::new();
1413 encode_segment_seed_begin(123_456, &mut buf);
1414
1415 let payload = &buf[4..];
1416 let msg = decode_primary_message(payload).unwrap();
1417 match msg {
1418 PrimaryMessage::SegmentSeedBegin { seed_len } => {
1419 assert_eq!(seed_len, 123_456);
1420 }
1421 _ => panic!("expected SegmentSeedBegin"),
1422 }
1423 }
1424
1425 #[test]
1426 fn unknown_replica_message_type_is_error() {
1427 let payload = [0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
1428 let result = decode_replica_message(&payload);
1429 assert!(result.is_err());
1430 }
1431
1432 #[test]
1433 fn unknown_primary_message_type_is_error() {
1434 let payload = [0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
1435 let result = decode_primary_message(&payload);
1436 assert!(result.is_err());
1437 }
1438
1439 #[test]
1440 fn read_frame_enforces_max_size() {
1441 let mut buf = Vec::new();
1443 buf.extend_from_slice(&1000u32.to_le_bytes());
1444 buf.extend_from_slice(&[0u8; 100]); let mut cursor = std::io::Cursor::new(buf);
1447 let result = read_frame(&mut cursor, 64);
1448 assert!(result.is_err());
1449 }
1450
1451 #[test]
1452 fn challenge_encode_decode_round_trip() {
1453 let nonce = [0x42; 32];
1454 let mut buf = Vec::new();
1455 encode_challenge(&nonce, &mut buf);
1456
1457 let payload = &buf[4..];
1458 let decoded = decode_challenge(payload).unwrap();
1459 assert_eq!(decoded, nonce);
1460 }
1461
1462 #[test]
1463 fn challenge_response_encode_decode_round_trip() {
1464 let sig = [0xAA; 64];
1465 let pubkey = [0xBB; 32];
1466 let mut buf = Vec::new();
1467 encode_challenge_response(&sig, &pubkey, &mut buf);
1468
1469 let payload = &buf[4..];
1470 let (decoded_sig, decoded_pubkey) = decode_challenge_response(payload).unwrap();
1471 assert_eq!(decoded_sig, sig);
1472 assert_eq!(decoded_pubkey, pubkey);
1473 }
1474
1475 #[test]
1476 fn auth_ok_encode_decode_round_trip() {
1477 let mut buf = Vec::new();
1478 encode_auth_ok(&mut buf);
1479
1480 let payload = &buf[4..];
1481 assert!(decode_auth_result(payload).unwrap());
1482 }
1483
1484 #[test]
1485 fn auth_failed_encode_decode_round_trip() {
1486 let mut buf = Vec::new();
1487 encode_auth_failed(&mut buf);
1488
1489 let payload = &buf[4..];
1490 assert!(!decode_auth_result(payload).unwrap());
1491 }
1492
1493 #[test]
1494 fn decode_challenge_rejects_wrong_tag() {
1495 let mut payload = [0u8; 33];
1496 payload[0] = MSG_AUTH_OK;
1497 assert!(decode_challenge(&payload).is_err());
1498 }
1499
1500 #[test]
1501 fn decode_challenge_response_rejects_short_payload() {
1502 let payload = [MSG_CHALLENGE_RESPONSE; 10]; assert!(decode_challenge_response(&payload).is_err());
1504 }
1505
1506 #[test]
1507 fn auth_round_trip_valid_key() {
1508 use ed25519_dalek::SigningKey;
1509 use std::os::unix::net::UnixStream;
1510
1511 let repl_key = SigningKey::from_bytes(&[0xFC; 32]);
1512 let pub_b64 = base64::Engine::encode(
1513 &base64::engine::general_purpose::STANDARD,
1514 repl_key.verifying_key().to_bytes(),
1515 );
1516 let keys_content = format!("replication {pub_b64} test-replica\n");
1517 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1518
1519 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1520 primary_stream
1521 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1522 .unwrap();
1523 replica_stream
1524 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1525 .unwrap();
1526
1527 let repl_key_clone = SigningKey::from_bytes(&[0xFC; 32]);
1528 let replica_handle = std::thread::spawn(move || {
1529 let mut conn = replica_stream;
1530 authenticate_with_primary(&mut conn, &repl_key_clone)
1531 });
1532
1533 let mut conn = primary_stream;
1534 authenticate_replica(&mut conn, &authorized_keys).unwrap();
1535
1536 replica_handle.join().unwrap().unwrap();
1537 }
1538
1539 #[test]
1540 fn auth_rejects_unknown_key() {
1541 use ed25519_dalek::SigningKey;
1542 use std::os::unix::net::UnixStream;
1543
1544 let authorized_key = SigningKey::from_bytes(&[0xAA; 32]);
1546 let rogue_key = SigningKey::from_bytes(&[0xBB; 32]);
1547 let pub_b64 = base64::Engine::encode(
1548 &base64::engine::general_purpose::STANDARD,
1549 authorized_key.verifying_key().to_bytes(),
1550 );
1551 let keys_content = format!("replication {pub_b64} authorized-replica\n");
1552 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1553
1554 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1555 primary_stream
1556 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1557 .unwrap();
1558 replica_stream
1559 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1560 .unwrap();
1561
1562 let replica_handle = std::thread::spawn(move || {
1563 let mut conn = replica_stream;
1564 authenticate_with_primary(&mut conn, &rogue_key)
1565 });
1566
1567 let mut conn = primary_stream;
1568 let result = authenticate_replica(&mut conn, &authorized_keys);
1569 assert!(result.is_err());
1570 assert!(result.unwrap_err().to_string().contains("unknown"));
1571
1572 let replica_result = replica_handle.join().unwrap();
1574 assert!(replica_result.is_err());
1575 }
1576
1577 #[test]
1578 fn auth_rejects_wrong_permission() {
1579 use ed25519_dalek::SigningKey;
1580 use std::os::unix::net::UnixStream;
1581
1582 let key = SigningKey::from_bytes(&[0xCC; 32]);
1584 let pub_b64 = base64::Engine::encode(
1585 &base64::engine::general_purpose::STANDARD,
1586 key.verifying_key().to_bytes(),
1587 );
1588 let keys_content = format!("trader {pub_b64} wrong-role\n");
1589 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1590
1591 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1592 primary_stream
1593 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1594 .unwrap();
1595 replica_stream
1596 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1597 .unwrap();
1598
1599 let replica_handle = std::thread::spawn(move || {
1600 let mut conn = replica_stream;
1601 authenticate_with_primary(&mut conn, &key)
1602 });
1603
1604 let mut conn = primary_stream;
1605 let result = authenticate_replica(&mut conn, &authorized_keys);
1606 assert!(result.is_err());
1607 assert!(result.unwrap_err().to_string().contains("Replication"));
1608
1609 let replica_result = replica_handle.join().unwrap();
1610 assert!(replica_result.is_err());
1611 }
1612
1613 #[test]
1616 fn auth_rejects_invalid_signature() {
1617 use ed25519_dalek::SigningKey;
1618 use std::os::unix::net::UnixStream;
1619
1620 let correct_key = SigningKey::from_bytes(&[0xDD; 32]);
1622 let pub_b64 = base64::Engine::encode(
1623 &base64::engine::general_purpose::STANDARD,
1624 correct_key.verifying_key().to_bytes(),
1625 );
1626 let keys_content = format!("replication {pub_b64} test-replica\n");
1627 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1628
1629 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1630 primary_stream
1631 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1632 .unwrap();
1633 replica_stream
1634 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1635 .unwrap();
1636
1637 let replica_handle = std::thread::spawn(move || {
1640 use melin_transport_core::replication::protocol::*;
1641
1642 let mut reader = replica_stream.try_clone().unwrap();
1643 let mut writer = replica_stream;
1644
1645 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1647 let nonce = decode_challenge(&frame).unwrap();
1648
1649 let wrong_key = SigningKey::from_bytes(&[0xEE; 32]);
1651 let bad_signature = ed25519_dalek::Signer::sign(&wrong_key, &nonce);
1652 let correct_pubkey = correct_key.verifying_key();
1653
1654 let mut buf = Vec::with_capacity(128);
1655 encode_challenge_response(
1656 &bad_signature.to_bytes(),
1657 correct_pubkey.as_bytes(),
1658 &mut buf,
1659 );
1660 writer.write_all(&buf).unwrap();
1661 writer.flush().unwrap();
1662
1663 let result_frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1665 let ok = decode_auth_result(&result_frame).unwrap();
1666 assert!(!ok, "should receive auth failure");
1667 });
1668
1669 let mut conn = primary_stream;
1670 let result = authenticate_replica(&mut conn, &authorized_keys);
1671 assert!(result.is_err());
1672 assert!(
1673 result
1674 .unwrap_err()
1675 .to_string()
1676 .contains("signature verification failed"),
1677 "should fail on signature verification"
1678 );
1679
1680 replica_handle.join().unwrap();
1681 }
1682
1683 #[test]
1684 fn sender_receiver_end_to_end() {
1685 use std::os::unix::net::UnixStream;
1686
1687 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1689
1690 let replica_handle = std::thread::spawn(move || {
1692 let mut reader = replica_stream.try_clone().unwrap();
1693 let mut writer = replica_stream;
1694
1695 let mut buf = Vec::new();
1697 let handshake = Handshake {
1698 last_sequence: 0,
1699 chain_hash: [0u8; 32],
1700 epoch: 0,
1701 };
1702 encode_handshake(&handshake, &mut buf);
1703 writer.write_all(&buf).unwrap();
1704 writer.flush().unwrap();
1705 buf.clear();
1706
1707 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1709 let msg = decode_primary_message(&frame).unwrap();
1710 assert!(matches!(msg, PrimaryMessage::StreamStart { .. }));
1711
1712 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1714 let slots: Vec<InputSlot> = try_decode_input_batch(&frame).expect("decode InputBatch");
1715 let end_seq = slots
1716 .last()
1717 .map(|s| s.sequence)
1718 .expect("InputBatch carried at least one slot");
1719
1720 let ack = Ack {
1722 acked_sequence: end_seq,
1723 in_memory_sequence: end_seq,
1724 };
1725 encode_ack(&ack, &mut buf);
1726 writer.write_all(&buf).unwrap();
1727 writer.flush().unwrap();
1728
1729 end_seq
1730 });
1731
1732 let mut p_reader = primary_stream.try_clone().unwrap();
1734 let mut p_writer = primary_stream;
1735
1736 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1738 let handshake = match decode_replica_message(&frame).unwrap() {
1739 ReplicaMessage::Handshake(h) => h,
1740 _ => panic!("expected Handshake"),
1741 };
1742 assert_eq!(handshake.last_sequence, 0);
1743
1744 let mut buf = Vec::new();
1746 encode_stream_start(0, 1, [0u8; 32], 0, 1, &mut buf); p_writer.write_all(&buf).unwrap();
1748 p_writer.flush().unwrap();
1749 buf.clear();
1750
1751 encode_input_batch_with_seq(42, &mut buf);
1753 p_writer.write_all(&buf).unwrap();
1754 p_writer.flush().unwrap();
1755 buf.clear();
1756
1757 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1759 let ack = match decode_replica_message(&frame).unwrap() {
1760 ReplicaMessage::Ack(a) => a,
1761 _ => panic!("expected Ack"),
1762 };
1763 assert_eq!(ack.acked_sequence, 42);
1764
1765 let end_seq = replica_handle.join().unwrap();
1767 assert_eq!(end_seq, 42);
1768 }
1769
1770 #[test]
1771 fn multiple_data_batches_acked_in_order() {
1772 use std::os::unix::net::UnixStream;
1775
1776 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1777
1778 let replica_handle = std::thread::spawn(move || {
1779 let mut reader = replica_stream.try_clone().unwrap();
1780 let mut writer = replica_stream;
1781 let mut buf = Vec::new();
1782
1783 encode_handshake(
1785 &Handshake {
1786 last_sequence: 0,
1787 chain_hash: [0u8; 32],
1788 epoch: 0,
1789 },
1790 &mut buf,
1791 );
1792 writer.write_all(&buf).unwrap();
1793 writer.flush().unwrap();
1794 buf.clear();
1795
1796 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1798 assert!(matches!(
1799 decode_primary_message(&frame).unwrap(),
1800 PrimaryMessage::StreamStart { .. }
1801 ));
1802
1803 let mut acked_seqs = Vec::new();
1805 for _ in 0..3 {
1806 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1807 let slots: Vec<InputSlot> =
1808 try_decode_input_batch(&frame).expect("decode InputBatch");
1809 let end_seq = slots
1810 .last()
1811 .map(|s| s.sequence)
1812 .expect("InputBatch carried at least one slot");
1813 acked_seqs.push(end_seq);
1814
1815 encode_ack(
1816 &Ack {
1817 acked_sequence: end_seq,
1818 in_memory_sequence: end_seq,
1819 },
1820 &mut buf,
1821 );
1822 writer.write_all(&buf).unwrap();
1823 writer.flush().unwrap();
1824 buf.clear();
1825 }
1826
1827 acked_seqs
1828 });
1829
1830 let mut p_reader = primary_stream.try_clone().unwrap();
1832 let mut p_writer = primary_stream;
1833 let mut buf = Vec::new();
1834
1835 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1837 assert!(matches!(
1838 decode_replica_message(&frame).unwrap(),
1839 ReplicaMessage::Handshake(_)
1840 ));
1841
1842 encode_stream_start(0, 1, [0u8; 32], 0, 1, &mut buf);
1844 p_writer.write_all(&buf).unwrap();
1845 p_writer.flush().unwrap();
1846 buf.clear();
1847
1848 for seq in [10u64, 20, 30] {
1850 encode_input_batch_with_seq(seq, &mut buf);
1851 p_writer.write_all(&buf).unwrap();
1852 p_writer.flush().unwrap();
1853 buf.clear();
1854 }
1855
1856 for expected_seq in [10u64, 20, 30] {
1858 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1859 let ack = match decode_replica_message(&frame).unwrap() {
1860 ReplicaMessage::Ack(a) => a,
1861 other => panic!("expected Ack, got {other:?}"),
1862 };
1863 assert_eq!(ack.acked_sequence, expected_seq);
1864 }
1865
1866 let acked = replica_handle.join().unwrap();
1867 assert_eq!(acked, vec![10, 20, 30]);
1868 }
1869
1870 #[test]
1871 fn heartbeat_encode_contains_sequence() {
1872 let mut buf = Vec::new();
1875 encode_heartbeat(999, 1, &mut buf);
1876
1877 let payload = &buf[4..];
1878 match decode_primary_message(payload).unwrap() {
1879 PrimaryMessage::Heartbeat { sequence, .. } => {
1880 assert_eq!(sequence, 999);
1881 }
1882 other => panic!("expected Heartbeat, got {other:?}"),
1883 }
1884 }
1885
1886 #[test]
1887 fn replica_mid_stream_handshake_with_nonzero_sequence() {
1888 use std::os::unix::net::UnixStream;
1893
1894 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1895
1896 let replica_handle = std::thread::spawn(move || {
1897 let mut reader = replica_stream.try_clone().unwrap();
1898 let mut writer = replica_stream;
1899 let mut buf = Vec::new();
1900
1901 encode_handshake(
1903 &Handshake {
1904 last_sequence: 100,
1905 chain_hash: [0xBB; 32],
1906 epoch: 0,
1907 },
1908 &mut buf,
1909 );
1910 writer.write_all(&buf).unwrap();
1911 writer.flush().unwrap();
1912 buf.clear();
1913
1914 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1916 match decode_primary_message(&frame).unwrap() {
1917 PrimaryMessage::StreamStart { start_sequence, .. } => {
1918 assert_eq!(
1919 start_sequence, 100,
1920 "StreamStart should echo replica's last_sequence"
1921 );
1922 }
1923 other => panic!("expected StreamStart, got {other:?}"),
1924 }
1925
1926 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1928 let slots: Vec<InputSlot> = try_decode_input_batch(&frame).expect("decode InputBatch");
1929 let end_sequence = slots
1930 .last()
1931 .map(|s| s.sequence)
1932 .expect("InputBatch carried at least one slot");
1933 assert!(
1934 end_sequence > 100,
1935 "InputBatch should be after replica's last_sequence"
1936 );
1937 });
1938
1939 let mut p_reader = primary_stream.try_clone().unwrap();
1941 let mut p_writer = primary_stream;
1942 let mut buf = Vec::new();
1943
1944 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1946 let handshake = match decode_replica_message(&frame).unwrap() {
1947 ReplicaMessage::Handshake(h) => h,
1948 _ => panic!("expected Handshake"),
1949 };
1950 assert_eq!(handshake.last_sequence, 100);
1951 assert_eq!(handshake.chain_hash, [0xBB; 32]);
1952
1953 encode_stream_start(handshake.last_sequence, 1, [0u8; 32], 0, 1, &mut buf);
1955 p_writer.write_all(&buf).unwrap();
1956 p_writer.flush().unwrap();
1957 buf.clear();
1958
1959 encode_input_batch_with_seq(150, &mut buf);
1961 p_writer.write_all(&buf).unwrap();
1962 p_writer.flush().unwrap();
1963
1964 replica_handle.join().unwrap();
1965 }
1966
1967 #[test]
1982 fn receiver_refuses_stream_start_from_stale_primary() {
1983 use std::os::unix::net::UnixStream;
1984
1985 use melin_transport_core::fence::FenceState;
1986
1987 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1988
1989 const STALE_EPOCH: u64 = 3;
1993 let primary_handle = std::thread::spawn(move || {
1994 let mut reader = primary_stream.try_clone().unwrap();
1995 let mut writer = primary_stream;
1996
1997 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1998 let handshake = match decode_replica_message(&frame).unwrap() {
1999 ReplicaMessage::Handshake(h) => h,
2000 _ => panic!("expected Handshake"),
2001 };
2002 assert_eq!(handshake.epoch, 5, "replica advertises its real epoch");
2005
2006 let mut buf = Vec::new();
2007 encode_stream_start(
2008 handshake.last_sequence,
2009 1,
2010 [0u8; 32],
2011 STALE_EPOCH,
2012 1,
2013 &mut buf,
2014 );
2015 writer.write_all(&buf).unwrap();
2016 writer.flush().unwrap();
2017 });
2018
2019 let fence_state = Arc::new(FenceState::new(5));
2022 let mut r_reader = replica_stream.try_clone().unwrap();
2023 let mut r_writer = replica_stream;
2024
2025 let mut buf = Vec::new();
2026 encode_handshake(
2027 &Handshake {
2028 last_sequence: 0,
2029 chain_hash: [0u8; 32],
2030 epoch: fence_state.epoch(),
2031 },
2032 &mut buf,
2033 );
2034 r_writer.write_all(&buf).unwrap();
2035 r_writer.flush().unwrap();
2036
2037 let frame = read_frame(&mut r_reader, MAX_CONTROL_FRAME).unwrap();
2038 let stream_epoch = match decode_primary_message(&frame).unwrap() {
2039 PrimaryMessage::StreamStart { epoch, .. } => epoch,
2040 other => panic!("expected StreamStart, got {other:?}"),
2041 };
2042 assert_eq!(stream_epoch, STALE_EPOCH, "epoch must survive the wire");
2043
2044 assert!(
2047 fence_state.refuses_primary(stream_epoch),
2048 "replica at epoch 5 must refuse a StreamStart from epoch {STALE_EPOCH}"
2049 );
2050 assert_eq!(fence_state.epoch(), 5, "refusal must not lower our epoch");
2051
2052 assert!(!fence_state.refuses_primary(5), "same tenure is followed");
2055 assert!(!fence_state.refuses_primary(6), "newer primary is followed");
2056
2057 primary_handle.join().unwrap();
2058 }
2059
2060 #[test]
2061 fn snapshot_begin_encode_decode_round_trip() {
2062 let mut buf = Vec::new();
2063 encode_snapshot_begin(1_000_000, 42, &[0xAB; 32], &mut buf);
2064
2065 let payload = &buf[4..];
2066 let msg = decode_primary_message(payload).unwrap();
2067 match msg {
2068 PrimaryMessage::SnapshotBegin {
2069 snapshot_len,
2070 snap_sequence,
2071 snap_chain_hash,
2072 } => {
2073 assert_eq!(snapshot_len, 1_000_000);
2074 assert_eq!(snap_sequence, 42);
2075 assert_eq!(snap_chain_hash, [0xAB; 32]);
2076 }
2077 _ => panic!("expected SnapshotBegin"),
2078 }
2079 }
2080
2081 #[test]
2082 fn snapshot_chunk_encode_decode_round_trip() {
2083 let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
2084 let mut buf = Vec::new();
2085 encode_snapshot_chunk(&data, &mut buf);
2086
2087 let payload = &buf[4..];
2088 let msg = decode_primary_message(payload).unwrap();
2089 match msg {
2090 PrimaryMessage::SnapshotChunk(chunk) => {
2091 assert_eq!(chunk, data);
2092 }
2093 _ => panic!("expected SnapshotChunk"),
2094 }
2095 }
2096
2097 #[test]
2098 fn snapshot_end_encode_decode_round_trip() {
2099 let mut buf = Vec::new();
2100 encode_snapshot_end(0xDEADBEEF, &mut buf);
2101
2102 let payload = &buf[4..];
2103 let msg = decode_primary_message(payload).unwrap();
2104 match msg {
2105 PrimaryMessage::SnapshotEnd { crc32c } => {
2106 assert_eq!(crc32c, 0xDEADBEEF);
2107 }
2108 _ => panic!("expected SnapshotEnd"),
2109 }
2110 }
2111
2112 #[test]
2116 fn snapshot_receiver_detects_length_mismatch() {
2117 use std::os::unix::net::UnixStream;
2118
2119 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2120
2121 let receiver = std::thread::spawn(move || -> String {
2123 let mut reader = replica_stream.try_clone().unwrap();
2124
2125 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2127 assert!(matches!(
2128 decode_primary_message(&frame).unwrap(),
2129 PrimaryMessage::NeedSnapshot,
2130 ));
2131
2132 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2134 let (snap_len, _snap_sequence, _snap_chain_hash) =
2135 match decode_primary_message(&frame).unwrap() {
2136 PrimaryMessage::SnapshotBegin {
2137 snapshot_len,
2138 snap_sequence,
2139 snap_chain_hash,
2140 } => (snapshot_len, snap_sequence, snap_chain_hash),
2141 other => panic!("expected SnapshotBegin, got {other:?}"),
2142 };
2143
2144 let mut received: u64 = 0;
2146 loop {
2147 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
2148 match decode_primary_message(&frame).unwrap() {
2149 PrimaryMessage::SnapshotChunk(data) => {
2150 received += data.len() as u64;
2151 }
2152 PrimaryMessage::SnapshotEnd { .. } => {
2153 if received != snap_len {
2154 return format!(
2155 "snapshot length mismatch: expected {snap_len} bytes, got {received}"
2156 );
2157 }
2158 return String::new(); }
2160 other => panic!("unexpected message: {other:?}"),
2161 }
2162 }
2163 });
2164
2165 let mut writer = primary_stream;
2167 let mut buf = Vec::new();
2168
2169 let actual_data = vec![0xAA; 100];
2170 let wrong_len = 999u64; encode_need_snapshot(&mut buf);
2173 std::io::Write::write_all(&mut writer, &buf).unwrap();
2174 buf.clear();
2175
2176 encode_snapshot_begin(wrong_len, 42, &[0xBB; 32], &mut buf);
2177 std::io::Write::write_all(&mut writer, &buf).unwrap();
2178 buf.clear();
2179
2180 encode_snapshot_chunk(&actual_data, &mut buf);
2181 std::io::Write::write_all(&mut writer, &buf).unwrap();
2182 buf.clear();
2183
2184 let crc = crc32c::crc32c(&actual_data);
2185 encode_snapshot_end(crc, &mut buf);
2186 std::io::Write::write_all(&mut writer, &buf).unwrap();
2187 std::io::Write::flush(&mut writer).unwrap();
2188
2189 let error_msg = receiver.join().unwrap();
2190 assert!(
2191 error_msg.contains("length mismatch"),
2192 "expected length mismatch error, got: {error_msg:?}"
2193 );
2194 }
2195
2196 #[test]
2200 fn snapshot_receiver_detects_crc_mismatch() {
2201 use std::os::unix::net::UnixStream;
2202
2203 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2204
2205 let receiver = std::thread::spawn(move || -> String {
2206 let mut reader = replica_stream.try_clone().unwrap();
2207
2208 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2210 assert!(matches!(
2211 decode_primary_message(&frame).unwrap(),
2212 PrimaryMessage::NeedSnapshot,
2213 ));
2214
2215 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2217 let snap_len = match decode_primary_message(&frame).unwrap() {
2218 PrimaryMessage::SnapshotBegin { snapshot_len, .. } => snapshot_len,
2219 other => panic!("expected SnapshotBegin, got {other:?}"),
2220 };
2221
2222 let mut received_data = Vec::new();
2224 let mut received: u64 = 0;
2225 loop {
2226 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
2227 match decode_primary_message(&frame).unwrap() {
2228 PrimaryMessage::SnapshotChunk(data) => {
2229 received += data.len() as u64;
2230 received_data.extend_from_slice(&data);
2231 }
2232 PrimaryMessage::SnapshotEnd {
2233 crc32c: expected_crc,
2234 } => {
2235 if received != snap_len {
2236 return format!("length mismatch: {snap_len} vs {received}");
2237 }
2238 let actual_crc = crc32c::crc32c(&received_data);
2239 if actual_crc != expected_crc {
2240 return format!(
2241 "CRC mismatch: expected {expected_crc:#x}, got {actual_crc:#x}"
2242 );
2243 }
2244 return String::new();
2245 }
2246 other => panic!("unexpected message: {other:?}"),
2247 }
2248 }
2249 });
2250
2251 let mut writer = primary_stream;
2253 let mut buf = Vec::new();
2254
2255 let data = vec![0xAA; 100];
2256
2257 encode_need_snapshot(&mut buf);
2258 std::io::Write::write_all(&mut writer, &buf).unwrap();
2259 buf.clear();
2260
2261 encode_snapshot_begin(data.len() as u64, 42, &[0xBB; 32], &mut buf);
2262 std::io::Write::write_all(&mut writer, &buf).unwrap();
2263 buf.clear();
2264
2265 encode_snapshot_chunk(&data, &mut buf);
2266 std::io::Write::write_all(&mut writer, &buf).unwrap();
2267 buf.clear();
2268
2269 let wrong_crc = !crc32c::crc32c(&data);
2271 encode_snapshot_end(wrong_crc, &mut buf);
2272 std::io::Write::write_all(&mut writer, &buf).unwrap();
2273 std::io::Write::flush(&mut writer).unwrap();
2274
2275 let error_msg = receiver.join().unwrap();
2276 assert!(
2277 error_msg.contains("CRC mismatch"),
2278 "expected CRC mismatch error, got: {error_msg:?}"
2279 );
2280 }
2281
2282 #[test]
2285 fn snapshot_receiver_detects_chain_hash_mismatch() {
2286 use std::os::unix::net::UnixStream;
2287
2288 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2289
2290 let receiver = std::thread::spawn(move || -> String {
2291 let mut reader = replica_stream.try_clone().unwrap();
2292
2293 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2294 assert!(matches!(
2295 decode_primary_message(&frame).unwrap(),
2296 PrimaryMessage::NeedSnapshot,
2297 ));
2298
2299 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2300 let (snap_len, _snap_sequence, snap_chain_hash) =
2301 match decode_primary_message(&frame).unwrap() {
2302 PrimaryMessage::SnapshotBegin {
2303 snapshot_len,
2304 snap_sequence,
2305 snap_chain_hash,
2306 } => (snapshot_len, snap_sequence, snap_chain_hash),
2307 other => panic!("expected SnapshotBegin, got {other:?}"),
2308 };
2309
2310 let mut received_data = Vec::new();
2312 let mut received: u64 = 0;
2313 loop {
2314 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
2315 match decode_primary_message(&frame).unwrap() {
2316 PrimaryMessage::SnapshotChunk(data) => {
2317 received += data.len() as u64;
2318 received_data.extend_from_slice(&data);
2319 }
2320 PrimaryMessage::SnapshotEnd {
2321 crc32c: expected_crc,
2322 } => {
2323 assert_eq!(received, snap_len, "length should match");
2324 let actual_crc = crc32c::crc32c(&received_data);
2325 assert_eq!(actual_crc, expected_crc, "CRC should match");
2326 break;
2327 }
2328 other => panic!("unexpected message: {other:?}"),
2329 }
2330 }
2331
2332 let loaded_hash = [0xFF; 32]; if loaded_hash != snap_chain_hash {
2336 return format!(
2337 "snapshot chain hash mismatch: primary sent {snap_chain_hash:02x?}, \
2338 loaded snapshot has {loaded_hash:02x?}"
2339 );
2340 }
2341 String::new()
2342 });
2343
2344 let mut writer = primary_stream;
2347 let mut buf = Vec::new();
2348
2349 let data = vec![0xAA; 64];
2350 let advertised_hash = [0xBB; 32];
2352
2353 encode_need_snapshot(&mut buf);
2354 std::io::Write::write_all(&mut writer, &buf).unwrap();
2355 buf.clear();
2356
2357 encode_snapshot_begin(data.len() as u64, 10, &advertised_hash, &mut buf);
2358 std::io::Write::write_all(&mut writer, &buf).unwrap();
2359 buf.clear();
2360
2361 encode_snapshot_chunk(&data, &mut buf);
2362 std::io::Write::write_all(&mut writer, &buf).unwrap();
2363 buf.clear();
2364
2365 let crc = crc32c::crc32c(&data);
2366 encode_snapshot_end(crc, &mut buf);
2367 std::io::Write::write_all(&mut writer, &buf).unwrap();
2368 std::io::Write::flush(&mut writer).unwrap();
2369
2370 let error_msg = receiver.join().unwrap();
2371 assert!(
2372 error_msg.contains("chain hash mismatch"),
2373 "expected chain hash mismatch error, got: {error_msg:?}"
2374 );
2375 }
2376
2377 #[test]
2380 fn primary_rejects_snapshot_with_invalid_magic() {
2381 let tmp = tempfile::tempdir().unwrap();
2382 let snap_path = tmp.path().join("test.snapshot");
2383
2384 let mut bad_snap = vec![0u8; 64];
2386 bad_snap[0..4].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes()); std::fs::write(&snap_path, &bad_snap).unwrap();
2389
2390 let snap_data = std::fs::read(&snap_path).unwrap();
2392 assert!(
2393 snap_data.len() >= 48,
2394 "file should be big enough for header"
2395 );
2396
2397 let magic = u32::from_le_bytes(snap_data[0..4].try_into().unwrap());
2398 assert_ne!(magic, 0x534E_4150);
2399 assert_eq!(magic, 0xDEAD_BEEF);
2400 }
2401
2402 #[test]
2405 fn primary_rejects_snapshot_too_small_for_header() {
2406 let tmp = tempfile::tempdir().unwrap();
2407 let snap_path = tmp.path().join("test.snapshot");
2408
2409 std::fs::write(&snap_path, [0u8; 20]).unwrap();
2411
2412 let snap_data = std::fs::read(&snap_path).unwrap();
2413 assert!(
2414 snap_data.len() < 48,
2415 "file must be too small for header validation"
2416 );
2417 }
2418
2419 #[test]
2420 fn decode_snapshot_begin_too_short() {
2421 let payload = [MSG_SNAPSHOT_BEGIN, 0x01, 0x02, 0x03];
2424 let err = decode_primary_message(&payload).unwrap_err();
2425 assert!(
2426 err.to_string().contains("SnapshotBegin too short"),
2427 "expected 'SnapshotBegin too short', got: {err}"
2428 );
2429 }
2430
2431 #[test]
2432 fn decode_snapshot_end_too_short() {
2433 let payload = [MSG_SNAPSHOT_END];
2435 let err = decode_primary_message(&payload).unwrap_err();
2436 assert!(
2437 err.to_string().contains("SnapshotEnd too short"),
2438 "expected 'SnapshotEnd too short', got: {err}"
2439 );
2440 }
2441
2442 #[test]
2443 fn decode_snapshot_chunk_empty_data() {
2444 let payload = [MSG_SNAPSHOT_CHUNK];
2446 let msg = decode_primary_message(&payload).unwrap();
2447 match msg {
2448 PrimaryMessage::SnapshotChunk(data) => {
2449 assert!(data.is_empty());
2450 }
2451 _ => panic!("expected SnapshotChunk"),
2452 }
2453 }
2454
2455 fn make_journal_cursor(val: u64) -> melin_pipeline::padding::Sequence {
2458 melin_pipeline::padding::Sequence::new(AtomicU64::new(val))
2459 }
2460
2461 #[test]
2462 fn pending_ack_queue_push_and_pop_ready() {
2463 let mut q = PendingAckQueue::new(8);
2464 assert!(q.is_empty());
2465 assert!(!q.is_full());
2466
2467 q.push(10, 100);
2468 q.push(20, 200);
2469 assert!(!q.is_empty());
2470
2471 let cursor = make_journal_cursor(5);
2473 assert!(q.pop_ready(&cursor).is_none());
2474
2475 cursor.get().store(15, Ordering::Relaxed);
2477 assert_eq!(q.pop_ready(&cursor), Some(100));
2478 assert!(!q.is_empty());
2480
2481 cursor.get().store(25, Ordering::Relaxed);
2483 assert_eq!(q.pop_ready(&cursor), Some(200));
2484 assert!(q.is_empty());
2485 }
2486
2487 #[test]
2488 fn pending_ack_queue_pop_ready_returns_highest_sequence() {
2489 let mut q = PendingAckQueue::new(8);
2493 q.push(10, 100);
2494 q.push(20, 200);
2495 q.push(30, 300);
2496
2497 let cursor = make_journal_cursor(30);
2498 assert_eq!(q.pop_ready(&cursor), Some(300));
2499 assert!(q.is_empty());
2500 }
2501
2502 #[test]
2503 fn pending_ack_queue_capacity_and_full() {
2504 let mut q = PendingAckQueue::new(8);
2505 for i in 0..8 {
2506 assert!(!q.is_full());
2507 q.push(i as u64 + 1, (i + 1) as u64 * 100);
2508 }
2509 assert!(q.is_full());
2510 }
2511
2512 #[test]
2517 fn pending_ack_queue_merges_into_the_newest_entry_when_full() {
2518 let mut q = PendingAckQueue::new(4);
2519 for i in 1..=4u64 {
2520 q.push(i * 10, i * 100);
2521 }
2522 assert!(q.is_full());
2523 assert_eq!(q.merged(), 0);
2524
2525 q.push(50, 500);
2527 assert_eq!(q.merged(), 1);
2528 assert_eq!(q.len(), 4, "merge must not grow the queue");
2529
2530 let cursor = make_journal_cursor(40);
2533 assert_eq!(q.pop_ready(&cursor), Some(300));
2534 assert!(!q.is_empty(), "merged entry is not durable yet");
2535
2536 cursor.get().store(50, Ordering::Relaxed);
2539 assert_eq!(q.pop_ready(&cursor), Some(500));
2540 assert!(q.is_empty());
2541 }
2542
2543 #[test]
2546 fn pending_ack_queue_merges_repeatedly_without_growing() {
2547 let mut q = PendingAckQueue::new(2);
2548 for i in 1..=100u64 {
2549 q.push(i, i * 10);
2550 }
2551 assert_eq!(q.len(), 2);
2552 assert_eq!(q.merged(), 98);
2553
2554 let cursor = make_journal_cursor(99);
2556 assert_eq!(q.pop_ready(&cursor), Some(10), "only the first entry");
2557 cursor.get().store(100, Ordering::Relaxed);
2559 assert_eq!(q.pop_ready(&cursor), Some(1000));
2560 }
2561
2562 #[test]
2563 fn pending_ack_queue_pop_oldest_blocking() {
2564 let mut q = PendingAckQueue::new(8);
2565 q.push(10, 100);
2566 q.push(20, 200);
2567
2568 let cursor = make_journal_cursor(25);
2571 let seq = q.pop_oldest_blocking(&cursor, true, &AtomicBool::new(false));
2572 assert_eq!(seq, Some(200));
2574 assert!(q.is_empty());
2575 }
2576
2577 #[test]
2580 fn pending_ack_queue_blocking_wait_aborts_on_journal_failure() {
2581 let mut q = PendingAckQueue::new(8);
2582 q.push(10, 100);
2583
2584 let cursor = make_journal_cursor(5);
2586 let abort = AtomicBool::new(true);
2587 assert_eq!(q.pop_oldest_blocking(&cursor, true, &abort), None);
2588 assert!(!q.is_empty(), "aborted wait must not pop the entry");
2589 assert_eq!(q.pop_all_blocking(&cursor, true, &abort), None);
2590 }
2591
2592 #[test]
2601 fn shutdown_pipeline_surfaces_journal_error_kind() {
2602 let flag = AtomicBool::new(false);
2603 let matching_joined = Arc::new(AtomicBool::new(false));
2604 let mj = Arc::clone(&matching_joined);
2605
2606 let journal = std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> {
2607 Err(melin_journal::JournalError::ReplicaChainDivergence {
2608 sequence: 42,
2609 expected: [1u8; 32],
2610 actual: [2u8; 32],
2611 })
2612 });
2613 let matching = std::thread::spawn(move || {
2614 mj.store(true, Ordering::Release);
2615 7u64
2616 });
2617 let drain = std::thread::spawn(|| {});
2618
2619 let outcome = shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None);
2620 assert!(flag.load(Ordering::Acquire), "shutdown flag must be set");
2621 assert!(
2622 matching_joined.load(Ordering::Acquire),
2623 "matching thread must be joined even when the journal failed"
2624 );
2625 match outcome {
2626 TeardownOutcome::JournalFailed(
2627 melin_journal::JournalError::ReplicaChainDivergence { sequence, .. },
2628 ) => assert_eq!(sequence, 42),
2629 _ => panic!("expected JournalFailed(ReplicaChainDivergence)"),
2630 }
2631 }
2632
2633 #[test]
2634 fn shutdown_pipeline_clean_returns_both_states() {
2635 let flag = AtomicBool::new(false);
2636 let journal =
2637 std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> { Ok(11u32) });
2638 let matching = std::thread::spawn(|| 7u64);
2639 let drain = std::thread::spawn(|| {});
2640
2641 match shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None) {
2642 TeardownOutcome::Clean(app, writer) => {
2643 assert_eq!(app, 7);
2644 assert_eq!(writer, 11);
2645 }
2646 _ => panic!("expected Clean"),
2647 }
2648 }
2649
2650 #[test]
2651 fn shutdown_pipeline_panicked_stage_reports_panicked() {
2652 let flag = AtomicBool::new(false);
2653 let journal =
2654 std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> { Ok(11u32) });
2655 let matching = std::thread::spawn(|| -> u64 { panic!("matching stage died") });
2656 let drain = std::thread::spawn(|| {});
2657
2658 assert!(matches!(
2659 shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None),
2660 TeardownOutcome::Panicked
2661 ));
2662 }
2663
2664 fn teardown_fixture(
2669 capacity: usize,
2670 ) -> (
2671 ReplicaPipelineHandles<counter_server::Counter, u32>,
2672 melin_pipeline::ring::Consumer<InputSlot>,
2673 ) {
2674 use melin_app::app_factory::AppFactory;
2675 let (input_producer, mut consumers) =
2676 melin_pipeline::ring::DisruptorBuilder::<InputSlot>::new(capacity)
2677 .add_consumer()
2678 .build();
2679 let consumer = consumers.pop().expect("one consumer");
2680 let handles = ReplicaPipelineHandles {
2681 input_producer,
2682 journal_cursor: Arc::new(make_journal_cursor(0)),
2683 last_seq: melin_transport_core::DurableWireSeqCursor::detached(
2684 melin_transport_core::WireSeq::new(0),
2685 ),
2686 chain_hash_lock: None,
2687 stream_marks: Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new())),
2688 journal_failed: Arc::new(AtomicBool::new(false)),
2689 pipeline_shutdown: Arc::new(AtomicBool::new(false)),
2690 journal_handle: std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> {
2691 Ok(11)
2692 }),
2693 matching_handle: std::thread::spawn(|| counter_server::CounterFactory.empty()),
2694 drain_handle: std::thread::spawn(|| {}),
2695 shadow_handle: None,
2696 };
2697 (handles, consumer)
2698 }
2699
2700 #[test]
2706 fn teardown_publishes_shutdown_sentinel() {
2707 let (handles, mut consumer) = teardown_fixture(8);
2708 assert!(matches!(
2709 teardown_replica_pipeline::<counter_server::Counter, u32>(handles),
2710 TeardownOutcome::Clean(..)
2711 ));
2712 let (_, slot) = consumer.try_consume().expect("sentinel on the ring");
2713 assert!(
2714 slot.event.is_shutdown(),
2715 "published slot must be the sentinel"
2716 );
2717 assert!(consumer.try_consume().is_none(), "exactly one sentinel");
2718 }
2719
2720 fn backoff_after_disconnect(heard_from_primary: bool) -> std::time::Duration {
2727 let dir = tempfile::tempdir().expect("tempdir");
2728 type Writer = melin_journal::BufferedWriter<CounterEvent>;
2729 let mut pipeline: Option<ReplicaPipelineHandles<counter_server::Counter, Writer>> = None;
2730 let mut divergence_resyncs = 0u32;
2731 let mut backoff = MAX_BACKOFF;
2732 let shutdown = AtomicBool::new(true);
2733 let promote = crate::promotion::PromotionRequest::new();
2734 let after = handle_session_exit::<counter_server::Counter, Writer>(
2735 StreamingResult {
2736 exit: SessionExit::Disconnected,
2737 heard_from_primary,
2738 },
2739 &mut pipeline,
2740 &mut divergence_resyncs,
2741 &mut backoff,
2742 0,
2743 &dir.path().join("r.journal"),
2744 &dir.path().join("r.snapshot"),
2745 &counter_server::CounterFactory,
2746 &melin_transport_core::fence::FenceState::new(0),
2747 &shutdown,
2748 &promote,
2749 || {},
2750 );
2751 assert!(matches!(after, AfterSession::Reconnect));
2752 backoff
2753 }
2754
2755 #[test]
2760 fn disconnect_resets_backoff_when_primary_spoke() {
2761 assert_eq!(
2764 backoff_after_disconnect(true),
2765 std::time::Duration::from_secs(2)
2766 );
2767 }
2768
2769 #[test]
2774 fn disconnect_keeps_escalated_backoff_when_primary_silent() {
2775 assert_eq!(backoff_after_disconnect(false), MAX_BACKOFF);
2776 }
2777
2778 #[test]
2787 fn stream_gap_reconnects_and_keeps_pipeline() {
2788 use melin_app::app_factory::AppFactory;
2789 let dir = tempfile::tempdir().expect("tempdir");
2790 type Writer = melin_journal::BufferedWriter<CounterEvent>;
2791 let (input_producer, mut consumers) =
2795 melin_pipeline::ring::DisruptorBuilder::<InputSlot>::new(4)
2796 .add_consumer()
2797 .build();
2798 let _consumer = consumers.pop().expect("one consumer");
2799 let writer_path = dir.path().join("w.journal");
2800 let handles = ReplicaPipelineHandles {
2801 input_producer,
2802 journal_cursor: Arc::new(make_journal_cursor(0)),
2803 last_seq: melin_transport_core::DurableWireSeqCursor::detached(
2804 melin_transport_core::WireSeq::new(0),
2805 ),
2806 chain_hash_lock: None,
2807 stream_marks: Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new())),
2808 journal_failed: Arc::new(AtomicBool::new(false)),
2809 pipeline_shutdown: Arc::new(AtomicBool::new(false)),
2810 journal_handle: std::thread::spawn(move || Writer::create(&writer_path)),
2811 matching_handle: std::thread::spawn(|| counter_server::CounterFactory.empty()),
2812 drain_handle: std::thread::spawn(|| {}),
2813 shadow_handle: None,
2814 };
2815 let mut pipeline: Option<ReplicaPipelineHandles<counter_server::Counter, Writer>> =
2816 Some(handles);
2817 let mut divergence_resyncs = 0u32;
2818 let mut backoff = MAX_BACKOFF;
2819 let shutdown = AtomicBool::new(true);
2820 let promote = crate::promotion::PromotionRequest::new();
2821 let mut closed = false;
2822 let after = handle_session_exit::<counter_server::Counter, Writer>(
2823 StreamingResult {
2824 exit: SessionExit::StreamGap("sequence gap: expected 3, got 5".into()),
2825 heard_from_primary: true,
2826 },
2827 &mut pipeline,
2828 &mut divergence_resyncs,
2829 &mut backoff,
2830 2,
2831 &dir.path().join("r.journal"),
2832 &dir.path().join("r.snapshot"),
2833 &counter_server::CounterFactory,
2834 &melin_transport_core::fence::FenceState::new(0),
2835 &shutdown,
2836 &promote,
2837 || closed = true,
2838 );
2839 assert!(
2840 matches!(after, AfterSession::Reconnect),
2841 "a stream gap must reconnect, not return an error"
2842 );
2843 assert!(
2844 pipeline.is_some(),
2845 "the pipeline must survive for the reconnect"
2846 );
2847 assert!(closed, "the transport must be closed before redialing");
2848 assert_eq!(
2849 backoff,
2850 std::time::Duration::from_secs(2),
2851 "backoff reset to 1s then doubled by the shared helper"
2852 );
2853 assert_eq!(divergence_resyncs, 0, "not a resync");
2854 if let Some(p) = pipeline.take() {
2855 let _ = teardown_replica_pipeline::<counter_server::Counter, Writer>(p);
2856 }
2857 }
2858
2859 #[test]
2864 fn teardown_skips_sentinel_when_journal_failed_and_ring_full() {
2865 let (mut handles, mut consumer) = teardown_fixture(4);
2866 for seq in 0..4 {
2867 assert!(
2868 handles
2869 .input_producer
2870 .try_publish(InputSlot {
2871 sequence: seq,
2872 ..InputSlot::default()
2873 })
2874 .is_ok(),
2875 "ring must have space while filling"
2876 );
2877 }
2878 assert!(
2879 handles
2880 .input_producer
2881 .try_publish(InputSlot::default())
2882 .is_err(),
2883 "ring must be full"
2884 );
2885 handles.journal_failed.store(true, Ordering::Release);
2886 assert!(matches!(
2888 teardown_replica_pipeline::<counter_server::Counter, u32>(handles),
2889 TeardownOutcome::Clean(..)
2890 ));
2891 while let Some((_, slot)) = consumer.try_consume() {
2892 assert!(!slot.event.is_shutdown(), "no sentinel may be published");
2893 }
2894 }
2895
2896 #[test]
2897 fn pending_ack_queue_wraps_around() {
2898 let mut q = PendingAckQueue::new(8);
2899 let cursor = make_journal_cursor(100);
2900
2901 for round in 0..3 {
2903 for i in 0..8 {
2904 let target = (round * 8 + i) as u64 + 1;
2905 q.push(target, target * 10);
2906 }
2907 assert!(q.is_full());
2908 let seq = q.pop_ready(&cursor).expect("should be ready");
2909 assert_eq!(seq, (round * 8 + 8) as u64 * 10);
2910 assert!(q.is_empty());
2911 }
2912 }
2913
2914 #[test]
2915 fn pending_ack_queue_pop_all_blocking_empty() {
2916 let mut q = PendingAckQueue::new(8);
2917 let cursor = make_journal_cursor(0);
2918 assert!(
2919 q.pop_all_blocking(&cursor, true, &AtomicBool::new(false))
2920 .is_none()
2921 );
2922 }
2923
2924 #[test]
2927 fn dual_track_returns_none_when_both_tracks_idle() {
2928 let mut q = PendingAckQueue::new(8);
2929 let cursor = make_journal_cursor(0);
2930 assert!(
2931 try_flush_dual_track(&mut q, &cursor, 0, 0, 0).is_none(),
2932 "no advance on either track → no ack"
2933 );
2934 }
2935
2936 #[test]
2937 fn dual_track_fires_on_persisted_advance_only() {
2938 let mut q = PendingAckQueue::new(8);
2943 q.push(10, 100);
2944 let cursor = make_journal_cursor(20);
2945 let ack = try_flush_dual_track(&mut q, &cursor, 50, 0, 50).expect("persisted advanced");
2946 assert_eq!(ack.acked_sequence, 100);
2947 assert_eq!(ack.in_memory_sequence, 50);
2948 }
2949
2950 #[test]
2951 fn dual_track_fires_on_in_memory_advance_only() {
2952 let mut q = PendingAckQueue::new(8);
2955 let cursor = make_journal_cursor(0);
2956 let ack = try_flush_dual_track(&mut q, &cursor, 200, 100, 100).expect("in-memory advanced");
2957 assert_eq!(ack.acked_sequence, 100);
2958 assert_eq!(ack.in_memory_sequence, 200);
2959 }
2960
2961 #[test]
2962 fn dual_track_coalesces_until_caller_updates_trackers() {
2963 let mut q = PendingAckQueue::new(8);
2969 q.push(10, 100);
2970 let cursor = make_journal_cursor(20);
2971 let ack1 = try_flush_dual_track(&mut q, &cursor, 50, 0, 50).expect("call 1 fires");
2972 assert_eq!(ack1.acked_sequence, 100);
2973 let ack2 = try_flush_dual_track(&mut q, &cursor, 80, 0, 50)
2975 .expect("call 2 fires on in-memory advance");
2976 assert_eq!(
2977 ack2.acked_sequence, 0,
2978 "no new persisted pop → unwrap_or(last_sent)"
2979 );
2980 assert_eq!(ack2.in_memory_sequence, 80);
2981 }
2982
2983 #[test]
2984 fn dual_track_no_duplicate_ack_after_backpressure_drain() {
2985 let mut q = PendingAckQueue::new(4);
3003 for i in 1..=4u64 {
3006 q.push(i * 10, i * 100);
3007 }
3008 assert!(q.is_full());
3009
3010 let cursor = make_journal_cursor(40);
3013 let drained = q.pop_ready(&cursor).expect("all entries durable");
3014 assert_eq!(drained, 400);
3015 assert!(q.is_empty());
3016
3017 let mut last_sent_acked = drained;
3020 let mut last_sent_in_mem = 450u64;
3021
3022 assert!(
3027 try_flush_dual_track(
3028 &mut q,
3029 &cursor,
3030 last_sent_in_mem,
3031 last_sent_acked,
3032 last_sent_in_mem,
3033 )
3034 .is_none(),
3035 "no advance on either track after backpressure drain → no duplicate ack",
3036 );
3037
3038 q.push(50, 500);
3041 let cursor2 = make_journal_cursor(50);
3042 let ack = try_flush_dual_track(&mut q, &cursor2, 500, last_sent_acked, last_sent_in_mem)
3043 .expect("fresh batch after drain fires a new ack");
3044 assert!(
3045 ack.acked_sequence >= last_sent_acked,
3046 "regression: drain sent {last_sent_acked} but next ack carries {}",
3047 ack.acked_sequence,
3048 );
3049 assert_eq!(ack.acked_sequence, 500);
3050 assert_eq!(ack.in_memory_sequence, 500);
3051
3052 last_sent_acked = ack.acked_sequence;
3053 last_sent_in_mem = ack.in_memory_sequence;
3054
3055 assert!(
3059 try_flush_dual_track(
3060 &mut q,
3061 &cursor2,
3062 last_sent_in_mem,
3063 last_sent_acked,
3064 last_sent_in_mem,
3065 )
3066 .is_none(),
3067 "post-resume idle → no further ack",
3068 );
3069 }
3070}