1use std::sync::Arc;
59#[cfg(test)]
60use std::sync::atomic::AtomicU64;
61use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
62
63use melin_journal::JournalWrite;
64use melin_transport_core::pipeline::{JournalStage, JournalStageRun};
65
66use melin_app::Application;
67use melin_transport_core::pipeline::{InputSlot, OutputSlot};
68use melin_transport_core::replication::archive::{ArchiveReason, archive_local_lineage};
69use melin_transport_core::replication::protocol::{MAX_CONTROL_FRAME, decode_primary_message};
70
71mod auth;
72#[cfg(feature = "dpdk")]
73mod dpdk;
74mod receiver_transport;
75mod tcp_receiver;
76mod tcp_sender;
77
78use receiver_transport::{ControlFrameSource, SessionExit, StreamingResult, receive_chunked_body};
79
80pub(crate) struct ReplicaGate<'a> {
93 count: &'a AtomicU32,
94}
95
96impl<'a> ReplicaGate<'a> {
97 pub(crate) fn new(count: &'a AtomicU32) -> Self {
98 Self { count }
99 }
100
101 pub(crate) fn lift(&self) {
104 self.count.fetch_add(1, Ordering::Release);
105 }
106
107 pub(crate) fn lower(&self) -> bool {
114 let was_last = self.count.fetch_sub(1, Ordering::Release) == 1;
115 if was_last {
116 tracing::warn!("all replicas disconnected — trading halted");
117 }
118 was_last
119 }
120}
121
122pub use melin_transport_core::replication::ack_queue::{
129 PendingAck, PendingAckQueue, try_flush_dual_track, wait_for_journal_cursor,
130};
131pub use melin_transport_core::replication::protocol::{
132 Ack, Handshake, PrimaryMessage, ReplicaMessage,
133};
134pub use melin_transport_core::replication::{ReplicaCursors, ReplicationMetrics, SentHighWater};
135
136#[cfg(feature = "dpdk")]
137pub use dpdk::{DpdkReplicationDriver, run_receiver_dpdk};
138pub use tcp_receiver::{ReceiverResult, run_receiver};
139pub use tcp_sender::{ReplicationListener, Sender, run_sender};
140
141#[derive(Clone)]
148pub struct ReplicaControlPlane {
149 pub promote: crate::promotion::PromotionRequest,
152 pub tip_ready: std::sync::Arc<AtomicBool>,
156 pub journal_tip: melin_transport_core::AdvertisedJournalTip,
159 pub primary_link_up: std::sync::Arc<AtomicBool>,
166 pub primary_acking_mode: std::sync::Arc<std::sync::atomic::AtomicU8>,
176 pub pipeline_healthy: std::sync::Arc<AtomicBool>,
184}
185
186impl ReplicaControlPlane {
187 pub fn new() -> Self {
191 Self {
192 promote: crate::promotion::PromotionRequest::new(),
193 tip_ready: std::sync::Arc::new(AtomicBool::new(false)),
194 journal_tip: melin_transport_core::AdvertisedJournalTip::new(
195 melin_transport_core::WireSeq::new(0),
196 ),
197 primary_link_up: std::sync::Arc::new(AtomicBool::new(false)),
198 primary_acking_mode: std::sync::Arc::new(std::sync::atomic::AtomicU8::new(
199 crate::durability_policy::ACKING_MODE_UNKNOWN,
200 )),
201 pipeline_healthy: std::sync::Arc::new(AtomicBool::new(true)),
202 }
203 }
204}
205
206impl Default for ReplicaControlPlane {
207 fn default() -> Self {
208 Self::new()
209 }
210}
211
212pub(super) fn log_tcp_info(fd: std::os::unix::io::RawFd, tag: &str, slot: usize) {
221 if !tracing::enabled!(tracing::Level::DEBUG) {
222 return;
223 }
224 let mut info: libc::tcp_info = unsafe { std::mem::zeroed() };
228 let mut len = std::mem::size_of::<libc::tcp_info>() as libc::socklen_t;
229 let rc = unsafe {
230 libc::getsockopt(
231 fd,
232 libc::IPPROTO_TCP,
233 libc::TCP_INFO,
234 &mut info as *mut _ as *mut libc::c_void,
235 &mut len,
236 )
237 };
238 if rc != 0 {
239 tracing::debug!(
240 slot,
241 tag,
242 errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
243 "tcp_info getsockopt failed"
244 );
245 return;
246 }
247 tracing::debug!(
248 slot,
249 tag,
250 state = info.tcpi_state,
251 ca_state = info.tcpi_ca_state,
252 rtt_us = info.tcpi_rtt,
253 rttvar_us = info.tcpi_rttvar,
254 snd_cwnd = info.tcpi_snd_cwnd,
255 snd_ssthresh = info.tcpi_snd_ssthresh,
256 snd_mss = info.tcpi_snd_mss,
257 rcv_mss = info.tcpi_rcv_mss,
258 unacked = info.tcpi_unacked,
259 retrans = info.tcpi_retrans,
260 total_retrans = info.tcpi_total_retrans,
261 lost = info.tcpi_lost,
262 rcv_space = info.tcpi_rcv_space,
263 rto_us = info.tcpi_rto,
264 "tcp_info"
265 );
266}
267
268pub(super) fn sleep_checking_flags(
272 duration: std::time::Duration,
273 shutdown: &AtomicBool,
274 promote: &crate::promotion::PromotionRequest,
275) {
276 let deadline = std::time::Instant::now() + duration;
277 while std::time::Instant::now() < deadline {
278 if shutdown.load(Ordering::Relaxed) || promote.is_requested() {
279 return;
280 }
281 std::thread::sleep(std::time::Duration::from_millis(100));
282 }
283}
284
285pub(super) fn sleep_then_double_backoff(
292 backoff: &mut std::time::Duration,
293 shutdown: &AtomicBool,
294 promote: &crate::promotion::PromotionRequest,
295) {
296 sleep_checking_flags(*backoff, shutdown, promote);
297 *backoff = (*backoff * 2).min(MAX_BACKOFF);
298}
299
300pub(super) enum TeardownOutcome<A, W> {
304 Clean(A, W),
307 JournalFailed(melin_journal::JournalError),
313 Panicked,
315}
316
317pub(super) fn shutdown_pipeline<A: Send + 'static, W: Send + 'static>(
332 shutdown_flag: &AtomicBool,
333 journal_handle: std::thread::JoinHandle<Result<W, melin_journal::JournalError>>,
334 matching_handle: std::thread::JoinHandle<A>,
335 drain_handle: std::thread::JoinHandle<()>,
336 shadow_handle: Option<std::thread::JoinHandle<()>>,
337) -> TeardownOutcome<A, W> {
338 shutdown_flag.store(true, Ordering::Release);
352 let journal_result = journal_handle.join();
357 let matching_result = matching_handle.join();
358 let _ = drain_handle.join();
359 if let Some(h) = shadow_handle {
360 let _ = h.join();
361 }
362 let writer = match journal_result {
363 Ok(Ok(w)) => w,
364 Ok(Err(e)) => return TeardownOutcome::JournalFailed(e),
366 Err(_) => return TeardownOutcome::Panicked,
367 };
368 match matching_result {
369 Ok(exchange) => TeardownOutcome::Clean(exchange, writer),
370 Err(_) => TeardownOutcome::Panicked,
371 }
372}
373
374pub(super) struct ReplicaPipelineHandles<A: Application, W: Send + 'static> {
383 pub(super) input_producer: melin_pipeline::ring::Producer<InputSlot<A::Event>>,
384 pub(super) journal_cursor: Arc<melin_pipeline::padding::Sequence>,
385 pub(super) last_seq: melin_transport_core::DurableWireSeqCursor,
391 pub(super) chain_hash_lock:
395 Option<melin_pipeline::seqlock::SeqLockReader<melin_transport_core::pipeline::FsyncState>>,
396 pub(super) stream_marks: melin_transport_core::pipeline::StreamMarkQueue,
401 pub(super) journal_failed: Arc<AtomicBool>,
408 pub(super) pipeline_shutdown: Arc<AtomicBool>,
411 pub(super) journal_handle: std::thread::JoinHandle<Result<W, melin_journal::JournalError>>,
412 pub(super) matching_handle: std::thread::JoinHandle<A>,
413 pub(super) drain_handle: std::thread::JoinHandle<()>,
414 pub(super) shadow_handle: Option<std::thread::JoinHandle<()>>,
415}
416
417#[allow(clippy::too_many_arguments)]
421pub(super) fn build_replica_pipeline_with_threads<A, W>(
422 exchange: A,
423 writer: W,
424 cores: crate::server::PipelineCores,
425 snapshot_interval_ms: u64,
426 snapshot_path: std::path::PathBuf,
427 group_commit_delay: std::time::Duration,
428 busy_spin: bool,
429 fence_state: Arc<melin_transport_core::fence::FenceState>,
430 pipeline_healthy: Arc<AtomicBool>,
436) -> Result<ReplicaPipelineHandles<A, W>, Box<dyn std::error::Error>>
437where
438 A: Application + Send + 'static,
439 A::Event: Send + Sync + 'static,
440 A::Report: Send + 'static,
441 A::QueryResponse: Send + 'static,
442 W: JournalWrite<A::Event> + Send + 'static,
443 JournalStage<A::Event, W>: JournalStageRun<A::Event, Writer = W>,
444{
445 let shadow_exchange = <A as Application>::clone_via_snapshot(&exchange)?;
446
447 let enable_shadow = snapshot_interval_ms > 0;
448 let shadow_initial_epoch = fence_state.epoch();
451 let pipeline = melin_transport_core::pipeline::build_replica_pipeline(
452 exchange,
453 writer,
454 4096, group_commit_delay,
456 busy_spin,
457 enable_shadow,
458 fence_state,
459 );
460
461 let pipeline_shutdown = Arc::new(AtomicBool::new(false));
462
463 let ps = Arc::clone(&pipeline_shutdown);
464 let journal_core = cores.journal;
465 let mut journal_stage = pipeline.journal_stage;
466 let stream_marks: melin_transport_core::pipeline::StreamMarkQueue =
471 Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new()));
472 journal_stage.set_stream_marks(Arc::clone(&stream_marks));
473 journal_stage.set_preparer_core(cores.journal_prep);
474 let journal_failed = Arc::new(AtomicBool::new(false));
475 let journal_failed_latch = Arc::clone(&journal_failed);
476 pipeline_healthy.store(true, Ordering::Release);
479 let journal_handle = std::thread::Builder::new()
480 .name("journal".into())
481 .spawn(move || {
482 melin_app::affinity::pin_thread("journal", journal_core);
483 let result = journal_stage.run(&ps);
484 if let Err(ref e) = result {
485 journal_failed_latch.store(true, Ordering::Release);
492 pipeline_healthy.store(false, Ordering::Release);
495 tracing::error!(error = %e, "replica journal stage failed — session teardown");
496 }
497 result
498 })
499 .expect("spawn journal thread");
500
501 let ps = Arc::clone(&pipeline_shutdown);
502 let matching_core = cores.matching;
503 let matching_stage = pipeline.matching_stage;
504 let matching_handle = std::thread::Builder::new()
505 .name("matching".into())
506 .spawn(move || {
507 melin_app::affinity::pin_thread("matching", matching_core);
508 matching_stage.run(&ps)
509 })
510 .expect("spawn matching thread");
511
512 let ps = Arc::clone(&pipeline_shutdown);
515 let drain_core = cores.response;
516 let drain_consumer = pipeline.drain_consumer;
517 let drain_handle = std::thread::Builder::new()
518 .name("drain".into())
519 .spawn(move || {
520 melin_app::affinity::pin_thread("drain", drain_core);
521 let mut consumer = drain_consumer;
522 let mut batch = vec![OutputSlot::<A::Report, A::QueryResponse>::default(); 256];
523 loop {
524 if ps.load(Ordering::Relaxed) {
525 return;
526 }
527 let count = consumer.consume_batch(&mut batch, 256);
528 if count == 0 {
529 if busy_spin {
530 std::hint::spin_loop();
531 } else {
532 std::thread::yield_now();
533 }
534 }
535 }
536 })
537 .expect("spawn drain thread");
538
539 let shadow_handle = if let Some(shadow_cons) = pipeline.shadow_consumer {
540 let snap_path = snapshot_path;
541 let chain_lock = pipeline
542 .chain_hash_lock
543 .as_ref()
544 .expect("chain hash lock with shadow")
545 .clone();
546 let ps = Arc::clone(&pipeline_shutdown);
547 let shadow_core = cores.shadow;
548 Some(
549 std::thread::Builder::new()
550 .name("replica-shadow".into())
551 .spawn(move || {
552 melin_app::affinity::pin_thread("replica-shadow", shadow_core);
553 melin_transport_core::shadow::run(
554 shadow_cons,
555 shadow_exchange,
556 snap_path,
557 std::time::Duration::from_millis(snapshot_interval_ms),
558 chain_lock,
559 &ps,
560 false,
561 shadow_initial_epoch,
562 );
563 })
564 .expect("spawn shadow thread"),
565 )
566 } else {
567 None
568 };
569
570 Ok(ReplicaPipelineHandles {
571 input_producer: pipeline.input_producer,
572 journal_cursor: pipeline.cursors.journal_ring_arc(),
573 last_seq: pipeline.cursors.durable_wire_seq(),
574 chain_hash_lock: pipeline.chain_hash_lock,
575 stream_marks,
576 journal_failed,
577 pipeline_shutdown,
578 journal_handle,
579 matching_handle,
580 drain_handle,
581 shadow_handle,
582 })
583}
584
585pub(super) fn teardown_replica_pipeline<A: Application + Send + 'static, W: Send + 'static>(
590 mut handles: ReplicaPipelineHandles<A, W>,
591) -> TeardownOutcome<A, W> {
592 while !handles.journal_failed.load(Ordering::Acquire) {
604 match handles
605 .input_producer
606 .try_publish(InputSlot::<A::Event>::shutdown_sentinel())
607 {
608 Ok(_) => break,
609 Err(_) => std::thread::yield_now(),
611 }
612 }
613 shutdown_pipeline::<A, W>(
614 &handles.pipeline_shutdown,
615 handles.journal_handle,
616 handles.matching_handle,
617 handles.drain_handle,
618 handles.shadow_handle,
619 )
620}
621
622pub(super) const MAX_INPROCESS_DIVERGENCE_RESYNCS: u32 = 1;
633
634#[allow(clippy::type_complexity)]
651pub(super) fn recover_replica_state<A, W>(
652 journal_path: &std::path::Path,
653 snapshot_path: &std::path::Path,
654 factory: &dyn melin_app::app_factory::AppFactory<App = A>,
655 fence_state: &melin_transport_core::fence::FenceState,
656) -> Result<(Option<A>, Option<W>, u64, [u8; 32]), Box<dyn std::error::Error>>
657where
658 A: Application,
659 W: melin_journal::JournalWrite<A::Event>,
660{
661 let lineage_exists =
662 journal_path.exists() || !melin_journal::segment::list_archives(journal_path)?.is_empty();
663 if !lineage_exists {
664 return Ok((None, None, 0u64, [0u8; 32]));
665 }
666 let engine = if snapshot_path.exists() {
667 tracing::info!("recovering replica from snapshot + journal");
668 melin_transport_core::JournaledApp::<A, W>::recover_from_snapshot(
669 snapshot_path,
670 journal_path,
671 )?
672 } else {
673 melin_transport_core::JournaledApp::<A, W>::recover(factory.empty(), journal_path)?
674 };
675 let next = engine.next_sequence();
676 let last = next.saturating_sub(1);
677 let hash = engine.chain_hash().unwrap_or([0u8; 32]);
678 fence_state.observe_epoch(engine.recovered_epoch());
681 let (mut exchange, writer) = engine.into_parts();
682 factory.apply_operator_policy(&mut exchange);
683 Ok((Some(exchange), Some(writer), last, hash))
684}
685
686pub(super) const MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(30);
692
693pub(in crate::replication) enum AfterSession<A, W> {
697 Return(ReceiverResult<A, W>),
701 Resync {
706 exchange: Option<A>,
707 journal_writer: Option<W>,
708 last_sequence: u64,
709 chain_hash: [u8; 32],
710 },
711 Reconnect,
715}
716
717#[allow(clippy::too_many_arguments)]
738pub(in crate::replication) fn handle_session_exit<A, W>(
739 result: StreamingResult,
740 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
741 divergence_resyncs: &mut u32,
742 backoff: &mut std::time::Duration,
743 last_sequence: u64,
744 journal_path: &std::path::Path,
745 snapshot_path: &std::path::Path,
746 factory: &dyn melin_app::app_factory::AppFactory<App = A>,
747 fence_state: &melin_transport_core::fence::FenceState,
748 shutdown: &AtomicBool,
749 promote: &crate::promotion::PromotionRequest,
750 mut close: impl FnMut(),
751) -> AfterSession<A, W>
752where
753 A: Application + Send + 'static,
754 W: JournalWrite<A::Event> + Send + 'static,
755{
756 let StreamingResult {
757 exit,
758 heard_from_primary,
759 } = result;
760
761 match exit {
762 SessionExit::Shutdown => {
763 if let Some(p) = pipeline.take() {
764 let _ = teardown_replica_pipeline::<A, W>(p);
765 }
766 AfterSession::Return(Ok(None))
767 }
768
769 SessionExit::Promote => AfterSession::Return(match pipeline.take() {
770 Some(p) => match teardown_replica_pipeline::<A, W>(p) {
771 TeardownOutcome::Clean(ex, wr) => Ok(Some((ex, wr))),
772 _ => Err("pipeline failed during promotion".into()),
773 },
774 None => Err("pipeline missing on promote".into()),
775 }),
776
777 SessionExit::Fatal(e) => {
778 let outcome = match pipeline.take() {
779 Some(p) => teardown_replica_pipeline::<A, W>(p),
780 None => return AfterSession::Return(Err(e)),
783 };
784 let TeardownOutcome::JournalFailed(
794 je @ melin_journal::JournalError::ReplicaChainDivergence { .. },
795 ) = outcome
796 else {
797 return AfterSession::Return(Err(e));
798 };
799
800 *divergence_resyncs += 1;
801 let attempt = *divergence_resyncs;
802 if attempt > MAX_INPROCESS_DIVERGENCE_RESYNCS {
803 return AfterSession::Return(Err(format!(
804 "mid-stream chain divergence recurred {attempt} times — giving up on \
805 in-process resync (each cycle archives the local journal and re-seeds \
806 from the primary; recurrence at this rate means the primary keeps \
807 streaming history that forks from what it announces): {je}"
808 )
809 .into()));
810 }
811 tracing::warn!(
812 error = %je,
813 attempt,
814 max_attempts = MAX_INPROCESS_DIVERGENCE_RESYNCS,
815 "mid-stream chain divergence — re-deriving local state for in-process resync"
816 );
817 close();
819 match recover_replica_state::<A, W>(journal_path, snapshot_path, factory, fence_state) {
820 Ok((exchange, journal_writer, seq, hash)) => AfterSession::Resync {
821 exchange,
822 journal_writer,
823 last_sequence: seq,
824 chain_hash: hash,
825 },
826 Err(e) => AfterSession::Return(Err(e)),
827 }
828 }
829
830 SessionExit::Disconnected => {
831 close();
834 if heard_from_primary {
843 *backoff = std::time::Duration::from_secs(1);
844 }
845 tracing::warn!(
846 last_sequence,
847 heard_from_primary,
848 backoff_secs = backoff.as_secs(),
849 "reconnecting to primary"
850 );
851 sleep_then_double_backoff(backoff, shutdown, promote);
852 AfterSession::Reconnect
853 }
854 }
855}
856
857pub(in crate::replication) fn take_pipeline_for_promotion<A, W>(
867 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
868 exchange: &mut Option<A>,
869 journal_writer: &mut Option<W>,
870) -> ReceiverResult<A, W>
871where
872 A: Application + Send + 'static,
873 W: JournalWrite<A::Event> + Send + 'static,
874{
875 if let Some(p) = pipeline.take()
876 && let TeardownOutcome::Clean(e, w) = teardown_replica_pipeline::<A, W>(p)
877 {
878 *exchange = Some(e);
879 *journal_writer = Some(w);
880 }
881 match (exchange.take(), journal_writer.take()) {
882 (Some(e), Some(w)) => Ok(Some((e, w))),
883 _ => Err("promotion requested but no local state available".into()),
884 }
885}
886
887type ResyncTransfer<A> = (A, u64, [u8; 32], u64);
890
891pub(in crate::replication) enum ResyncDecision {
894 Ready {
899 segment_start_sequence: u64,
900 anchor_hash: [u8; 32],
901 resume_sequence: u64,
902 },
903 Retry,
907}
908
909fn receive_resync_transfer<A, S>(
919 source: &mut S,
920 snapshot_path: &std::path::Path,
921 journal_path: &std::path::Path,
922 fence_state: &melin_transport_core::fence::FenceState,
923) -> Result<ResyncTransfer<A>, Box<dyn std::error::Error + Send + Sync>>
924where
925 A: Application,
926 S: ControlFrameSource,
927{
928 let (snap_len, snap_sequence, snap_chain_hash) =
929 match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
930 PrimaryMessage::SnapshotBegin {
931 snapshot_len,
932 snap_sequence,
933 snap_chain_hash,
934 } => (snapshot_len, snap_sequence, snap_chain_hash),
935 other => return Err(format!("expected SnapshotBegin, got {other:?}").into()),
936 };
937
938 tracing::info!(snap_sequence, snap_len, "receiving snapshot");
939 let tmp_path = snapshot_path.with_extension("snapshot.tmp");
940 receive_chunked_body(source, &tmp_path, snap_len, "snapshot")?;
941 std::fs::rename(&tmp_path, snapshot_path)?;
942 tracing::info!(snap_sequence, snap_len, "snapshot received and verified");
943
944 let (snap_exchange, _snap_seq, snap_hash, snap_epoch) =
945 melin_transport_core::snapshot::load::<A>(snapshot_path)?;
946 if snap_hash != snap_chain_hash {
947 return Err(format!(
948 "snapshot chain hash mismatch: primary sent {snap_chain_hash:02x?}, \
949 loaded snapshot has {snap_hash:02x?}"
950 )
951 .into());
952 }
953 fence_state.observe_epoch(snap_epoch);
956
957 let seed_len = match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
962 PrimaryMessage::SegmentSeedBegin { seed_len } => seed_len,
963 other => {
964 return Err(format!("expected SegmentSeedBegin after snapshot, got {other:?}").into());
965 }
966 };
967 let seed_tmp = journal_path.with_extension("seed.tmp");
968 receive_chunked_body(source, &seed_tmp, seed_len, "segment seed")?;
969 if let Err(e) =
975 melin_journal::segment::verify_segment_prefix(&seed_tmp, snap_sequence, seed_len)
976 {
977 let _ = std::fs::remove_file(&seed_tmp);
978 return Err(format!("segment seed failed structural verification: {e}").into());
979 }
980 std::fs::rename(&seed_tmp, journal_path)?;
981 melin_journal::segment::fsync_parent_dir(journal_path)?;
982 Ok((snap_exchange, snap_sequence, snap_chain_hash, seed_len))
983}
984
985#[allow(clippy::too_many_arguments)]
998pub(in crate::replication) fn handle_resync_verdict<A, W, S>(
999 divergent: bool,
1000 source: &mut S,
1001 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
1002 exchange: &mut Option<A>,
1003 journal_writer: &mut Option<W>,
1004 journal_path: &std::path::Path,
1005 snapshot_path: &std::path::Path,
1006 fence_state: &melin_transport_core::fence::FenceState,
1007 control: &ReplicaControlPlane,
1008 last_sequence: &mut u64,
1009 chain_hash: &mut [u8; 32],
1010) -> Result<ResyncDecision, Box<dyn std::error::Error + Send + Sync>>
1011where
1012 A: Application + Send + 'static,
1013 W: JournalWrite<A::Event> + Send + 'static,
1014 S: ControlFrameSource,
1015{
1016 if divergent {
1021 tracing::warn!(
1022 last_sequence = *last_sequence,
1023 "primary reports chain divergence — archiving local journal, resyncing from snapshot"
1024 );
1025 } else {
1026 tracing::info!("primary requires snapshot transfer — receiving snapshot");
1027 }
1028
1029 if let Some(p) = pipeline.take() {
1030 let _ = teardown_replica_pipeline::<A, W>(p);
1031 }
1032
1033 *exchange = None;
1040 *journal_writer = None;
1041
1042 let reason = if divergent {
1046 ArchiveReason::Divergent
1047 } else {
1048 ArchiveReason::Resync
1049 };
1050 archive_local_lineage(journal_path, snapshot_path, reason)?;
1051 *last_sequence = 0;
1053 *chain_hash = [0u8; 32];
1054 control
1059 .journal_tip
1060 .reset(melin_transport_core::WireSeq::new(0));
1061
1062 let (snap_exchange, snap_sequence, snap_chain_hash, seed_len) =
1063 match receive_resync_transfer::<A, S>(source, snapshot_path, journal_path, fence_state) {
1064 Ok(v) => v,
1065 Err(e) => {
1066 tracing::warn!(error = %e, "snapshot transfer failed — retrying");
1067 let _ = std::fs::remove_file(snapshot_path);
1071 return Ok(ResyncDecision::Retry);
1072 }
1073 };
1074 *exchange = Some(snap_exchange);
1075
1076 let writer = W::open_append(journal_path, snap_sequence, seed_len)?;
1083 let seeded_chain = writer.chain_hash().unwrap_or(snap_chain_hash);
1084 if snap_chain_hash != [0u8; 32] && seeded_chain != snap_chain_hash {
1085 return Err(format!(
1086 "segment seed chain at {snap_sequence} disagrees with the transferred snapshot's \
1087 hash — inconsistent primary"
1088 )
1089 .into());
1090 }
1091 let seeded_info = melin_journal::segment::read_header_info(journal_path)?;
1092 *journal_writer = Some(writer);
1093
1094 match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
1097 PrimaryMessage::StreamStart {
1098 start_sequence,
1099 segment_start_sequence,
1100 anchor_hash,
1101 epoch,
1102 durability_mode,
1103 } => {
1104 if segment_start_sequence != seeded_info.starting_sequence
1105 || anchor_hash != seeded_info.anchor_hash
1106 {
1107 return Err(format!(
1108 "post-snapshot StreamStart lineage (start {segment_start_sequence}) \
1109 disagrees with the transferred segment seed (start {}) — inconsistent \
1110 primary",
1111 seeded_info.starting_sequence
1112 )
1113 .into());
1114 }
1115 fence_state.observe_epoch(epoch);
1119 control
1120 .primary_acking_mode
1121 .store(durability_mode, Ordering::Release);
1122 tracing::info!(
1123 start_sequence,
1124 epoch,
1125 durability_mode,
1126 "streaming resumed after snapshot transfer"
1127 );
1128 control
1131 .journal_tip
1132 .reset(melin_transport_core::WireSeq::new(snap_sequence));
1133 Ok(ResyncDecision::Ready {
1134 segment_start_sequence,
1135 anchor_hash,
1136 resume_sequence: snap_sequence,
1137 })
1138 }
1139 other => Err(format!("expected StreamStart after snapshot, got {other:?}").into()),
1140 }
1141}
1142
1143#[cfg(test)]
1144mod tests {
1145 use std::io::Write;
1146 use std::sync::Arc;
1147
1148 use super::auth::{authenticate_replica, authenticate_with_primary};
1149 use super::*;
1150 use counter_server::CounterEvent;
1156 type InputSlot = melin_transport_core::pipeline::InputSlot<CounterEvent>;
1157 use melin_transport_core::replication::protocol::{
1158 MAX_CONTROL_FRAME, MAX_DATA_FRAME, MSG_AUTH_OK, MSG_CHALLENGE_RESPONSE, MSG_SNAPSHOT_BEGIN,
1159 MSG_SNAPSHOT_CHUNK, MSG_SNAPSHOT_END, decode_auth_result, decode_challenge,
1160 decode_challenge_response, decode_primary_message, decode_replica_message, encode_ack,
1161 encode_auth_failed, encode_auth_ok, encode_chain_check, encode_challenge,
1162 encode_challenge_response, encode_handshake, encode_hash_mismatch, encode_heartbeat,
1163 encode_input_batch, encode_need_snapshot, encode_rotate, encode_segment_seed_begin,
1164 encode_snapshot_begin, encode_snapshot_chunk, encode_snapshot_end, encode_stream_start,
1165 read_frame, try_decode_input_batch,
1166 };
1167
1168 fn encode_input_batch_with_seq(end_sequence: u64, buf: &mut Vec<u8>) {
1172 let slot = InputSlot {
1173 connection_id: 0,
1174 key_hash: 0,
1175 request_seq: 0,
1176 sequence: end_sequence,
1177 timestamp_ns: 0,
1178 event: melin_journal::JournalEvent::Tick { now_ns: 0 },
1179 publish_ts: Default::default(),
1180 recv_ts: Default::default(),
1181 };
1182 encode_input_batch(&[slot], buf);
1183 }
1184
1185 #[test]
1186 fn handshake_encode_decode_round_trip() {
1187 let handshake = Handshake {
1188 last_sequence: 42,
1189 chain_hash: [0xAB; 32],
1190 epoch: 9,
1192 };
1193 let mut buf = Vec::new();
1194 encode_handshake(&handshake, &mut buf);
1195
1196 let payload = &buf[4..];
1198 let msg = decode_replica_message(payload).unwrap();
1199 match msg {
1200 ReplicaMessage::Handshake(h) => {
1201 assert_eq!(h.last_sequence, 42);
1202 assert_eq!(h.chain_hash, [0xAB; 32]);
1203 assert_eq!(h.epoch, 9);
1204 }
1205 _ => panic!("expected Handshake"),
1206 }
1207 }
1208
1209 #[test]
1210 fn ack_encode_decode_round_trip() {
1211 let ack = Ack {
1212 acked_sequence: 1000,
1213 in_memory_sequence: 1024,
1214 };
1215 let mut buf = Vec::new();
1216 encode_ack(&ack, &mut buf);
1217
1218 let payload = &buf[4..];
1219 let msg = decode_replica_message(payload).unwrap();
1220 match msg {
1221 ReplicaMessage::Ack(a) => {
1222 assert_eq!(a.acked_sequence, 1000);
1223 assert_eq!(a.in_memory_sequence, 1024);
1224 }
1225 _ => panic!("expected Ack"),
1226 }
1227 }
1228
1229 #[test]
1237 fn ack_wire_byte_pattern() {
1238 let ack = Ack {
1239 acked_sequence: 0xDEAD_BEEF_CAFE_F00D,
1240 in_memory_sequence: 0x1122_3344_5566_7788,
1241 };
1242 let mut buf = Vec::new();
1243 encode_ack(&ack, &mut buf);
1244 let expected: &[u8] = &[
1247 0x11, 0x00, 0x00, 0x00, 0x02, 0x0D, 0xF0, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, ];
1252 assert_eq!(buf.as_slice(), expected, "Ack wire layout drifted");
1253 }
1254
1255 #[test]
1256 fn stream_start_encode_decode_round_trip() {
1257 let mut buf = Vec::new();
1258 encode_stream_start(99, 42, [0xAA; 32], 5, 2, &mut buf);
1261
1262 let payload = &buf[4..];
1263 let msg = decode_primary_message(payload).unwrap();
1264 match msg {
1265 PrimaryMessage::StreamStart {
1266 start_sequence,
1267 segment_start_sequence,
1268 anchor_hash,
1269 epoch,
1270 durability_mode,
1271 } => {
1272 assert_eq!(start_sequence, 99);
1273 assert_eq!(segment_start_sequence, 42);
1274 assert_eq!(anchor_hash, [0xAA; 32]);
1275 assert_eq!(durability_mode, 2);
1276 assert_eq!(epoch, 5);
1277 }
1278 _ => panic!("expected StreamStart"),
1279 }
1280 }
1281
1282 #[test]
1283 fn heartbeat_encode_decode_round_trip() {
1284 let mut buf = Vec::new();
1285 encode_heartbeat(123, 2, &mut buf);
1286
1287 let payload = &buf[4..];
1288 let msg = decode_primary_message(payload).unwrap();
1289 match msg {
1290 PrimaryMessage::Heartbeat {
1291 sequence,
1292 durability_mode,
1293 } => {
1294 assert_eq!(sequence, 123);
1295 assert_eq!(durability_mode, 2);
1296 }
1297 _ => panic!("expected Heartbeat"),
1298 }
1299 }
1300
1301 #[test]
1302 fn need_snapshot_encode_decode_round_trip() {
1303 let mut buf = Vec::new();
1304 encode_need_snapshot(&mut buf);
1305
1306 let payload = &buf[4..];
1307 let msg = decode_primary_message(payload).unwrap();
1308 assert!(matches!(msg, PrimaryMessage::NeedSnapshot));
1309 }
1310
1311 #[test]
1312 fn hash_mismatch_encode_decode_round_trip() {
1313 let mut buf = Vec::new();
1314 encode_hash_mismatch(&mut buf);
1315
1316 let payload = &buf[4..];
1317 let msg = decode_primary_message(payload).unwrap();
1318 assert!(matches!(msg, PrimaryMessage::HashMismatch));
1319 }
1320
1321 #[test]
1322 fn rotate_encode_decode_round_trip() {
1323 let mut buf = Vec::new();
1324 encode_rotate(7_000_000, &[0xBB; 32], &mut buf);
1325
1326 let payload = &buf[4..];
1327 let msg = decode_primary_message(payload).unwrap();
1328 match msg {
1329 PrimaryMessage::Rotate {
1330 boundary_seq,
1331 tail_hash,
1332 } => {
1333 assert_eq!(boundary_seq, 7_000_000);
1334 assert_eq!(tail_hash, [0xBB; 32]);
1335 }
1336 _ => panic!("expected Rotate"),
1337 }
1338 }
1339
1340 #[test]
1341 fn chain_check_encode_decode_round_trip() {
1342 let mut buf = Vec::new();
1343 encode_chain_check(555, &[0xCC; 32], &mut buf);
1344
1345 let payload = &buf[4..];
1346 let msg = decode_primary_message(payload).unwrap();
1347 match msg {
1348 PrimaryMessage::ChainCheck {
1349 sequence,
1350 chain_hash,
1351 } => {
1352 assert_eq!(sequence, 555);
1353 assert_eq!(chain_hash, [0xCC; 32]);
1354 }
1355 _ => panic!("expected ChainCheck"),
1356 }
1357 }
1358
1359 #[test]
1363 fn rotate_wire_byte_pattern() {
1364 let mut hash = [0u8; 32];
1365 for (i, b) in hash.iter_mut().enumerate() {
1366 *b = i as u8;
1367 }
1368 let mut buf = Vec::new();
1369 encode_rotate(0xDEAD_BEEF_CAFE_F00D, &hash, &mut buf);
1370 let mut expected = vec![
1373 0x29, 0x00, 0x00, 0x00, 0x16, 0x0D, 0xF0, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE, ];
1377 expected.extend_from_slice(&hash);
1378 assert_eq!(
1379 buf.as_slice(),
1380 expected.as_slice(),
1381 "Rotate wire layout drifted"
1382 );
1383 }
1384
1385 #[test]
1386 fn segment_seed_begin_encode_decode_round_trip() {
1387 let mut buf = Vec::new();
1388 encode_segment_seed_begin(123_456, &mut buf);
1389
1390 let payload = &buf[4..];
1391 let msg = decode_primary_message(payload).unwrap();
1392 match msg {
1393 PrimaryMessage::SegmentSeedBegin { seed_len } => {
1394 assert_eq!(seed_len, 123_456);
1395 }
1396 _ => panic!("expected SegmentSeedBegin"),
1397 }
1398 }
1399
1400 #[test]
1401 fn unknown_replica_message_type_is_error() {
1402 let payload = [0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
1403 let result = decode_replica_message(&payload);
1404 assert!(result.is_err());
1405 }
1406
1407 #[test]
1408 fn unknown_primary_message_type_is_error() {
1409 let payload = [0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
1410 let result = decode_primary_message(&payload);
1411 assert!(result.is_err());
1412 }
1413
1414 #[test]
1415 fn read_frame_enforces_max_size() {
1416 let mut buf = Vec::new();
1418 buf.extend_from_slice(&1000u32.to_le_bytes());
1419 buf.extend_from_slice(&[0u8; 100]); let mut cursor = std::io::Cursor::new(buf);
1422 let result = read_frame(&mut cursor, 64);
1423 assert!(result.is_err());
1424 }
1425
1426 #[test]
1427 fn challenge_encode_decode_round_trip() {
1428 let nonce = [0x42; 32];
1429 let mut buf = Vec::new();
1430 encode_challenge(&nonce, &mut buf);
1431
1432 let payload = &buf[4..];
1433 let decoded = decode_challenge(payload).unwrap();
1434 assert_eq!(decoded, nonce);
1435 }
1436
1437 #[test]
1438 fn challenge_response_encode_decode_round_trip() {
1439 let sig = [0xAA; 64];
1440 let pubkey = [0xBB; 32];
1441 let mut buf = Vec::new();
1442 encode_challenge_response(&sig, &pubkey, &mut buf);
1443
1444 let payload = &buf[4..];
1445 let (decoded_sig, decoded_pubkey) = decode_challenge_response(payload).unwrap();
1446 assert_eq!(decoded_sig, sig);
1447 assert_eq!(decoded_pubkey, pubkey);
1448 }
1449
1450 #[test]
1451 fn auth_ok_encode_decode_round_trip() {
1452 let mut buf = Vec::new();
1453 encode_auth_ok(&mut buf);
1454
1455 let payload = &buf[4..];
1456 assert!(decode_auth_result(payload).unwrap());
1457 }
1458
1459 #[test]
1460 fn auth_failed_encode_decode_round_trip() {
1461 let mut buf = Vec::new();
1462 encode_auth_failed(&mut buf);
1463
1464 let payload = &buf[4..];
1465 assert!(!decode_auth_result(payload).unwrap());
1466 }
1467
1468 #[test]
1469 fn decode_challenge_rejects_wrong_tag() {
1470 let mut payload = [0u8; 33];
1471 payload[0] = MSG_AUTH_OK;
1472 assert!(decode_challenge(&payload).is_err());
1473 }
1474
1475 #[test]
1476 fn decode_challenge_response_rejects_short_payload() {
1477 let payload = [MSG_CHALLENGE_RESPONSE; 10]; assert!(decode_challenge_response(&payload).is_err());
1479 }
1480
1481 #[test]
1482 fn auth_round_trip_valid_key() {
1483 use ed25519_dalek::SigningKey;
1484 use std::os::unix::net::UnixStream;
1485
1486 let repl_key = SigningKey::from_bytes(&[0xFC; 32]);
1487 let pub_b64 = base64::Engine::encode(
1488 &base64::engine::general_purpose::STANDARD,
1489 repl_key.verifying_key().to_bytes(),
1490 );
1491 let keys_content = format!("replication {pub_b64} test-replica\n");
1492 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1493
1494 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1495 primary_stream
1496 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1497 .unwrap();
1498 replica_stream
1499 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1500 .unwrap();
1501
1502 let repl_key_clone = SigningKey::from_bytes(&[0xFC; 32]);
1503 let replica_handle = std::thread::spawn(move || {
1504 let mut conn = replica_stream;
1505 authenticate_with_primary(&mut conn, &repl_key_clone)
1506 });
1507
1508 let mut conn = primary_stream;
1509 authenticate_replica(&mut conn, &authorized_keys).unwrap();
1510
1511 replica_handle.join().unwrap().unwrap();
1512 }
1513
1514 #[test]
1515 fn auth_rejects_unknown_key() {
1516 use ed25519_dalek::SigningKey;
1517 use std::os::unix::net::UnixStream;
1518
1519 let authorized_key = SigningKey::from_bytes(&[0xAA; 32]);
1521 let rogue_key = SigningKey::from_bytes(&[0xBB; 32]);
1522 let pub_b64 = base64::Engine::encode(
1523 &base64::engine::general_purpose::STANDARD,
1524 authorized_key.verifying_key().to_bytes(),
1525 );
1526 let keys_content = format!("replication {pub_b64} authorized-replica\n");
1527 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1528
1529 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1530 primary_stream
1531 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1532 .unwrap();
1533 replica_stream
1534 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1535 .unwrap();
1536
1537 let replica_handle = std::thread::spawn(move || {
1538 let mut conn = replica_stream;
1539 authenticate_with_primary(&mut conn, &rogue_key)
1540 });
1541
1542 let mut conn = primary_stream;
1543 let result = authenticate_replica(&mut conn, &authorized_keys);
1544 assert!(result.is_err());
1545 assert!(result.unwrap_err().to_string().contains("unknown"));
1546
1547 let replica_result = replica_handle.join().unwrap();
1549 assert!(replica_result.is_err());
1550 }
1551
1552 #[test]
1553 fn auth_rejects_wrong_permission() {
1554 use ed25519_dalek::SigningKey;
1555 use std::os::unix::net::UnixStream;
1556
1557 let key = SigningKey::from_bytes(&[0xCC; 32]);
1559 let pub_b64 = base64::Engine::encode(
1560 &base64::engine::general_purpose::STANDARD,
1561 key.verifying_key().to_bytes(),
1562 );
1563 let keys_content = format!("trader {pub_b64} wrong-role\n");
1564 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1565
1566 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1567 primary_stream
1568 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1569 .unwrap();
1570 replica_stream
1571 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1572 .unwrap();
1573
1574 let replica_handle = std::thread::spawn(move || {
1575 let mut conn = replica_stream;
1576 authenticate_with_primary(&mut conn, &key)
1577 });
1578
1579 let mut conn = primary_stream;
1580 let result = authenticate_replica(&mut conn, &authorized_keys);
1581 assert!(result.is_err());
1582 assert!(result.unwrap_err().to_string().contains("Replication"));
1583
1584 let replica_result = replica_handle.join().unwrap();
1585 assert!(replica_result.is_err());
1586 }
1587
1588 #[test]
1591 fn auth_rejects_invalid_signature() {
1592 use ed25519_dalek::SigningKey;
1593 use std::os::unix::net::UnixStream;
1594
1595 let correct_key = SigningKey::from_bytes(&[0xDD; 32]);
1597 let pub_b64 = base64::Engine::encode(
1598 &base64::engine::general_purpose::STANDARD,
1599 correct_key.verifying_key().to_bytes(),
1600 );
1601 let keys_content = format!("replication {pub_b64} test-replica\n");
1602 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1603
1604 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1605 primary_stream
1606 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1607 .unwrap();
1608 replica_stream
1609 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1610 .unwrap();
1611
1612 let replica_handle = std::thread::spawn(move || {
1615 use melin_transport_core::replication::protocol::*;
1616
1617 let mut reader = replica_stream.try_clone().unwrap();
1618 let mut writer = replica_stream;
1619
1620 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1622 let nonce = decode_challenge(&frame).unwrap();
1623
1624 let wrong_key = SigningKey::from_bytes(&[0xEE; 32]);
1626 let bad_signature = ed25519_dalek::Signer::sign(&wrong_key, &nonce);
1627 let correct_pubkey = correct_key.verifying_key();
1628
1629 let mut buf = Vec::with_capacity(128);
1630 encode_challenge_response(
1631 &bad_signature.to_bytes(),
1632 correct_pubkey.as_bytes(),
1633 &mut buf,
1634 );
1635 writer.write_all(&buf).unwrap();
1636 writer.flush().unwrap();
1637
1638 let result_frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1640 let ok = decode_auth_result(&result_frame).unwrap();
1641 assert!(!ok, "should receive auth failure");
1642 });
1643
1644 let mut conn = primary_stream;
1645 let result = authenticate_replica(&mut conn, &authorized_keys);
1646 assert!(result.is_err());
1647 assert!(
1648 result
1649 .unwrap_err()
1650 .to_string()
1651 .contains("signature verification failed"),
1652 "should fail on signature verification"
1653 );
1654
1655 replica_handle.join().unwrap();
1656 }
1657
1658 #[test]
1659 fn sender_receiver_end_to_end() {
1660 use std::os::unix::net::UnixStream;
1661
1662 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1664
1665 let replica_handle = std::thread::spawn(move || {
1667 let mut reader = replica_stream.try_clone().unwrap();
1668 let mut writer = replica_stream;
1669
1670 let mut buf = Vec::new();
1672 let handshake = Handshake {
1673 last_sequence: 0,
1674 chain_hash: [0u8; 32],
1675 epoch: 0,
1676 };
1677 encode_handshake(&handshake, &mut buf);
1678 writer.write_all(&buf).unwrap();
1679 writer.flush().unwrap();
1680 buf.clear();
1681
1682 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1684 let msg = decode_primary_message(&frame).unwrap();
1685 assert!(matches!(msg, PrimaryMessage::StreamStart { .. }));
1686
1687 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1689 let slots: Vec<InputSlot> = try_decode_input_batch(&frame).expect("decode InputBatch");
1690 let end_seq = slots
1691 .last()
1692 .map(|s| s.sequence)
1693 .expect("InputBatch carried at least one slot");
1694
1695 let ack = Ack {
1697 acked_sequence: end_seq,
1698 in_memory_sequence: end_seq,
1699 };
1700 encode_ack(&ack, &mut buf);
1701 writer.write_all(&buf).unwrap();
1702 writer.flush().unwrap();
1703
1704 end_seq
1705 });
1706
1707 let mut p_reader = primary_stream.try_clone().unwrap();
1709 let mut p_writer = primary_stream;
1710
1711 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1713 let handshake = match decode_replica_message(&frame).unwrap() {
1714 ReplicaMessage::Handshake(h) => h,
1715 _ => panic!("expected Handshake"),
1716 };
1717 assert_eq!(handshake.last_sequence, 0);
1718
1719 let mut buf = Vec::new();
1721 encode_stream_start(0, 1, [0u8; 32], 0, 1, &mut buf); p_writer.write_all(&buf).unwrap();
1723 p_writer.flush().unwrap();
1724 buf.clear();
1725
1726 encode_input_batch_with_seq(42, &mut buf);
1728 p_writer.write_all(&buf).unwrap();
1729 p_writer.flush().unwrap();
1730 buf.clear();
1731
1732 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1734 let ack = match decode_replica_message(&frame).unwrap() {
1735 ReplicaMessage::Ack(a) => a,
1736 _ => panic!("expected Ack"),
1737 };
1738 assert_eq!(ack.acked_sequence, 42);
1739
1740 let end_seq = replica_handle.join().unwrap();
1742 assert_eq!(end_seq, 42);
1743 }
1744
1745 #[test]
1746 fn multiple_data_batches_acked_in_order() {
1747 use std::os::unix::net::UnixStream;
1750
1751 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1752
1753 let replica_handle = std::thread::spawn(move || {
1754 let mut reader = replica_stream.try_clone().unwrap();
1755 let mut writer = replica_stream;
1756 let mut buf = Vec::new();
1757
1758 encode_handshake(
1760 &Handshake {
1761 last_sequence: 0,
1762 chain_hash: [0u8; 32],
1763 epoch: 0,
1764 },
1765 &mut buf,
1766 );
1767 writer.write_all(&buf).unwrap();
1768 writer.flush().unwrap();
1769 buf.clear();
1770
1771 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1773 assert!(matches!(
1774 decode_primary_message(&frame).unwrap(),
1775 PrimaryMessage::StreamStart { .. }
1776 ));
1777
1778 let mut acked_seqs = Vec::new();
1780 for _ in 0..3 {
1781 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1782 let slots: Vec<InputSlot> =
1783 try_decode_input_batch(&frame).expect("decode InputBatch");
1784 let end_seq = slots
1785 .last()
1786 .map(|s| s.sequence)
1787 .expect("InputBatch carried at least one slot");
1788 acked_seqs.push(end_seq);
1789
1790 encode_ack(
1791 &Ack {
1792 acked_sequence: end_seq,
1793 in_memory_sequence: end_seq,
1794 },
1795 &mut buf,
1796 );
1797 writer.write_all(&buf).unwrap();
1798 writer.flush().unwrap();
1799 buf.clear();
1800 }
1801
1802 acked_seqs
1803 });
1804
1805 let mut p_reader = primary_stream.try_clone().unwrap();
1807 let mut p_writer = primary_stream;
1808 let mut buf = Vec::new();
1809
1810 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1812 assert!(matches!(
1813 decode_replica_message(&frame).unwrap(),
1814 ReplicaMessage::Handshake(_)
1815 ));
1816
1817 encode_stream_start(0, 1, [0u8; 32], 0, 1, &mut buf);
1819 p_writer.write_all(&buf).unwrap();
1820 p_writer.flush().unwrap();
1821 buf.clear();
1822
1823 for seq in [10u64, 20, 30] {
1825 encode_input_batch_with_seq(seq, &mut buf);
1826 p_writer.write_all(&buf).unwrap();
1827 p_writer.flush().unwrap();
1828 buf.clear();
1829 }
1830
1831 for expected_seq in [10u64, 20, 30] {
1833 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1834 let ack = match decode_replica_message(&frame).unwrap() {
1835 ReplicaMessage::Ack(a) => a,
1836 other => panic!("expected Ack, got {other:?}"),
1837 };
1838 assert_eq!(ack.acked_sequence, expected_seq);
1839 }
1840
1841 let acked = replica_handle.join().unwrap();
1842 assert_eq!(acked, vec![10, 20, 30]);
1843 }
1844
1845 #[test]
1846 fn heartbeat_encode_contains_sequence() {
1847 let mut buf = Vec::new();
1850 encode_heartbeat(999, 1, &mut buf);
1851
1852 let payload = &buf[4..];
1853 match decode_primary_message(payload).unwrap() {
1854 PrimaryMessage::Heartbeat { sequence, .. } => {
1855 assert_eq!(sequence, 999);
1856 }
1857 other => panic!("expected Heartbeat, got {other:?}"),
1858 }
1859 }
1860
1861 #[test]
1862 fn replica_mid_stream_handshake_with_nonzero_sequence() {
1863 use std::os::unix::net::UnixStream;
1868
1869 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1870
1871 let replica_handle = std::thread::spawn(move || {
1872 let mut reader = replica_stream.try_clone().unwrap();
1873 let mut writer = replica_stream;
1874 let mut buf = Vec::new();
1875
1876 encode_handshake(
1878 &Handshake {
1879 last_sequence: 100,
1880 chain_hash: [0xBB; 32],
1881 epoch: 0,
1882 },
1883 &mut buf,
1884 );
1885 writer.write_all(&buf).unwrap();
1886 writer.flush().unwrap();
1887 buf.clear();
1888
1889 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1891 match decode_primary_message(&frame).unwrap() {
1892 PrimaryMessage::StreamStart { start_sequence, .. } => {
1893 assert_eq!(
1894 start_sequence, 100,
1895 "StreamStart should echo replica's last_sequence"
1896 );
1897 }
1898 other => panic!("expected StreamStart, got {other:?}"),
1899 }
1900
1901 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1903 let slots: Vec<InputSlot> = try_decode_input_batch(&frame).expect("decode InputBatch");
1904 let end_sequence = slots
1905 .last()
1906 .map(|s| s.sequence)
1907 .expect("InputBatch carried at least one slot");
1908 assert!(
1909 end_sequence > 100,
1910 "InputBatch should be after replica's last_sequence"
1911 );
1912 });
1913
1914 let mut p_reader = primary_stream.try_clone().unwrap();
1916 let mut p_writer = primary_stream;
1917 let mut buf = Vec::new();
1918
1919 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1921 let handshake = match decode_replica_message(&frame).unwrap() {
1922 ReplicaMessage::Handshake(h) => h,
1923 _ => panic!("expected Handshake"),
1924 };
1925 assert_eq!(handshake.last_sequence, 100);
1926 assert_eq!(handshake.chain_hash, [0xBB; 32]);
1927
1928 encode_stream_start(handshake.last_sequence, 1, [0u8; 32], 0, 1, &mut buf);
1930 p_writer.write_all(&buf).unwrap();
1931 p_writer.flush().unwrap();
1932 buf.clear();
1933
1934 encode_input_batch_with_seq(150, &mut buf);
1936 p_writer.write_all(&buf).unwrap();
1937 p_writer.flush().unwrap();
1938
1939 replica_handle.join().unwrap();
1940 }
1941
1942 #[test]
1957 fn receiver_refuses_stream_start_from_stale_primary() {
1958 use std::os::unix::net::UnixStream;
1959
1960 use melin_transport_core::fence::FenceState;
1961
1962 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1963
1964 const STALE_EPOCH: u64 = 3;
1968 let primary_handle = std::thread::spawn(move || {
1969 let mut reader = primary_stream.try_clone().unwrap();
1970 let mut writer = primary_stream;
1971
1972 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1973 let handshake = match decode_replica_message(&frame).unwrap() {
1974 ReplicaMessage::Handshake(h) => h,
1975 _ => panic!("expected Handshake"),
1976 };
1977 assert_eq!(handshake.epoch, 5, "replica advertises its real epoch");
1980
1981 let mut buf = Vec::new();
1982 encode_stream_start(
1983 handshake.last_sequence,
1984 1,
1985 [0u8; 32],
1986 STALE_EPOCH,
1987 1,
1988 &mut buf,
1989 );
1990 writer.write_all(&buf).unwrap();
1991 writer.flush().unwrap();
1992 });
1993
1994 let fence_state = Arc::new(FenceState::new(5));
1997 let mut r_reader = replica_stream.try_clone().unwrap();
1998 let mut r_writer = replica_stream;
1999
2000 let mut buf = Vec::new();
2001 encode_handshake(
2002 &Handshake {
2003 last_sequence: 0,
2004 chain_hash: [0u8; 32],
2005 epoch: fence_state.epoch(),
2006 },
2007 &mut buf,
2008 );
2009 r_writer.write_all(&buf).unwrap();
2010 r_writer.flush().unwrap();
2011
2012 let frame = read_frame(&mut r_reader, MAX_CONTROL_FRAME).unwrap();
2013 let stream_epoch = match decode_primary_message(&frame).unwrap() {
2014 PrimaryMessage::StreamStart { epoch, .. } => epoch,
2015 other => panic!("expected StreamStart, got {other:?}"),
2016 };
2017 assert_eq!(stream_epoch, STALE_EPOCH, "epoch must survive the wire");
2018
2019 assert!(
2022 fence_state.refuses_primary(stream_epoch),
2023 "replica at epoch 5 must refuse a StreamStart from epoch {STALE_EPOCH}"
2024 );
2025 assert_eq!(fence_state.epoch(), 5, "refusal must not lower our epoch");
2026
2027 assert!(!fence_state.refuses_primary(5), "same tenure is followed");
2030 assert!(!fence_state.refuses_primary(6), "newer primary is followed");
2031
2032 primary_handle.join().unwrap();
2033 }
2034
2035 #[test]
2036 fn snapshot_begin_encode_decode_round_trip() {
2037 let mut buf = Vec::new();
2038 encode_snapshot_begin(1_000_000, 42, &[0xAB; 32], &mut buf);
2039
2040 let payload = &buf[4..];
2041 let msg = decode_primary_message(payload).unwrap();
2042 match msg {
2043 PrimaryMessage::SnapshotBegin {
2044 snapshot_len,
2045 snap_sequence,
2046 snap_chain_hash,
2047 } => {
2048 assert_eq!(snapshot_len, 1_000_000);
2049 assert_eq!(snap_sequence, 42);
2050 assert_eq!(snap_chain_hash, [0xAB; 32]);
2051 }
2052 _ => panic!("expected SnapshotBegin"),
2053 }
2054 }
2055
2056 #[test]
2057 fn snapshot_chunk_encode_decode_round_trip() {
2058 let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
2059 let mut buf = Vec::new();
2060 encode_snapshot_chunk(&data, &mut buf);
2061
2062 let payload = &buf[4..];
2063 let msg = decode_primary_message(payload).unwrap();
2064 match msg {
2065 PrimaryMessage::SnapshotChunk(chunk) => {
2066 assert_eq!(chunk, data);
2067 }
2068 _ => panic!("expected SnapshotChunk"),
2069 }
2070 }
2071
2072 #[test]
2073 fn snapshot_end_encode_decode_round_trip() {
2074 let mut buf = Vec::new();
2075 encode_snapshot_end(0xDEADBEEF, &mut buf);
2076
2077 let payload = &buf[4..];
2078 let msg = decode_primary_message(payload).unwrap();
2079 match msg {
2080 PrimaryMessage::SnapshotEnd { crc32c } => {
2081 assert_eq!(crc32c, 0xDEADBEEF);
2082 }
2083 _ => panic!("expected SnapshotEnd"),
2084 }
2085 }
2086
2087 #[test]
2091 fn snapshot_receiver_detects_length_mismatch() {
2092 use std::os::unix::net::UnixStream;
2093
2094 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2095
2096 let receiver = std::thread::spawn(move || -> String {
2098 let mut reader = replica_stream.try_clone().unwrap();
2099
2100 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2102 assert!(matches!(
2103 decode_primary_message(&frame).unwrap(),
2104 PrimaryMessage::NeedSnapshot,
2105 ));
2106
2107 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2109 let (snap_len, _snap_sequence, _snap_chain_hash) =
2110 match decode_primary_message(&frame).unwrap() {
2111 PrimaryMessage::SnapshotBegin {
2112 snapshot_len,
2113 snap_sequence,
2114 snap_chain_hash,
2115 } => (snapshot_len, snap_sequence, snap_chain_hash),
2116 other => panic!("expected SnapshotBegin, got {other:?}"),
2117 };
2118
2119 let mut received: u64 = 0;
2121 loop {
2122 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
2123 match decode_primary_message(&frame).unwrap() {
2124 PrimaryMessage::SnapshotChunk(data) => {
2125 received += data.len() as u64;
2126 }
2127 PrimaryMessage::SnapshotEnd { .. } => {
2128 if received != snap_len {
2129 return format!(
2130 "snapshot length mismatch: expected {snap_len} bytes, got {received}"
2131 );
2132 }
2133 return String::new(); }
2135 other => panic!("unexpected message: {other:?}"),
2136 }
2137 }
2138 });
2139
2140 let mut writer = primary_stream;
2142 let mut buf = Vec::new();
2143
2144 let actual_data = vec![0xAA; 100];
2145 let wrong_len = 999u64; encode_need_snapshot(&mut buf);
2148 std::io::Write::write_all(&mut writer, &buf).unwrap();
2149 buf.clear();
2150
2151 encode_snapshot_begin(wrong_len, 42, &[0xBB; 32], &mut buf);
2152 std::io::Write::write_all(&mut writer, &buf).unwrap();
2153 buf.clear();
2154
2155 encode_snapshot_chunk(&actual_data, &mut buf);
2156 std::io::Write::write_all(&mut writer, &buf).unwrap();
2157 buf.clear();
2158
2159 let crc = crc32c::crc32c(&actual_data);
2160 encode_snapshot_end(crc, &mut buf);
2161 std::io::Write::write_all(&mut writer, &buf).unwrap();
2162 std::io::Write::flush(&mut writer).unwrap();
2163
2164 let error_msg = receiver.join().unwrap();
2165 assert!(
2166 error_msg.contains("length mismatch"),
2167 "expected length mismatch error, got: {error_msg:?}"
2168 );
2169 }
2170
2171 #[test]
2175 fn snapshot_receiver_detects_crc_mismatch() {
2176 use std::os::unix::net::UnixStream;
2177
2178 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2179
2180 let receiver = std::thread::spawn(move || -> String {
2181 let mut reader = replica_stream.try_clone().unwrap();
2182
2183 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2185 assert!(matches!(
2186 decode_primary_message(&frame).unwrap(),
2187 PrimaryMessage::NeedSnapshot,
2188 ));
2189
2190 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2192 let snap_len = match decode_primary_message(&frame).unwrap() {
2193 PrimaryMessage::SnapshotBegin { snapshot_len, .. } => snapshot_len,
2194 other => panic!("expected SnapshotBegin, got {other:?}"),
2195 };
2196
2197 let mut received_data = Vec::new();
2199 let mut received: u64 = 0;
2200 loop {
2201 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
2202 match decode_primary_message(&frame).unwrap() {
2203 PrimaryMessage::SnapshotChunk(data) => {
2204 received += data.len() as u64;
2205 received_data.extend_from_slice(&data);
2206 }
2207 PrimaryMessage::SnapshotEnd {
2208 crc32c: expected_crc,
2209 } => {
2210 if received != snap_len {
2211 return format!("length mismatch: {snap_len} vs {received}");
2212 }
2213 let actual_crc = crc32c::crc32c(&received_data);
2214 if actual_crc != expected_crc {
2215 return format!(
2216 "CRC mismatch: expected {expected_crc:#x}, got {actual_crc:#x}"
2217 );
2218 }
2219 return String::new();
2220 }
2221 other => panic!("unexpected message: {other:?}"),
2222 }
2223 }
2224 });
2225
2226 let mut writer = primary_stream;
2228 let mut buf = Vec::new();
2229
2230 let data = vec![0xAA; 100];
2231
2232 encode_need_snapshot(&mut buf);
2233 std::io::Write::write_all(&mut writer, &buf).unwrap();
2234 buf.clear();
2235
2236 encode_snapshot_begin(data.len() as u64, 42, &[0xBB; 32], &mut buf);
2237 std::io::Write::write_all(&mut writer, &buf).unwrap();
2238 buf.clear();
2239
2240 encode_snapshot_chunk(&data, &mut buf);
2241 std::io::Write::write_all(&mut writer, &buf).unwrap();
2242 buf.clear();
2243
2244 let wrong_crc = !crc32c::crc32c(&data);
2246 encode_snapshot_end(wrong_crc, &mut buf);
2247 std::io::Write::write_all(&mut writer, &buf).unwrap();
2248 std::io::Write::flush(&mut writer).unwrap();
2249
2250 let error_msg = receiver.join().unwrap();
2251 assert!(
2252 error_msg.contains("CRC mismatch"),
2253 "expected CRC mismatch error, got: {error_msg:?}"
2254 );
2255 }
2256
2257 #[test]
2260 fn snapshot_receiver_detects_chain_hash_mismatch() {
2261 use std::os::unix::net::UnixStream;
2262
2263 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2264
2265 let receiver = std::thread::spawn(move || -> String {
2266 let mut reader = replica_stream.try_clone().unwrap();
2267
2268 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2269 assert!(matches!(
2270 decode_primary_message(&frame).unwrap(),
2271 PrimaryMessage::NeedSnapshot,
2272 ));
2273
2274 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2275 let (snap_len, _snap_sequence, snap_chain_hash) =
2276 match decode_primary_message(&frame).unwrap() {
2277 PrimaryMessage::SnapshotBegin {
2278 snapshot_len,
2279 snap_sequence,
2280 snap_chain_hash,
2281 } => (snapshot_len, snap_sequence, snap_chain_hash),
2282 other => panic!("expected SnapshotBegin, got {other:?}"),
2283 };
2284
2285 let mut received_data = Vec::new();
2287 let mut received: u64 = 0;
2288 loop {
2289 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
2290 match decode_primary_message(&frame).unwrap() {
2291 PrimaryMessage::SnapshotChunk(data) => {
2292 received += data.len() as u64;
2293 received_data.extend_from_slice(&data);
2294 }
2295 PrimaryMessage::SnapshotEnd {
2296 crc32c: expected_crc,
2297 } => {
2298 assert_eq!(received, snap_len, "length should match");
2299 let actual_crc = crc32c::crc32c(&received_data);
2300 assert_eq!(actual_crc, expected_crc, "CRC should match");
2301 break;
2302 }
2303 other => panic!("unexpected message: {other:?}"),
2304 }
2305 }
2306
2307 let loaded_hash = [0xFF; 32]; if loaded_hash != snap_chain_hash {
2311 return format!(
2312 "snapshot chain hash mismatch: primary sent {snap_chain_hash:02x?}, \
2313 loaded snapshot has {loaded_hash:02x?}"
2314 );
2315 }
2316 String::new()
2317 });
2318
2319 let mut writer = primary_stream;
2322 let mut buf = Vec::new();
2323
2324 let data = vec![0xAA; 64];
2325 let advertised_hash = [0xBB; 32];
2327
2328 encode_need_snapshot(&mut buf);
2329 std::io::Write::write_all(&mut writer, &buf).unwrap();
2330 buf.clear();
2331
2332 encode_snapshot_begin(data.len() as u64, 10, &advertised_hash, &mut buf);
2333 std::io::Write::write_all(&mut writer, &buf).unwrap();
2334 buf.clear();
2335
2336 encode_snapshot_chunk(&data, &mut buf);
2337 std::io::Write::write_all(&mut writer, &buf).unwrap();
2338 buf.clear();
2339
2340 let crc = crc32c::crc32c(&data);
2341 encode_snapshot_end(crc, &mut buf);
2342 std::io::Write::write_all(&mut writer, &buf).unwrap();
2343 std::io::Write::flush(&mut writer).unwrap();
2344
2345 let error_msg = receiver.join().unwrap();
2346 assert!(
2347 error_msg.contains("chain hash mismatch"),
2348 "expected chain hash mismatch error, got: {error_msg:?}"
2349 );
2350 }
2351
2352 #[test]
2355 fn primary_rejects_snapshot_with_invalid_magic() {
2356 let tmp = tempfile::tempdir().unwrap();
2357 let snap_path = tmp.path().join("test.snapshot");
2358
2359 let mut bad_snap = vec![0u8; 64];
2361 bad_snap[0..4].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes()); std::fs::write(&snap_path, &bad_snap).unwrap();
2364
2365 let snap_data = std::fs::read(&snap_path).unwrap();
2367 assert!(
2368 snap_data.len() >= 48,
2369 "file should be big enough for header"
2370 );
2371
2372 let magic = u32::from_le_bytes(snap_data[0..4].try_into().unwrap());
2373 assert_ne!(magic, 0x534E_4150);
2374 assert_eq!(magic, 0xDEAD_BEEF);
2375 }
2376
2377 #[test]
2380 fn primary_rejects_snapshot_too_small_for_header() {
2381 let tmp = tempfile::tempdir().unwrap();
2382 let snap_path = tmp.path().join("test.snapshot");
2383
2384 std::fs::write(&snap_path, [0u8; 20]).unwrap();
2386
2387 let snap_data = std::fs::read(&snap_path).unwrap();
2388 assert!(
2389 snap_data.len() < 48,
2390 "file must be too small for header validation"
2391 );
2392 }
2393
2394 #[test]
2395 fn decode_snapshot_begin_too_short() {
2396 let payload = [MSG_SNAPSHOT_BEGIN, 0x01, 0x02, 0x03];
2399 let err = decode_primary_message(&payload).unwrap_err();
2400 assert!(
2401 err.to_string().contains("SnapshotBegin too short"),
2402 "expected 'SnapshotBegin too short', got: {err}"
2403 );
2404 }
2405
2406 #[test]
2407 fn decode_snapshot_end_too_short() {
2408 let payload = [MSG_SNAPSHOT_END];
2410 let err = decode_primary_message(&payload).unwrap_err();
2411 assert!(
2412 err.to_string().contains("SnapshotEnd too short"),
2413 "expected 'SnapshotEnd too short', got: {err}"
2414 );
2415 }
2416
2417 #[test]
2418 fn decode_snapshot_chunk_empty_data() {
2419 let payload = [MSG_SNAPSHOT_CHUNK];
2421 let msg = decode_primary_message(&payload).unwrap();
2422 match msg {
2423 PrimaryMessage::SnapshotChunk(data) => {
2424 assert!(data.is_empty());
2425 }
2426 _ => panic!("expected SnapshotChunk"),
2427 }
2428 }
2429
2430 fn make_journal_cursor(val: u64) -> melin_pipeline::padding::Sequence {
2433 melin_pipeline::padding::Sequence::new(AtomicU64::new(val))
2434 }
2435
2436 #[test]
2437 fn pending_ack_queue_push_and_pop_ready() {
2438 let mut q = PendingAckQueue::new(8);
2439 assert!(q.is_empty());
2440 assert!(!q.is_full());
2441
2442 q.push(10, 100);
2443 q.push(20, 200);
2444 assert!(!q.is_empty());
2445
2446 let cursor = make_journal_cursor(5);
2448 assert!(q.pop_ready(&cursor).is_none());
2449
2450 cursor.get().store(15, Ordering::Relaxed);
2452 assert_eq!(q.pop_ready(&cursor), Some(100));
2453 assert!(!q.is_empty());
2455
2456 cursor.get().store(25, Ordering::Relaxed);
2458 assert_eq!(q.pop_ready(&cursor), Some(200));
2459 assert!(q.is_empty());
2460 }
2461
2462 #[test]
2463 fn pending_ack_queue_pop_ready_returns_highest_sequence() {
2464 let mut q = PendingAckQueue::new(8);
2468 q.push(10, 100);
2469 q.push(20, 200);
2470 q.push(30, 300);
2471
2472 let cursor = make_journal_cursor(30);
2473 assert_eq!(q.pop_ready(&cursor), Some(300));
2474 assert!(q.is_empty());
2475 }
2476
2477 #[test]
2478 fn pending_ack_queue_capacity_and_full() {
2479 let mut q = PendingAckQueue::new(8);
2480 for i in 0..8 {
2481 assert!(!q.is_full());
2482 q.push(i as u64 + 1, (i + 1) as u64 * 100);
2483 }
2484 assert!(q.is_full());
2485 }
2486
2487 #[test]
2488 fn pending_ack_queue_pop_oldest_blocking() {
2489 let mut q = PendingAckQueue::new(8);
2490 q.push(10, 100);
2491 q.push(20, 200);
2492
2493 let cursor = make_journal_cursor(25);
2496 let seq = q.pop_oldest_blocking(&cursor, true, &AtomicBool::new(false));
2497 assert_eq!(seq, Some(200));
2499 assert!(q.is_empty());
2500 }
2501
2502 #[test]
2505 fn pending_ack_queue_blocking_wait_aborts_on_journal_failure() {
2506 let mut q = PendingAckQueue::new(8);
2507 q.push(10, 100);
2508
2509 let cursor = make_journal_cursor(5);
2511 let abort = AtomicBool::new(true);
2512 assert_eq!(q.pop_oldest_blocking(&cursor, true, &abort), None);
2513 assert!(!q.is_empty(), "aborted wait must not pop the entry");
2514 assert_eq!(q.pop_all_blocking(&cursor, true, &abort), None);
2515 }
2516
2517 #[test]
2526 fn shutdown_pipeline_surfaces_journal_error_kind() {
2527 let flag = AtomicBool::new(false);
2528 let matching_joined = Arc::new(AtomicBool::new(false));
2529 let mj = Arc::clone(&matching_joined);
2530
2531 let journal = std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> {
2532 Err(melin_journal::JournalError::ReplicaChainDivergence {
2533 sequence: 42,
2534 expected: [1u8; 32],
2535 actual: [2u8; 32],
2536 })
2537 });
2538 let matching = std::thread::spawn(move || {
2539 mj.store(true, Ordering::Release);
2540 7u64
2541 });
2542 let drain = std::thread::spawn(|| {});
2543
2544 let outcome = shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None);
2545 assert!(flag.load(Ordering::Acquire), "shutdown flag must be set");
2546 assert!(
2547 matching_joined.load(Ordering::Acquire),
2548 "matching thread must be joined even when the journal failed"
2549 );
2550 match outcome {
2551 TeardownOutcome::JournalFailed(
2552 melin_journal::JournalError::ReplicaChainDivergence { sequence, .. },
2553 ) => assert_eq!(sequence, 42),
2554 _ => panic!("expected JournalFailed(ReplicaChainDivergence)"),
2555 }
2556 }
2557
2558 #[test]
2559 fn shutdown_pipeline_clean_returns_both_states() {
2560 let flag = AtomicBool::new(false);
2561 let journal =
2562 std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> { Ok(11u32) });
2563 let matching = std::thread::spawn(|| 7u64);
2564 let drain = std::thread::spawn(|| {});
2565
2566 match shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None) {
2567 TeardownOutcome::Clean(app, writer) => {
2568 assert_eq!(app, 7);
2569 assert_eq!(writer, 11);
2570 }
2571 _ => panic!("expected Clean"),
2572 }
2573 }
2574
2575 #[test]
2576 fn shutdown_pipeline_panicked_stage_reports_panicked() {
2577 let flag = AtomicBool::new(false);
2578 let journal =
2579 std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> { Ok(11u32) });
2580 let matching = std::thread::spawn(|| -> u64 { panic!("matching stage died") });
2581 let drain = std::thread::spawn(|| {});
2582
2583 assert!(matches!(
2584 shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None),
2585 TeardownOutcome::Panicked
2586 ));
2587 }
2588
2589 fn teardown_fixture(
2594 capacity: usize,
2595 ) -> (
2596 ReplicaPipelineHandles<counter_server::Counter, u32>,
2597 melin_pipeline::ring::Consumer<InputSlot>,
2598 ) {
2599 use melin_app::app_factory::AppFactory;
2600 let (input_producer, mut consumers) =
2601 melin_pipeline::ring::DisruptorBuilder::<InputSlot>::new(capacity)
2602 .add_consumer()
2603 .build();
2604 let consumer = consumers.pop().expect("one consumer");
2605 let handles = ReplicaPipelineHandles {
2606 input_producer,
2607 journal_cursor: Arc::new(make_journal_cursor(0)),
2608 last_seq: melin_transport_core::DurableWireSeqCursor::detached(
2609 melin_transport_core::WireSeq::new(0),
2610 ),
2611 chain_hash_lock: None,
2612 stream_marks: Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new())),
2613 journal_failed: Arc::new(AtomicBool::new(false)),
2614 pipeline_shutdown: Arc::new(AtomicBool::new(false)),
2615 journal_handle: std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> {
2616 Ok(11)
2617 }),
2618 matching_handle: std::thread::spawn(|| counter_server::CounterFactory.empty()),
2619 drain_handle: std::thread::spawn(|| {}),
2620 shadow_handle: None,
2621 };
2622 (handles, consumer)
2623 }
2624
2625 #[test]
2631 fn teardown_publishes_shutdown_sentinel() {
2632 let (handles, mut consumer) = teardown_fixture(8);
2633 assert!(matches!(
2634 teardown_replica_pipeline::<counter_server::Counter, u32>(handles),
2635 TeardownOutcome::Clean(..)
2636 ));
2637 let (_, slot) = consumer.try_consume().expect("sentinel on the ring");
2638 assert!(
2639 slot.event.is_shutdown(),
2640 "published slot must be the sentinel"
2641 );
2642 assert!(consumer.try_consume().is_none(), "exactly one sentinel");
2643 }
2644
2645 fn backoff_after_disconnect(heard_from_primary: bool) -> std::time::Duration {
2652 let dir = tempfile::tempdir().expect("tempdir");
2653 type Writer = melin_journal::BufferedWriter<CounterEvent>;
2654 let mut pipeline: Option<ReplicaPipelineHandles<counter_server::Counter, Writer>> = None;
2655 let mut divergence_resyncs = 0u32;
2656 let mut backoff = MAX_BACKOFF;
2657 let shutdown = AtomicBool::new(true);
2658 let promote = crate::promotion::PromotionRequest::new();
2659 let after = handle_session_exit::<counter_server::Counter, Writer>(
2660 StreamingResult {
2661 exit: SessionExit::Disconnected,
2662 heard_from_primary,
2663 },
2664 &mut pipeline,
2665 &mut divergence_resyncs,
2666 &mut backoff,
2667 0,
2668 &dir.path().join("r.journal"),
2669 &dir.path().join("r.snapshot"),
2670 &counter_server::CounterFactory,
2671 &melin_transport_core::fence::FenceState::new(0),
2672 &shutdown,
2673 &promote,
2674 || {},
2675 );
2676 assert!(matches!(after, AfterSession::Reconnect));
2677 backoff
2678 }
2679
2680 #[test]
2685 fn disconnect_resets_backoff_when_primary_spoke() {
2686 assert_eq!(
2689 backoff_after_disconnect(true),
2690 std::time::Duration::from_secs(2)
2691 );
2692 }
2693
2694 #[test]
2699 fn disconnect_keeps_escalated_backoff_when_primary_silent() {
2700 assert_eq!(backoff_after_disconnect(false), MAX_BACKOFF);
2701 }
2702
2703 #[test]
2708 fn teardown_skips_sentinel_when_journal_failed_and_ring_full() {
2709 let (mut handles, mut consumer) = teardown_fixture(4);
2710 for seq in 0..4 {
2711 assert!(
2712 handles
2713 .input_producer
2714 .try_publish(InputSlot {
2715 sequence: seq,
2716 ..InputSlot::default()
2717 })
2718 .is_ok(),
2719 "ring must have space while filling"
2720 );
2721 }
2722 assert!(
2723 handles
2724 .input_producer
2725 .try_publish(InputSlot::default())
2726 .is_err(),
2727 "ring must be full"
2728 );
2729 handles.journal_failed.store(true, Ordering::Release);
2730 assert!(matches!(
2732 teardown_replica_pipeline::<counter_server::Counter, u32>(handles),
2733 TeardownOutcome::Clean(..)
2734 ));
2735 while let Some((_, slot)) = consumer.try_consume() {
2736 assert!(!slot.event.is_shutdown(), "no sentinel may be published");
2737 }
2738 }
2739
2740 #[test]
2741 fn pending_ack_queue_wraps_around() {
2742 let mut q = PendingAckQueue::new(8);
2743 let cursor = make_journal_cursor(100);
2744
2745 for round in 0..3 {
2747 for i in 0..8 {
2748 let target = (round * 8 + i) as u64 + 1;
2749 q.push(target, target * 10);
2750 }
2751 assert!(q.is_full());
2752 let seq = q.pop_ready(&cursor).expect("should be ready");
2753 assert_eq!(seq, (round * 8 + 8) as u64 * 10);
2754 assert!(q.is_empty());
2755 }
2756 }
2757
2758 #[test]
2759 fn pending_ack_queue_pop_all_blocking_empty() {
2760 let mut q = PendingAckQueue::new(8);
2761 let cursor = make_journal_cursor(0);
2762 assert!(
2763 q.pop_all_blocking(&cursor, true, &AtomicBool::new(false))
2764 .is_none()
2765 );
2766 }
2767
2768 #[test]
2771 fn dual_track_returns_none_when_both_tracks_idle() {
2772 let mut q = PendingAckQueue::new(8);
2773 let cursor = make_journal_cursor(0);
2774 assert!(
2775 try_flush_dual_track(&mut q, &cursor, 0, 0, 0).is_none(),
2776 "no advance on either track → no ack"
2777 );
2778 }
2779
2780 #[test]
2781 fn dual_track_fires_on_persisted_advance_only() {
2782 let mut q = PendingAckQueue::new(8);
2787 q.push(10, 100);
2788 let cursor = make_journal_cursor(20);
2789 let ack = try_flush_dual_track(&mut q, &cursor, 50, 0, 50).expect("persisted advanced");
2790 assert_eq!(ack.acked_sequence, 100);
2791 assert_eq!(ack.in_memory_sequence, 50);
2792 }
2793
2794 #[test]
2795 fn dual_track_fires_on_in_memory_advance_only() {
2796 let mut q = PendingAckQueue::new(8);
2799 let cursor = make_journal_cursor(0);
2800 let ack = try_flush_dual_track(&mut q, &cursor, 200, 100, 100).expect("in-memory advanced");
2801 assert_eq!(ack.acked_sequence, 100);
2802 assert_eq!(ack.in_memory_sequence, 200);
2803 }
2804
2805 #[test]
2806 fn dual_track_coalesces_until_caller_updates_trackers() {
2807 let mut q = PendingAckQueue::new(8);
2813 q.push(10, 100);
2814 let cursor = make_journal_cursor(20);
2815 let ack1 = try_flush_dual_track(&mut q, &cursor, 50, 0, 50).expect("call 1 fires");
2816 assert_eq!(ack1.acked_sequence, 100);
2817 let ack2 = try_flush_dual_track(&mut q, &cursor, 80, 0, 50)
2819 .expect("call 2 fires on in-memory advance");
2820 assert_eq!(
2821 ack2.acked_sequence, 0,
2822 "no new persisted pop → unwrap_or(last_sent)"
2823 );
2824 assert_eq!(ack2.in_memory_sequence, 80);
2825 }
2826
2827 #[test]
2828 fn dual_track_no_duplicate_ack_after_backpressure_drain() {
2829 let mut q = PendingAckQueue::new(4);
2847 for i in 1..=4u64 {
2850 q.push(i * 10, i * 100);
2851 }
2852 assert!(q.is_full());
2853
2854 let cursor = make_journal_cursor(40);
2857 let drained = q.pop_ready(&cursor).expect("all entries durable");
2858 assert_eq!(drained, 400);
2859 assert!(q.is_empty());
2860
2861 let mut last_sent_acked = drained;
2864 let mut last_sent_in_mem = 450u64;
2865
2866 assert!(
2871 try_flush_dual_track(
2872 &mut q,
2873 &cursor,
2874 last_sent_in_mem,
2875 last_sent_acked,
2876 last_sent_in_mem,
2877 )
2878 .is_none(),
2879 "no advance on either track after backpressure drain → no duplicate ack",
2880 );
2881
2882 q.push(50, 500);
2885 let cursor2 = make_journal_cursor(50);
2886 let ack = try_flush_dual_track(&mut q, &cursor2, 500, last_sent_acked, last_sent_in_mem)
2887 .expect("fresh batch after drain fires a new ack");
2888 assert!(
2889 ack.acked_sequence >= last_sent_acked,
2890 "regression: drain sent {last_sent_acked} but next ack carries {}",
2891 ack.acked_sequence,
2892 );
2893 assert_eq!(ack.acked_sequence, 500);
2894 assert_eq!(ack.in_memory_sequence, 500);
2895
2896 last_sent_acked = ack.acked_sequence;
2897 last_sent_in_mem = ack.in_memory_sequence;
2898
2899 assert!(
2903 try_flush_dual_track(
2904 &mut q,
2905 &cursor2,
2906 last_sent_in_mem,
2907 last_sent_acked,
2908 last_sent_in_mem,
2909 )
2910 .is_none(),
2911 "post-resume idle → no further ack",
2912 );
2913 }
2914}