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::{Sender, run_sender};
140
141pub(super) fn log_tcp_info(fd: std::os::unix::io::RawFd, tag: &str, slot: usize) {
150 if !tracing::enabled!(tracing::Level::DEBUG) {
151 return;
152 }
153 let mut info: libc::tcp_info = unsafe { std::mem::zeroed() };
157 let mut len = std::mem::size_of::<libc::tcp_info>() as libc::socklen_t;
158 let rc = unsafe {
159 libc::getsockopt(
160 fd,
161 libc::IPPROTO_TCP,
162 libc::TCP_INFO,
163 &mut info as *mut _ as *mut libc::c_void,
164 &mut len,
165 )
166 };
167 if rc != 0 {
168 tracing::debug!(
169 slot,
170 tag,
171 errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
172 "tcp_info getsockopt failed"
173 );
174 return;
175 }
176 tracing::debug!(
177 slot,
178 tag,
179 state = info.tcpi_state,
180 ca_state = info.tcpi_ca_state,
181 rtt_us = info.tcpi_rtt,
182 rttvar_us = info.tcpi_rttvar,
183 snd_cwnd = info.tcpi_snd_cwnd,
184 snd_ssthresh = info.tcpi_snd_ssthresh,
185 snd_mss = info.tcpi_snd_mss,
186 rcv_mss = info.tcpi_rcv_mss,
187 unacked = info.tcpi_unacked,
188 retrans = info.tcpi_retrans,
189 total_retrans = info.tcpi_total_retrans,
190 lost = info.tcpi_lost,
191 rcv_space = info.tcpi_rcv_space,
192 rto_us = info.tcpi_rto,
193 "tcp_info"
194 );
195}
196
197pub(super) fn sleep_checking_flags(
200 duration: std::time::Duration,
201 shutdown: &AtomicBool,
202 promote: &AtomicBool,
203) {
204 let deadline = std::time::Instant::now() + duration;
205 while std::time::Instant::now() < deadline {
206 if shutdown.load(Ordering::Relaxed) || promote.load(Ordering::Acquire) {
207 return;
208 }
209 std::thread::sleep(std::time::Duration::from_millis(100));
210 }
211}
212
213pub(super) enum TeardownOutcome<A, W> {
217 Clean(A, W),
220 JournalFailed(melin_journal::JournalError),
226 Panicked,
228}
229
230pub(super) fn shutdown_pipeline<A: Send + 'static, W: Send + 'static>(
245 shutdown_flag: &AtomicBool,
246 journal_handle: std::thread::JoinHandle<Result<W, melin_journal::JournalError>>,
247 matching_handle: std::thread::JoinHandle<A>,
248 drain_handle: std::thread::JoinHandle<()>,
249 shadow_handle: Option<std::thread::JoinHandle<()>>,
250) -> TeardownOutcome<A, W> {
251 shutdown_flag.store(true, Ordering::Release);
264 let journal_result = journal_handle.join();
269 let matching_result = matching_handle.join();
270 let _ = drain_handle.join();
271 if let Some(h) = shadow_handle {
272 let _ = h.join();
273 }
274 let writer = match journal_result {
275 Ok(Ok(w)) => w,
276 Ok(Err(e)) => return TeardownOutcome::JournalFailed(e),
278 Err(_) => return TeardownOutcome::Panicked,
279 };
280 match matching_result {
281 Ok(exchange) => TeardownOutcome::Clean(exchange, writer),
282 Err(_) => TeardownOutcome::Panicked,
283 }
284}
285
286pub(super) struct ReplicaPipelineHandles<A: Application, W: Send + 'static> {
295 pub(super) input_producer: melin_pipeline::ring::Producer<InputSlot<A::Event>>,
296 pub(super) journal_cursor: Arc<melin_pipeline::padding::Sequence>,
297 pub(super) last_seq: melin_transport_core::DurableWireSeqCursor,
303 pub(super) chain_hash_lock:
307 Option<Arc<melin_pipeline::seqlock::SeqLock<melin_transport_core::pipeline::FsyncState>>>,
308 pub(super) stream_marks: melin_transport_core::pipeline::StreamMarkQueue,
313 pub(super) journal_failed: Arc<AtomicBool>,
320 pub(super) pipeline_shutdown: Arc<AtomicBool>,
323 pub(super) journal_handle: std::thread::JoinHandle<Result<W, melin_journal::JournalError>>,
324 pub(super) matching_handle: std::thread::JoinHandle<A>,
325 pub(super) drain_handle: std::thread::JoinHandle<()>,
326 pub(super) shadow_handle: Option<std::thread::JoinHandle<()>>,
327}
328
329#[allow(clippy::too_many_arguments)]
333pub(super) fn build_replica_pipeline_with_threads<A, W>(
334 exchange: A,
335 writer: W,
336 cores: crate::server::PipelineCores,
337 snapshot_interval_ms: u64,
338 snapshot_path: std::path::PathBuf,
339 group_commit_delay: std::time::Duration,
340 busy_spin: bool,
341 fence_state: Arc<melin_transport_core::fence::FenceState>,
342) -> Result<ReplicaPipelineHandles<A, W>, Box<dyn std::error::Error>>
343where
344 A: Application + Send + 'static,
345 A::Event: Send + Sync + 'static,
346 A::Report: Send + 'static,
347 A::QueryResponse: Send + 'static,
348 W: JournalWrite<A::Event> + Send + 'static,
349 JournalStage<A::Event, W>: JournalStageRun<A::Event, Writer = W>,
350{
351 let shadow_exchange = <A as Application>::clone_via_snapshot(&exchange)?;
352
353 let enable_shadow = snapshot_interval_ms > 0;
354 let shadow_initial_epoch = fence_state.epoch();
357 let pipeline = melin_transport_core::pipeline::build_replica_pipeline(
358 exchange,
359 writer,
360 4096, group_commit_delay,
362 busy_spin,
363 enable_shadow,
364 fence_state,
365 );
366
367 let pipeline_shutdown = Arc::new(AtomicBool::new(false));
368
369 let ps = Arc::clone(&pipeline_shutdown);
370 let journal_core = cores.journal;
371 let mut journal_stage = pipeline.journal_stage;
372 let stream_marks: melin_transport_core::pipeline::StreamMarkQueue =
377 Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new()));
378 journal_stage.set_stream_marks(Arc::clone(&stream_marks));
379 let journal_failed = Arc::new(AtomicBool::new(false));
380 let journal_failed_latch = Arc::clone(&journal_failed);
381 let journal_handle = std::thread::Builder::new()
382 .name("journal".into())
383 .spawn(move || {
384 melin_app::affinity::pin_thread("journal", journal_core);
385 let result = journal_stage.run(&ps);
386 if let Err(ref e) = result {
387 journal_failed_latch.store(true, Ordering::Release);
394 tracing::error!(error = %e, "replica journal stage failed — session teardown");
395 }
396 result
397 })
398 .expect("spawn journal thread");
399
400 let ps = Arc::clone(&pipeline_shutdown);
401 let matching_core = cores.matching;
402 let matching_stage = pipeline.matching_stage;
403 let matching_handle = std::thread::Builder::new()
404 .name("matching".into())
405 .spawn(move || {
406 melin_app::affinity::pin_thread("matching", matching_core);
407 matching_stage.run(&ps)
408 })
409 .expect("spawn matching thread");
410
411 let ps = Arc::clone(&pipeline_shutdown);
414 let drain_core = cores.response;
415 let drain_consumer = pipeline.drain_consumer;
416 let drain_handle = std::thread::Builder::new()
417 .name("drain".into())
418 .spawn(move || {
419 melin_app::affinity::pin_thread("drain", drain_core);
420 let mut consumer = drain_consumer;
421 let mut batch = vec![OutputSlot::<A::Report, A::QueryResponse>::default(); 256];
422 loop {
423 if ps.load(Ordering::Relaxed) {
424 return;
425 }
426 let count = consumer.consume_batch(&mut batch, 256);
427 if count == 0 {
428 if busy_spin {
429 std::hint::spin_loop();
430 } else {
431 std::thread::yield_now();
432 }
433 }
434 }
435 })
436 .expect("spawn drain thread");
437
438 let shadow_handle = if let Some(shadow_cons) = pipeline.shadow_consumer {
439 let snap_path = snapshot_path;
440 let chain_lock = pipeline
441 .chain_hash_lock
442 .as_ref()
443 .expect("chain hash lock with shadow")
444 .clone();
445 let ps = Arc::clone(&pipeline_shutdown);
446 let shadow_core = cores.shadow;
447 Some(
448 std::thread::Builder::new()
449 .name("replica-shadow".into())
450 .spawn(move || {
451 melin_app::affinity::pin_thread("replica-shadow", shadow_core);
452 melin_transport_core::shadow::run(
453 shadow_cons,
454 shadow_exchange,
455 snap_path,
456 std::time::Duration::from_millis(snapshot_interval_ms),
457 chain_lock,
458 &ps,
459 false,
460 shadow_initial_epoch,
461 );
462 })
463 .expect("spawn shadow thread"),
464 )
465 } else {
466 None
467 };
468
469 Ok(ReplicaPipelineHandles {
470 input_producer: pipeline.input_producer,
471 journal_cursor: pipeline.cursors.journal_ring_arc(),
472 last_seq: pipeline.cursors.durable_wire_seq(),
473 chain_hash_lock: pipeline.chain_hash_lock,
474 stream_marks,
475 journal_failed,
476 pipeline_shutdown,
477 journal_handle,
478 matching_handle,
479 drain_handle,
480 shadow_handle,
481 })
482}
483
484pub(super) fn teardown_replica_pipeline<A: Application + Send + 'static, W: Send + 'static>(
488 handles: ReplicaPipelineHandles<A, W>,
489) -> TeardownOutcome<A, W> {
490 shutdown_pipeline::<A, W>(
491 &handles.pipeline_shutdown,
492 handles.journal_handle,
493 handles.matching_handle,
494 handles.drain_handle,
495 handles.shadow_handle,
496 )
497}
498
499pub(super) const MAX_INPROCESS_DIVERGENCE_RESYNCS: u32 = 1;
510
511#[allow(clippy::type_complexity)]
528pub(super) fn recover_replica_state<A, W>(
529 journal_path: &std::path::Path,
530 snapshot_path: &std::path::Path,
531 factory: &dyn melin_app::app_factory::AppFactory<App = A>,
532 fence_state: &melin_transport_core::fence::FenceState,
533) -> Result<(Option<A>, Option<W>, u64, [u8; 32]), Box<dyn std::error::Error>>
534where
535 A: Application,
536 W: melin_journal::JournalWrite<A::Event>,
537{
538 let lineage_exists =
539 journal_path.exists() || !melin_journal::segment::list_archives(journal_path)?.is_empty();
540 if !lineage_exists {
541 return Ok((None, None, 0u64, [0u8; 32]));
542 }
543 let engine = if snapshot_path.exists() {
544 tracing::info!("recovering replica from snapshot + journal");
545 melin_transport_core::JournaledApp::<A, W>::recover_from_snapshot(
546 snapshot_path,
547 journal_path,
548 )?
549 } else {
550 melin_transport_core::JournaledApp::<A, W>::recover(factory.empty(), journal_path)?
551 };
552 let next = engine.next_sequence();
553 let last = next.saturating_sub(1);
554 let hash = engine.chain_hash().unwrap_or([0u8; 32]);
555 fence_state.observe_epoch(engine.recovered_epoch());
558 let (mut exchange, writer) = engine.into_parts();
559 factory.apply_operator_policy(&mut exchange);
560 Ok((Some(exchange), Some(writer), last, hash))
561}
562
563pub(super) const MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(30);
567
568pub(in crate::replication) enum AfterSession<A, W> {
572 Return(ReceiverResult<A, W>),
576 Resync {
581 exchange: Option<A>,
582 journal_writer: Option<W>,
583 last_sequence: u64,
584 chain_hash: [u8; 32],
585 },
586 Reconnect,
590}
591
592#[allow(clippy::too_many_arguments)]
613pub(in crate::replication) fn handle_session_exit<A, W>(
614 result: StreamingResult,
615 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
616 divergence_resyncs: &mut u32,
617 backoff: &mut std::time::Duration,
618 last_sequence: u64,
619 journal_path: &std::path::Path,
620 snapshot_path: &std::path::Path,
621 factory: &dyn melin_app::app_factory::AppFactory<App = A>,
622 fence_state: &melin_transport_core::fence::FenceState,
623 shutdown: &AtomicBool,
624 promote: &AtomicBool,
625 mut close: impl FnMut(),
626) -> AfterSession<A, W>
627where
628 A: Application + Send + 'static,
629 W: JournalWrite<A::Event> + Send + 'static,
630{
631 let StreamingResult {
632 exit,
633 received_data,
634 } = result;
635
636 if !matches!(exit, SessionExit::Disconnected)
642 && let Some(p) = pipeline.as_mut()
643 && !p.journal_failed.load(Ordering::Acquire)
644 {
645 p.input_producer
646 .publish(InputSlot::<A::Event>::shutdown_sentinel());
647 }
648
649 match exit {
650 SessionExit::Shutdown => {
651 if let Some(p) = pipeline.take() {
652 let _ = teardown_replica_pipeline::<A, W>(p);
653 }
654 AfterSession::Return(Ok(None))
655 }
656
657 SessionExit::Promote => AfterSession::Return(match pipeline.take() {
658 Some(p) => match teardown_replica_pipeline::<A, W>(p) {
659 TeardownOutcome::Clean(ex, wr) => Ok(Some((ex, wr))),
660 _ => Err("pipeline failed during promotion".into()),
661 },
662 None => Err("pipeline missing on promote".into()),
663 }),
664
665 SessionExit::Fatal(e) => {
666 let outcome = match pipeline.take() {
667 Some(p) => teardown_replica_pipeline::<A, W>(p),
668 None => return AfterSession::Return(Err(e)),
671 };
672 let TeardownOutcome::JournalFailed(
682 je @ melin_journal::JournalError::ReplicaChainDivergence { .. },
683 ) = outcome
684 else {
685 return AfterSession::Return(Err(e));
686 };
687
688 *divergence_resyncs += 1;
689 let attempt = *divergence_resyncs;
690 if attempt > MAX_INPROCESS_DIVERGENCE_RESYNCS {
691 return AfterSession::Return(Err(format!(
692 "mid-stream chain divergence recurred {attempt} times — giving up on \
693 in-process resync (each cycle archives the local journal and re-seeds \
694 from the primary; recurrence at this rate means the primary keeps \
695 streaming history that forks from what it announces): {je}"
696 )
697 .into()));
698 }
699 tracing::warn!(
700 error = %je,
701 attempt,
702 max_attempts = MAX_INPROCESS_DIVERGENCE_RESYNCS,
703 "mid-stream chain divergence — re-deriving local state for in-process resync"
704 );
705 close();
707 match recover_replica_state::<A, W>(journal_path, snapshot_path, factory, fence_state) {
708 Ok((exchange, journal_writer, seq, hash)) => AfterSession::Resync {
709 exchange,
710 journal_writer,
711 last_sequence: seq,
712 chain_hash: hash,
713 },
714 Err(e) => AfterSession::Return(Err(e)),
715 }
716 }
717
718 SessionExit::Disconnected => {
719 close();
722 if received_data {
725 *backoff = std::time::Duration::from_secs(1);
726 }
727 tracing::warn!(
728 last_sequence,
729 backoff_secs = backoff.as_secs(),
730 "reconnecting to primary"
731 );
732 sleep_checking_flags(*backoff, shutdown, promote);
733 *backoff = (*backoff * 2).min(MAX_BACKOFF);
734 AfterSession::Reconnect
735 }
736 }
737}
738
739pub(in crate::replication) fn take_pipeline_for_promotion<A, W>(
753 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
754 exchange: &mut Option<A>,
755 journal_writer: &mut Option<W>,
756) -> ReceiverResult<A, W>
757where
758 A: Application + Send + 'static,
759 W: JournalWrite<A::Event> + Send + 'static,
760{
761 if let Some(mut p) = pipeline.take() {
762 p.input_producer
763 .publish(InputSlot::<A::Event>::shutdown_sentinel());
764 if let TeardownOutcome::Clean(e, w) = teardown_replica_pipeline::<A, W>(p) {
765 *exchange = Some(e);
766 *journal_writer = Some(w);
767 }
768 }
769 match (exchange.take(), journal_writer.take()) {
770 (Some(e), Some(w)) => Ok(Some((e, w))),
771 _ => Err("promotion requested but no local state available".into()),
772 }
773}
774
775type ResyncTransfer<A> = (A, u64, [u8; 32], u64);
778
779pub(in crate::replication) enum ResyncDecision {
782 Ready {
787 segment_start_sequence: u64,
788 anchor_hash: [u8; 32],
789 resume_sequence: u64,
790 },
791 Retry,
795}
796
797fn receive_resync_transfer<A, S>(
807 source: &mut S,
808 snapshot_path: &std::path::Path,
809 journal_path: &std::path::Path,
810 fence_state: &melin_transport_core::fence::FenceState,
811) -> Result<ResyncTransfer<A>, Box<dyn std::error::Error + Send + Sync>>
812where
813 A: Application,
814 S: ControlFrameSource,
815{
816 let (snap_len, snap_sequence, snap_chain_hash) =
817 match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
818 PrimaryMessage::SnapshotBegin {
819 snapshot_len,
820 snap_sequence,
821 snap_chain_hash,
822 } => (snapshot_len, snap_sequence, snap_chain_hash),
823 other => return Err(format!("expected SnapshotBegin, got {other:?}").into()),
824 };
825
826 tracing::info!(snap_sequence, snap_len, "receiving snapshot");
827 let tmp_path = snapshot_path.with_extension("snapshot.tmp");
828 receive_chunked_body(source, &tmp_path, snap_len, "snapshot")?;
829 std::fs::rename(&tmp_path, snapshot_path)?;
830 tracing::info!(snap_sequence, snap_len, "snapshot received and verified");
831
832 let (snap_exchange, _snap_seq, snap_hash, snap_epoch) =
833 melin_transport_core::snapshot::load::<A>(snapshot_path)?;
834 if snap_hash != snap_chain_hash {
835 return Err(format!(
836 "snapshot chain hash mismatch: primary sent {snap_chain_hash:02x?}, \
837 loaded snapshot has {snap_hash:02x?}"
838 )
839 .into());
840 }
841 fence_state.observe_epoch(snap_epoch);
844
845 let seed_len = match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
850 PrimaryMessage::SegmentSeedBegin { seed_len } => seed_len,
851 other => {
852 return Err(format!("expected SegmentSeedBegin after snapshot, got {other:?}").into());
853 }
854 };
855 let seed_tmp = journal_path.with_extension("seed.tmp");
856 receive_chunked_body(source, &seed_tmp, seed_len, "segment seed")?;
857 if let Err(e) =
863 melin_journal::segment::verify_segment_prefix(&seed_tmp, snap_sequence, seed_len)
864 {
865 let _ = std::fs::remove_file(&seed_tmp);
866 return Err(format!("segment seed failed structural verification: {e}").into());
867 }
868 std::fs::rename(&seed_tmp, journal_path)?;
869 melin_journal::segment::fsync_parent_dir(journal_path)?;
870 Ok((snap_exchange, snap_sequence, snap_chain_hash, seed_len))
871}
872
873#[allow(clippy::too_many_arguments)]
886pub(in crate::replication) fn handle_resync_verdict<A, W, S>(
887 divergent: bool,
888 source: &mut S,
889 pipeline: &mut Option<ReplicaPipelineHandles<A, W>>,
890 exchange: &mut Option<A>,
891 journal_writer: &mut Option<W>,
892 journal_path: &std::path::Path,
893 snapshot_path: &std::path::Path,
894 fence_state: &melin_transport_core::fence::FenceState,
895 last_sequence: &mut u64,
896 chain_hash: &mut [u8; 32],
897) -> Result<ResyncDecision, Box<dyn std::error::Error + Send + Sync>>
898where
899 A: Application + Send + 'static,
900 W: JournalWrite<A::Event> + Send + 'static,
901 S: ControlFrameSource,
902{
903 if divergent {
908 tracing::warn!(
909 last_sequence = *last_sequence,
910 "primary reports chain divergence — archiving local journal, resyncing from snapshot"
911 );
912 } else {
913 tracing::info!("primary requires snapshot transfer — receiving snapshot");
914 }
915
916 if let Some(mut p) = pipeline.take() {
917 p.input_producer
918 .publish(InputSlot::<A::Event>::shutdown_sentinel());
919 let _ = teardown_replica_pipeline::<A, W>(p);
920 }
921
922 *exchange = None;
929 *journal_writer = None;
930
931 let reason = if divergent {
935 ArchiveReason::Divergent
936 } else {
937 ArchiveReason::Resync
938 };
939 archive_local_lineage(journal_path, snapshot_path, reason)?;
940 *last_sequence = 0;
942 *chain_hash = [0u8; 32];
943
944 let (snap_exchange, snap_sequence, snap_chain_hash, seed_len) =
945 match receive_resync_transfer::<A, S>(source, snapshot_path, journal_path, fence_state) {
946 Ok(v) => v,
947 Err(e) => {
948 tracing::warn!(error = %e, "snapshot transfer failed — retrying");
949 let _ = std::fs::remove_file(snapshot_path);
953 return Ok(ResyncDecision::Retry);
954 }
955 };
956 *exchange = Some(snap_exchange);
957
958 let writer = W::open_append(journal_path, snap_sequence, seed_len)?;
965 let seeded_chain = writer.chain_hash().unwrap_or(snap_chain_hash);
966 if snap_chain_hash != [0u8; 32] && seeded_chain != snap_chain_hash {
967 return Err(format!(
968 "segment seed chain at {snap_sequence} disagrees with the transferred snapshot's \
969 hash — inconsistent primary"
970 )
971 .into());
972 }
973 let seeded_info = melin_journal::segment::read_header_info(journal_path)?;
974 *journal_writer = Some(writer);
975
976 match decode_primary_message(&source.next_frame(MAX_CONTROL_FRAME)?)? {
979 PrimaryMessage::StreamStart {
980 start_sequence,
981 segment_start_sequence,
982 anchor_hash,
983 epoch,
984 } => {
985 if segment_start_sequence != seeded_info.starting_sequence
986 || anchor_hash != seeded_info.anchor_hash
987 {
988 return Err(format!(
989 "post-snapshot StreamStart lineage (start {segment_start_sequence}) \
990 disagrees with the transferred segment seed (start {}) — inconsistent \
991 primary",
992 seeded_info.starting_sequence
993 )
994 .into());
995 }
996 fence_state.observe_epoch(epoch);
1000 tracing::info!(
1001 start_sequence,
1002 epoch,
1003 "streaming resumed after snapshot transfer"
1004 );
1005 Ok(ResyncDecision::Ready {
1006 segment_start_sequence,
1007 anchor_hash,
1008 resume_sequence: snap_sequence,
1009 })
1010 }
1011 other => Err(format!("expected StreamStart after snapshot, got {other:?}").into()),
1012 }
1013}
1014
1015#[cfg(test)]
1016mod tests {
1017 use std::io::Write;
1018 use std::sync::Arc;
1019 use std::sync::atomic::AtomicU32;
1020
1021 use super::auth::{authenticate_replica, authenticate_with_primary};
1022 use super::*;
1023 use counter_server::CounterEvent;
1029 type InputSlot = melin_transport_core::pipeline::InputSlot<CounterEvent>;
1030 use melin_transport_core::PipelineCursors;
1031 use melin_transport_core::replication::protocol::{
1032 MAX_CONTROL_FRAME, MAX_DATA_FRAME, MSG_AUTH_OK, MSG_CHALLENGE_RESPONSE, MSG_SNAPSHOT_BEGIN,
1033 MSG_SNAPSHOT_CHUNK, MSG_SNAPSHOT_END, decode_auth_result, decode_challenge,
1034 decode_challenge_response, decode_primary_message, decode_replica_message, encode_ack,
1035 encode_auth_failed, encode_auth_ok, encode_chain_check, encode_challenge,
1036 encode_challenge_response, encode_handshake, encode_hash_mismatch, encode_heartbeat,
1037 encode_input_batch, encode_need_snapshot, encode_rotate, encode_segment_seed_begin,
1038 encode_snapshot_begin, encode_snapshot_chunk, encode_snapshot_end, encode_stream_start,
1039 read_frame, try_decode_input_batch,
1040 };
1041
1042 fn encode_input_batch_with_seq(end_sequence: u64, buf: &mut Vec<u8>) {
1046 let slot = InputSlot {
1047 connection_id: 0,
1048 key_hash: 0,
1049 request_seq: 0,
1050 sequence: end_sequence,
1051 timestamp_ns: 0,
1052 event: melin_journal::JournalEvent::Tick { now_ns: 0 },
1053 publish_ts: Default::default(),
1054 recv_ts: Default::default(),
1055 };
1056 encode_input_batch(&[slot], buf);
1057 }
1058
1059 #[test]
1060 fn handshake_encode_decode_round_trip() {
1061 let handshake = Handshake {
1062 last_sequence: 42,
1063 chain_hash: [0xAB; 32],
1064 epoch: 9,
1066 };
1067 let mut buf = Vec::new();
1068 encode_handshake(&handshake, &mut buf);
1069
1070 let payload = &buf[4..];
1072 let msg = decode_replica_message(payload).unwrap();
1073 match msg {
1074 ReplicaMessage::Handshake(h) => {
1075 assert_eq!(h.last_sequence, 42);
1076 assert_eq!(h.chain_hash, [0xAB; 32]);
1077 assert_eq!(h.epoch, 9);
1078 }
1079 _ => panic!("expected Handshake"),
1080 }
1081 }
1082
1083 #[test]
1084 fn ack_encode_decode_round_trip() {
1085 let ack = Ack {
1086 acked_sequence: 1000,
1087 in_memory_sequence: 1024,
1088 };
1089 let mut buf = Vec::new();
1090 encode_ack(&ack, &mut buf);
1091
1092 let payload = &buf[4..];
1093 let msg = decode_replica_message(payload).unwrap();
1094 match msg {
1095 ReplicaMessage::Ack(a) => {
1096 assert_eq!(a.acked_sequence, 1000);
1097 assert_eq!(a.in_memory_sequence, 1024);
1098 }
1099 _ => panic!("expected Ack"),
1100 }
1101 }
1102
1103 #[test]
1111 fn ack_wire_byte_pattern() {
1112 let ack = Ack {
1113 acked_sequence: 0xDEAD_BEEF_CAFE_F00D,
1114 in_memory_sequence: 0x1122_3344_5566_7788,
1115 };
1116 let mut buf = Vec::new();
1117 encode_ack(&ack, &mut buf);
1118 let expected: &[u8] = &[
1121 0x11, 0x00, 0x00, 0x00, 0x02, 0x0D, 0xF0, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, ];
1126 assert_eq!(buf.as_slice(), expected, "Ack wire layout drifted");
1127 }
1128
1129 #[test]
1130 fn stream_start_encode_decode_round_trip() {
1131 let mut buf = Vec::new();
1132 encode_stream_start(99, 42, [0xAA; 32], 5, &mut buf);
1134
1135 let payload = &buf[4..];
1136 let msg = decode_primary_message(payload).unwrap();
1137 match msg {
1138 PrimaryMessage::StreamStart {
1139 start_sequence,
1140 segment_start_sequence,
1141 anchor_hash,
1142 epoch,
1143 } => {
1144 assert_eq!(start_sequence, 99);
1145 assert_eq!(segment_start_sequence, 42);
1146 assert_eq!(anchor_hash, [0xAA; 32]);
1147 assert_eq!(epoch, 5);
1148 }
1149 _ => panic!("expected StreamStart"),
1150 }
1151 }
1152
1153 #[test]
1154 fn heartbeat_encode_decode_round_trip() {
1155 let mut buf = Vec::new();
1156 encode_heartbeat(123, &mut buf);
1157
1158 let payload = &buf[4..];
1159 let msg = decode_primary_message(payload).unwrap();
1160 match msg {
1161 PrimaryMessage::Heartbeat { sequence } => {
1162 assert_eq!(sequence, 123);
1163 }
1164 _ => panic!("expected Heartbeat"),
1165 }
1166 }
1167
1168 #[test]
1169 fn need_snapshot_encode_decode_round_trip() {
1170 let mut buf = Vec::new();
1171 encode_need_snapshot(&mut buf);
1172
1173 let payload = &buf[4..];
1174 let msg = decode_primary_message(payload).unwrap();
1175 assert!(matches!(msg, PrimaryMessage::NeedSnapshot));
1176 }
1177
1178 #[test]
1179 fn hash_mismatch_encode_decode_round_trip() {
1180 let mut buf = Vec::new();
1181 encode_hash_mismatch(&mut buf);
1182
1183 let payload = &buf[4..];
1184 let msg = decode_primary_message(payload).unwrap();
1185 assert!(matches!(msg, PrimaryMessage::HashMismatch));
1186 }
1187
1188 #[test]
1189 fn rotate_encode_decode_round_trip() {
1190 let mut buf = Vec::new();
1191 encode_rotate(7_000_000, &[0xBB; 32], &mut buf);
1192
1193 let payload = &buf[4..];
1194 let msg = decode_primary_message(payload).unwrap();
1195 match msg {
1196 PrimaryMessage::Rotate {
1197 boundary_seq,
1198 tail_hash,
1199 } => {
1200 assert_eq!(boundary_seq, 7_000_000);
1201 assert_eq!(tail_hash, [0xBB; 32]);
1202 }
1203 _ => panic!("expected Rotate"),
1204 }
1205 }
1206
1207 #[test]
1208 fn chain_check_encode_decode_round_trip() {
1209 let mut buf = Vec::new();
1210 encode_chain_check(555, &[0xCC; 32], &mut buf);
1211
1212 let payload = &buf[4..];
1213 let msg = decode_primary_message(payload).unwrap();
1214 match msg {
1215 PrimaryMessage::ChainCheck {
1216 sequence,
1217 chain_hash,
1218 } => {
1219 assert_eq!(sequence, 555);
1220 assert_eq!(chain_hash, [0xCC; 32]);
1221 }
1222 _ => panic!("expected ChainCheck"),
1223 }
1224 }
1225
1226 #[test]
1230 fn rotate_wire_byte_pattern() {
1231 let mut hash = [0u8; 32];
1232 for (i, b) in hash.iter_mut().enumerate() {
1233 *b = i as u8;
1234 }
1235 let mut buf = Vec::new();
1236 encode_rotate(0xDEAD_BEEF_CAFE_F00D, &hash, &mut buf);
1237 let mut expected = vec![
1240 0x29, 0x00, 0x00, 0x00, 0x16, 0x0D, 0xF0, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE, ];
1244 expected.extend_from_slice(&hash);
1245 assert_eq!(
1246 buf.as_slice(),
1247 expected.as_slice(),
1248 "Rotate wire layout drifted"
1249 );
1250 }
1251
1252 #[test]
1253 fn segment_seed_begin_encode_decode_round_trip() {
1254 let mut buf = Vec::new();
1255 encode_segment_seed_begin(123_456, &mut buf);
1256
1257 let payload = &buf[4..];
1258 let msg = decode_primary_message(payload).unwrap();
1259 match msg {
1260 PrimaryMessage::SegmentSeedBegin { seed_len } => {
1261 assert_eq!(seed_len, 123_456);
1262 }
1263 _ => panic!("expected SegmentSeedBegin"),
1264 }
1265 }
1266
1267 #[test]
1268 fn unknown_replica_message_type_is_error() {
1269 let payload = [0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
1270 let result = decode_replica_message(&payload);
1271 assert!(result.is_err());
1272 }
1273
1274 #[test]
1275 fn unknown_primary_message_type_is_error() {
1276 let payload = [0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
1277 let result = decode_primary_message(&payload);
1278 assert!(result.is_err());
1279 }
1280
1281 #[test]
1282 fn read_frame_enforces_max_size() {
1283 let mut buf = Vec::new();
1285 buf.extend_from_slice(&1000u32.to_le_bytes());
1286 buf.extend_from_slice(&[0u8; 100]); let mut cursor = std::io::Cursor::new(buf);
1289 let result = read_frame(&mut cursor, 64);
1290 assert!(result.is_err());
1291 }
1292
1293 #[test]
1294 fn challenge_encode_decode_round_trip() {
1295 let nonce = [0x42; 32];
1296 let mut buf = Vec::new();
1297 encode_challenge(&nonce, &mut buf);
1298
1299 let payload = &buf[4..];
1300 let decoded = decode_challenge(payload).unwrap();
1301 assert_eq!(decoded, nonce);
1302 }
1303
1304 #[test]
1305 fn challenge_response_encode_decode_round_trip() {
1306 let sig = [0xAA; 64];
1307 let pubkey = [0xBB; 32];
1308 let mut buf = Vec::new();
1309 encode_challenge_response(&sig, &pubkey, &mut buf);
1310
1311 let payload = &buf[4..];
1312 let (decoded_sig, decoded_pubkey) = decode_challenge_response(payload).unwrap();
1313 assert_eq!(decoded_sig, sig);
1314 assert_eq!(decoded_pubkey, pubkey);
1315 }
1316
1317 #[test]
1318 fn auth_ok_encode_decode_round_trip() {
1319 let mut buf = Vec::new();
1320 encode_auth_ok(&mut buf);
1321
1322 let payload = &buf[4..];
1323 assert!(decode_auth_result(payload).unwrap());
1324 }
1325
1326 #[test]
1327 fn auth_failed_encode_decode_round_trip() {
1328 let mut buf = Vec::new();
1329 encode_auth_failed(&mut buf);
1330
1331 let payload = &buf[4..];
1332 assert!(!decode_auth_result(payload).unwrap());
1333 }
1334
1335 #[test]
1336 fn decode_challenge_rejects_wrong_tag() {
1337 let mut payload = [0u8; 33];
1338 payload[0] = MSG_AUTH_OK;
1339 assert!(decode_challenge(&payload).is_err());
1340 }
1341
1342 #[test]
1343 fn decode_challenge_response_rejects_short_payload() {
1344 let payload = [MSG_CHALLENGE_RESPONSE; 10]; assert!(decode_challenge_response(&payload).is_err());
1346 }
1347
1348 #[test]
1349 fn auth_round_trip_valid_key() {
1350 use ed25519_dalek::SigningKey;
1351 use std::os::unix::net::UnixStream;
1352
1353 let repl_key = SigningKey::from_bytes(&[0xFC; 32]);
1354 let pub_b64 = base64::Engine::encode(
1355 &base64::engine::general_purpose::STANDARD,
1356 repl_key.verifying_key().to_bytes(),
1357 );
1358 let keys_content = format!("replication {pub_b64} test-replica\n");
1359 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1360
1361 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1362 primary_stream
1363 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1364 .unwrap();
1365 replica_stream
1366 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1367 .unwrap();
1368
1369 let repl_key_clone = SigningKey::from_bytes(&[0xFC; 32]);
1370 let replica_handle = std::thread::spawn(move || {
1371 let mut conn = replica_stream;
1372 authenticate_with_primary(&mut conn, &repl_key_clone)
1373 });
1374
1375 let mut conn = primary_stream;
1376 authenticate_replica(&mut conn, &authorized_keys).unwrap();
1377
1378 replica_handle.join().unwrap().unwrap();
1379 }
1380
1381 #[test]
1382 fn auth_rejects_unknown_key() {
1383 use ed25519_dalek::SigningKey;
1384 use std::os::unix::net::UnixStream;
1385
1386 let authorized_key = SigningKey::from_bytes(&[0xAA; 32]);
1388 let rogue_key = SigningKey::from_bytes(&[0xBB; 32]);
1389 let pub_b64 = base64::Engine::encode(
1390 &base64::engine::general_purpose::STANDARD,
1391 authorized_key.verifying_key().to_bytes(),
1392 );
1393 let keys_content = format!("replication {pub_b64} authorized-replica\n");
1394 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1395
1396 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1397 primary_stream
1398 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1399 .unwrap();
1400 replica_stream
1401 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1402 .unwrap();
1403
1404 let replica_handle = std::thread::spawn(move || {
1405 let mut conn = replica_stream;
1406 authenticate_with_primary(&mut conn, &rogue_key)
1407 });
1408
1409 let mut conn = primary_stream;
1410 let result = authenticate_replica(&mut conn, &authorized_keys);
1411 assert!(result.is_err());
1412 assert!(result.unwrap_err().to_string().contains("unknown"));
1413
1414 let replica_result = replica_handle.join().unwrap();
1416 assert!(replica_result.is_err());
1417 }
1418
1419 #[test]
1420 fn auth_rejects_wrong_permission() {
1421 use ed25519_dalek::SigningKey;
1422 use std::os::unix::net::UnixStream;
1423
1424 let key = SigningKey::from_bytes(&[0xCC; 32]);
1426 let pub_b64 = base64::Engine::encode(
1427 &base64::engine::general_purpose::STANDARD,
1428 key.verifying_key().to_bytes(),
1429 );
1430 let keys_content = format!("trader {pub_b64} wrong-role\n");
1431 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1432
1433 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1434 primary_stream
1435 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1436 .unwrap();
1437 replica_stream
1438 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1439 .unwrap();
1440
1441 let replica_handle = std::thread::spawn(move || {
1442 let mut conn = replica_stream;
1443 authenticate_with_primary(&mut conn, &key)
1444 });
1445
1446 let mut conn = primary_stream;
1447 let result = authenticate_replica(&mut conn, &authorized_keys);
1448 assert!(result.is_err());
1449 assert!(result.unwrap_err().to_string().contains("Replication"));
1450
1451 let replica_result = replica_handle.join().unwrap();
1452 assert!(replica_result.is_err());
1453 }
1454
1455 #[test]
1458 fn auth_rejects_invalid_signature() {
1459 use ed25519_dalek::SigningKey;
1460 use std::os::unix::net::UnixStream;
1461
1462 let correct_key = SigningKey::from_bytes(&[0xDD; 32]);
1464 let pub_b64 = base64::Engine::encode(
1465 &base64::engine::general_purpose::STANDARD,
1466 correct_key.verifying_key().to_bytes(),
1467 );
1468 let keys_content = format!("replication {pub_b64} test-replica\n");
1469 let authorized_keys = melin_app::auth::AuthorizedKeys::parse(&keys_content).unwrap();
1470
1471 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1472 primary_stream
1473 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1474 .unwrap();
1475 replica_stream
1476 .set_read_timeout(Some(std::time::Duration::from_secs(5)))
1477 .unwrap();
1478
1479 let replica_handle = std::thread::spawn(move || {
1482 use melin_transport_core::replication::protocol::*;
1483
1484 let mut reader = replica_stream.try_clone().unwrap();
1485 let mut writer = replica_stream;
1486
1487 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1489 let nonce = decode_challenge(&frame).unwrap();
1490
1491 let wrong_key = SigningKey::from_bytes(&[0xEE; 32]);
1493 let bad_signature = ed25519_dalek::Signer::sign(&wrong_key, &nonce);
1494 let correct_pubkey = correct_key.verifying_key();
1495
1496 let mut buf = Vec::with_capacity(128);
1497 encode_challenge_response(
1498 &bad_signature.to_bytes(),
1499 correct_pubkey.as_bytes(),
1500 &mut buf,
1501 );
1502 writer.write_all(&buf).unwrap();
1503 writer.flush().unwrap();
1504
1505 let result_frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1507 let ok = decode_auth_result(&result_frame).unwrap();
1508 assert!(!ok, "should receive auth failure");
1509 });
1510
1511 let mut conn = primary_stream;
1512 let result = authenticate_replica(&mut conn, &authorized_keys);
1513 assert!(result.is_err());
1514 assert!(
1515 result
1516 .unwrap_err()
1517 .to_string()
1518 .contains("signature verification failed"),
1519 "should fail on signature verification"
1520 );
1521
1522 replica_handle.join().unwrap();
1523 }
1524
1525 #[test]
1526 fn sender_receiver_end_to_end() {
1527 use std::os::unix::net::UnixStream;
1528
1529 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1531
1532 let replication_cursor = Arc::new(AtomicU64::new(0));
1533
1534 let _replica_cursor = Arc::clone(&replication_cursor);
1536 let replica_handle = std::thread::spawn(move || {
1537 let mut reader = replica_stream.try_clone().unwrap();
1538 let mut writer = replica_stream;
1539
1540 let mut buf = Vec::new();
1542 let handshake = Handshake {
1543 last_sequence: 0,
1544 chain_hash: [0u8; 32],
1545 epoch: 0,
1546 };
1547 encode_handshake(&handshake, &mut buf);
1548 writer.write_all(&buf).unwrap();
1549 writer.flush().unwrap();
1550 buf.clear();
1551
1552 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1554 let msg = decode_primary_message(&frame).unwrap();
1555 assert!(matches!(msg, PrimaryMessage::StreamStart { .. }));
1556
1557 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1559 let slots: Vec<InputSlot> = try_decode_input_batch(&frame).expect("decode InputBatch");
1560 let end_seq = slots
1561 .last()
1562 .map(|s| s.sequence)
1563 .expect("InputBatch carried at least one slot");
1564
1565 let ack = Ack {
1567 acked_sequence: end_seq,
1568 in_memory_sequence: end_seq,
1569 };
1570 encode_ack(&ack, &mut buf);
1571 writer.write_all(&buf).unwrap();
1572 writer.flush().unwrap();
1573
1574 end_seq
1575 });
1576
1577 let mut p_reader = primary_stream.try_clone().unwrap();
1579 let mut p_writer = primary_stream;
1580
1581 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1583 let handshake = match decode_replica_message(&frame).unwrap() {
1584 ReplicaMessage::Handshake(h) => h,
1585 _ => panic!("expected Handshake"),
1586 };
1587 assert_eq!(handshake.last_sequence, 0);
1588
1589 let mut buf = Vec::new();
1591 encode_stream_start(0, 1, [0u8; 32], 0, &mut buf); p_writer.write_all(&buf).unwrap();
1593 p_writer.flush().unwrap();
1594 buf.clear();
1595
1596 encode_input_batch_with_seq(42, &mut buf);
1598 p_writer.write_all(&buf).unwrap();
1599 p_writer.flush().unwrap();
1600 buf.clear();
1601
1602 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1604 let ack = match decode_replica_message(&frame).unwrap() {
1605 ReplicaMessage::Ack(a) => a,
1606 _ => panic!("expected Ack"),
1607 };
1608 assert_eq!(ack.acked_sequence, 42);
1609
1610 let end_seq = replica_handle.join().unwrap();
1612 assert_eq!(end_seq, 42);
1613 }
1614
1615 #[test]
1616 fn disconnect_degrades_cursor_to_max() {
1617 let cursor = Arc::new(AtomicU64::new(0));
1622
1623 let handshake_seq = 42u64;
1625 cursor.store(handshake_seq + 1, Ordering::Release);
1626 assert_eq!(cursor.load(Ordering::Acquire), 43);
1627
1628 cursor.fetch_max(100 + 1, Ordering::Release);
1630 assert_eq!(cursor.load(Ordering::Acquire), 101);
1631
1632 cursor.store(PipelineCursors::NO_REPLICA, Ordering::Release);
1635 assert_eq!(cursor.load(Ordering::Acquire), PipelineCursors::NO_REPLICA);
1636
1637 cursor.store(1, Ordering::Release);
1639 assert_eq!(cursor.load(Ordering::Acquire), 1);
1640 }
1641
1642 #[test]
1643 fn ack_advances_cursor_monotonically() {
1644 let cursor = Arc::new(AtomicU64::new(0));
1647
1648 let new_val = 100 + 1;
1650 cursor.fetch_max(new_val, Ordering::Release);
1651 assert_eq!(cursor.load(Ordering::Acquire), 101);
1652
1653 let stale_val = 50 + 1;
1655 cursor.fetch_max(stale_val, Ordering::Release);
1656 assert_eq!(cursor.load(Ordering::Acquire), 101);
1657
1658 let newer_val = 200 + 1;
1660 cursor.fetch_max(newer_val, Ordering::Release);
1661 assert_eq!(cursor.load(Ordering::Acquire), 201);
1662 }
1663
1664 #[test]
1665 fn multiple_data_batches_acked_in_order() {
1666 use std::os::unix::net::UnixStream;
1669
1670 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1671
1672 let replica_handle = std::thread::spawn(move || {
1673 let mut reader = replica_stream.try_clone().unwrap();
1674 let mut writer = replica_stream;
1675 let mut buf = Vec::new();
1676
1677 encode_handshake(
1679 &Handshake {
1680 last_sequence: 0,
1681 chain_hash: [0u8; 32],
1682 epoch: 0,
1683 },
1684 &mut buf,
1685 );
1686 writer.write_all(&buf).unwrap();
1687 writer.flush().unwrap();
1688 buf.clear();
1689
1690 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1692 assert!(matches!(
1693 decode_primary_message(&frame).unwrap(),
1694 PrimaryMessage::StreamStart { .. }
1695 ));
1696
1697 let mut acked_seqs = Vec::new();
1699 for _ in 0..3 {
1700 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1701 let slots: Vec<InputSlot> =
1702 try_decode_input_batch(&frame).expect("decode InputBatch");
1703 let end_seq = slots
1704 .last()
1705 .map(|s| s.sequence)
1706 .expect("InputBatch carried at least one slot");
1707 acked_seqs.push(end_seq);
1708
1709 encode_ack(
1710 &Ack {
1711 acked_sequence: end_seq,
1712 in_memory_sequence: end_seq,
1713 },
1714 &mut buf,
1715 );
1716 writer.write_all(&buf).unwrap();
1717 writer.flush().unwrap();
1718 buf.clear();
1719 }
1720
1721 acked_seqs
1722 });
1723
1724 let mut p_reader = primary_stream.try_clone().unwrap();
1726 let mut p_writer = primary_stream;
1727 let mut buf = Vec::new();
1728
1729 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1731 assert!(matches!(
1732 decode_replica_message(&frame).unwrap(),
1733 ReplicaMessage::Handshake(_)
1734 ));
1735
1736 encode_stream_start(0, 1, [0u8; 32], 0, &mut buf);
1738 p_writer.write_all(&buf).unwrap();
1739 p_writer.flush().unwrap();
1740 buf.clear();
1741
1742 for seq in [10u64, 20, 30] {
1744 encode_input_batch_with_seq(seq, &mut buf);
1745 p_writer.write_all(&buf).unwrap();
1746 p_writer.flush().unwrap();
1747 buf.clear();
1748 }
1749
1750 for expected_seq in [10u64, 20, 30] {
1752 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1753 let ack = match decode_replica_message(&frame).unwrap() {
1754 ReplicaMessage::Ack(a) => a,
1755 other => panic!("expected Ack, got {other:?}"),
1756 };
1757 assert_eq!(ack.acked_sequence, expected_seq);
1758 }
1759
1760 let acked = replica_handle.join().unwrap();
1761 assert_eq!(acked, vec![10, 20, 30]);
1762 }
1763
1764 #[test]
1765 fn heartbeat_encode_contains_sequence() {
1766 let mut buf = Vec::new();
1769 encode_heartbeat(999, &mut buf);
1770
1771 let payload = &buf[4..];
1772 match decode_primary_message(payload).unwrap() {
1773 PrimaryMessage::Heartbeat { sequence } => {
1774 assert_eq!(sequence, 999);
1775 }
1776 other => panic!("expected Heartbeat, got {other:?}"),
1777 }
1778 }
1779
1780 #[test]
1781 fn replica_mid_stream_handshake_with_nonzero_sequence() {
1782 use std::os::unix::net::UnixStream;
1787
1788 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1789
1790 let replica_handle = std::thread::spawn(move || {
1791 let mut reader = replica_stream.try_clone().unwrap();
1792 let mut writer = replica_stream;
1793 let mut buf = Vec::new();
1794
1795 encode_handshake(
1797 &Handshake {
1798 last_sequence: 100,
1799 chain_hash: [0xBB; 32],
1800 epoch: 0,
1801 },
1802 &mut buf,
1803 );
1804 writer.write_all(&buf).unwrap();
1805 writer.flush().unwrap();
1806 buf.clear();
1807
1808 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1810 match decode_primary_message(&frame).unwrap() {
1811 PrimaryMessage::StreamStart { start_sequence, .. } => {
1812 assert_eq!(
1813 start_sequence, 100,
1814 "StreamStart should echo replica's last_sequence"
1815 );
1816 }
1817 other => panic!("expected StreamStart, got {other:?}"),
1818 }
1819
1820 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
1822 let slots: Vec<InputSlot> = try_decode_input_batch(&frame).expect("decode InputBatch");
1823 let end_sequence = slots
1824 .last()
1825 .map(|s| s.sequence)
1826 .expect("InputBatch carried at least one slot");
1827 assert!(
1828 end_sequence > 100,
1829 "InputBatch should be after replica's last_sequence"
1830 );
1831 });
1832
1833 let mut p_reader = primary_stream.try_clone().unwrap();
1835 let mut p_writer = primary_stream;
1836 let mut buf = Vec::new();
1837
1838 let frame = read_frame(&mut p_reader, MAX_CONTROL_FRAME).unwrap();
1840 let handshake = match decode_replica_message(&frame).unwrap() {
1841 ReplicaMessage::Handshake(h) => h,
1842 _ => panic!("expected Handshake"),
1843 };
1844 assert_eq!(handshake.last_sequence, 100);
1845 assert_eq!(handshake.chain_hash, [0xBB; 32]);
1846
1847 encode_stream_start(handshake.last_sequence, 1, [0u8; 32], 0, &mut buf);
1849 p_writer.write_all(&buf).unwrap();
1850 p_writer.flush().unwrap();
1851 buf.clear();
1852
1853 encode_input_batch_with_seq(150, &mut buf);
1855 p_writer.write_all(&buf).unwrap();
1856 p_writer.flush().unwrap();
1857
1858 replica_handle.join().unwrap();
1859 }
1860
1861 #[test]
1876 fn receiver_refuses_stream_start_from_stale_primary() {
1877 use std::os::unix::net::UnixStream;
1878
1879 use melin_transport_core::fence::FenceState;
1880
1881 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
1882
1883 const STALE_EPOCH: u64 = 3;
1887 let primary_handle = std::thread::spawn(move || {
1888 let mut reader = primary_stream.try_clone().unwrap();
1889 let mut writer = primary_stream;
1890
1891 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
1892 let handshake = match decode_replica_message(&frame).unwrap() {
1893 ReplicaMessage::Handshake(h) => h,
1894 _ => panic!("expected Handshake"),
1895 };
1896 assert_eq!(handshake.epoch, 5, "replica advertises its real epoch");
1899
1900 let mut buf = Vec::new();
1901 encode_stream_start(handshake.last_sequence, 1, [0u8; 32], STALE_EPOCH, &mut buf);
1902 writer.write_all(&buf).unwrap();
1903 writer.flush().unwrap();
1904 });
1905
1906 let fence_state = Arc::new(FenceState::new(5));
1909 let mut r_reader = replica_stream.try_clone().unwrap();
1910 let mut r_writer = replica_stream;
1911
1912 let mut buf = Vec::new();
1913 encode_handshake(
1914 &Handshake {
1915 last_sequence: 0,
1916 chain_hash: [0u8; 32],
1917 epoch: fence_state.epoch(),
1918 },
1919 &mut buf,
1920 );
1921 r_writer.write_all(&buf).unwrap();
1922 r_writer.flush().unwrap();
1923
1924 let frame = read_frame(&mut r_reader, MAX_CONTROL_FRAME).unwrap();
1925 let stream_epoch = match decode_primary_message(&frame).unwrap() {
1926 PrimaryMessage::StreamStart { epoch, .. } => epoch,
1927 other => panic!("expected StreamStart, got {other:?}"),
1928 };
1929 assert_eq!(stream_epoch, STALE_EPOCH, "epoch must survive the wire");
1930
1931 assert!(
1934 fence_state.refuses_primary(stream_epoch),
1935 "replica at epoch 5 must refuse a StreamStart from epoch {STALE_EPOCH}"
1936 );
1937 assert_eq!(fence_state.epoch(), 5, "refusal must not lower our epoch");
1938
1939 assert!(!fence_state.refuses_primary(5), "same tenure is followed");
1942 assert!(!fence_state.refuses_primary(6), "newer primary is followed");
1943
1944 primary_handle.join().unwrap();
1945 }
1946
1947 #[test]
1948 fn snapshot_begin_encode_decode_round_trip() {
1949 let mut buf = Vec::new();
1950 encode_snapshot_begin(1_000_000, 42, &[0xAB; 32], &mut buf);
1951
1952 let payload = &buf[4..];
1953 let msg = decode_primary_message(payload).unwrap();
1954 match msg {
1955 PrimaryMessage::SnapshotBegin {
1956 snapshot_len,
1957 snap_sequence,
1958 snap_chain_hash,
1959 } => {
1960 assert_eq!(snapshot_len, 1_000_000);
1961 assert_eq!(snap_sequence, 42);
1962 assert_eq!(snap_chain_hash, [0xAB; 32]);
1963 }
1964 _ => panic!("expected SnapshotBegin"),
1965 }
1966 }
1967
1968 #[test]
1969 fn snapshot_chunk_encode_decode_round_trip() {
1970 let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
1971 let mut buf = Vec::new();
1972 encode_snapshot_chunk(&data, &mut buf);
1973
1974 let payload = &buf[4..];
1975 let msg = decode_primary_message(payload).unwrap();
1976 match msg {
1977 PrimaryMessage::SnapshotChunk(chunk) => {
1978 assert_eq!(chunk, data);
1979 }
1980 _ => panic!("expected SnapshotChunk"),
1981 }
1982 }
1983
1984 #[test]
1985 fn snapshot_end_encode_decode_round_trip() {
1986 let mut buf = Vec::new();
1987 encode_snapshot_end(0xDEADBEEF, &mut buf);
1988
1989 let payload = &buf[4..];
1990 let msg = decode_primary_message(payload).unwrap();
1991 match msg {
1992 PrimaryMessage::SnapshotEnd { crc32c } => {
1993 assert_eq!(crc32c, 0xDEADBEEF);
1994 }
1995 _ => panic!("expected SnapshotEnd"),
1996 }
1997 }
1998
1999 #[test]
2003 fn snapshot_receiver_detects_length_mismatch() {
2004 use std::os::unix::net::UnixStream;
2005
2006 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2007
2008 let receiver = std::thread::spawn(move || -> String {
2010 let mut reader = replica_stream.try_clone().unwrap();
2011
2012 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2014 assert!(matches!(
2015 decode_primary_message(&frame).unwrap(),
2016 PrimaryMessage::NeedSnapshot,
2017 ));
2018
2019 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2021 let (snap_len, _snap_sequence, _snap_chain_hash) =
2022 match decode_primary_message(&frame).unwrap() {
2023 PrimaryMessage::SnapshotBegin {
2024 snapshot_len,
2025 snap_sequence,
2026 snap_chain_hash,
2027 } => (snapshot_len, snap_sequence, snap_chain_hash),
2028 other => panic!("expected SnapshotBegin, got {other:?}"),
2029 };
2030
2031 let mut received: u64 = 0;
2033 loop {
2034 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
2035 match decode_primary_message(&frame).unwrap() {
2036 PrimaryMessage::SnapshotChunk(data) => {
2037 received += data.len() as u64;
2038 }
2039 PrimaryMessage::SnapshotEnd { .. } => {
2040 if received != snap_len {
2041 return format!(
2042 "snapshot length mismatch: expected {snap_len} bytes, got {received}"
2043 );
2044 }
2045 return String::new(); }
2047 other => panic!("unexpected message: {other:?}"),
2048 }
2049 }
2050 });
2051
2052 let mut writer = primary_stream;
2054 let mut buf = Vec::new();
2055
2056 let actual_data = vec![0xAA; 100];
2057 let wrong_len = 999u64; encode_need_snapshot(&mut buf);
2060 std::io::Write::write_all(&mut writer, &buf).unwrap();
2061 buf.clear();
2062
2063 encode_snapshot_begin(wrong_len, 42, &[0xBB; 32], &mut buf);
2064 std::io::Write::write_all(&mut writer, &buf).unwrap();
2065 buf.clear();
2066
2067 encode_snapshot_chunk(&actual_data, &mut buf);
2068 std::io::Write::write_all(&mut writer, &buf).unwrap();
2069 buf.clear();
2070
2071 let crc = crc32c::crc32c(&actual_data);
2072 encode_snapshot_end(crc, &mut buf);
2073 std::io::Write::write_all(&mut writer, &buf).unwrap();
2074 std::io::Write::flush(&mut writer).unwrap();
2075
2076 let error_msg = receiver.join().unwrap();
2077 assert!(
2078 error_msg.contains("length mismatch"),
2079 "expected length mismatch error, got: {error_msg:?}"
2080 );
2081 }
2082
2083 #[test]
2087 fn snapshot_receiver_detects_crc_mismatch() {
2088 use std::os::unix::net::UnixStream;
2089
2090 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2091
2092 let receiver = std::thread::spawn(move || -> String {
2093 let mut reader = replica_stream.try_clone().unwrap();
2094
2095 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2097 assert!(matches!(
2098 decode_primary_message(&frame).unwrap(),
2099 PrimaryMessage::NeedSnapshot,
2100 ));
2101
2102 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2104 let snap_len = match decode_primary_message(&frame).unwrap() {
2105 PrimaryMessage::SnapshotBegin { snapshot_len, .. } => snapshot_len,
2106 other => panic!("expected SnapshotBegin, got {other:?}"),
2107 };
2108
2109 let mut received_data = Vec::new();
2111 let mut received: u64 = 0;
2112 loop {
2113 let frame = read_frame(&mut reader, MAX_DATA_FRAME).unwrap();
2114 match decode_primary_message(&frame).unwrap() {
2115 PrimaryMessage::SnapshotChunk(data) => {
2116 received += data.len() as u64;
2117 received_data.extend_from_slice(&data);
2118 }
2119 PrimaryMessage::SnapshotEnd {
2120 crc32c: expected_crc,
2121 } => {
2122 if received != snap_len {
2123 return format!("length mismatch: {snap_len} vs {received}");
2124 }
2125 let actual_crc = crc32c::crc32c(&received_data);
2126 if actual_crc != expected_crc {
2127 return format!(
2128 "CRC mismatch: expected {expected_crc:#x}, got {actual_crc:#x}"
2129 );
2130 }
2131 return String::new();
2132 }
2133 other => panic!("unexpected message: {other:?}"),
2134 }
2135 }
2136 });
2137
2138 let mut writer = primary_stream;
2140 let mut buf = Vec::new();
2141
2142 let data = vec![0xAA; 100];
2143
2144 encode_need_snapshot(&mut buf);
2145 std::io::Write::write_all(&mut writer, &buf).unwrap();
2146 buf.clear();
2147
2148 encode_snapshot_begin(data.len() as u64, 42, &[0xBB; 32], &mut buf);
2149 std::io::Write::write_all(&mut writer, &buf).unwrap();
2150 buf.clear();
2151
2152 encode_snapshot_chunk(&data, &mut buf);
2153 std::io::Write::write_all(&mut writer, &buf).unwrap();
2154 buf.clear();
2155
2156 let wrong_crc = !crc32c::crc32c(&data);
2158 encode_snapshot_end(wrong_crc, &mut buf);
2159 std::io::Write::write_all(&mut writer, &buf).unwrap();
2160 std::io::Write::flush(&mut writer).unwrap();
2161
2162 let error_msg = receiver.join().unwrap();
2163 assert!(
2164 error_msg.contains("CRC mismatch"),
2165 "expected CRC mismatch error, got: {error_msg:?}"
2166 );
2167 }
2168
2169 #[test]
2172 fn snapshot_receiver_detects_chain_hash_mismatch() {
2173 use std::os::unix::net::UnixStream;
2174
2175 let (primary_stream, replica_stream) = UnixStream::pair().unwrap();
2176
2177 let receiver = std::thread::spawn(move || -> String {
2178 let mut reader = replica_stream.try_clone().unwrap();
2179
2180 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2181 assert!(matches!(
2182 decode_primary_message(&frame).unwrap(),
2183 PrimaryMessage::NeedSnapshot,
2184 ));
2185
2186 let frame = read_frame(&mut reader, MAX_CONTROL_FRAME).unwrap();
2187 let (snap_len, _snap_sequence, snap_chain_hash) =
2188 match decode_primary_message(&frame).unwrap() {
2189 PrimaryMessage::SnapshotBegin {
2190 snapshot_len,
2191 snap_sequence,
2192 snap_chain_hash,
2193 } => (snapshot_len, snap_sequence, snap_chain_hash),
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 assert_eq!(received, snap_len, "length should match");
2211 let actual_crc = crc32c::crc32c(&received_data);
2212 assert_eq!(actual_crc, expected_crc, "CRC should match");
2213 break;
2214 }
2215 other => panic!("unexpected message: {other:?}"),
2216 }
2217 }
2218
2219 let loaded_hash = [0xFF; 32]; if loaded_hash != snap_chain_hash {
2223 return format!(
2224 "snapshot chain hash mismatch: primary sent {snap_chain_hash:02x?}, \
2225 loaded snapshot has {loaded_hash:02x?}"
2226 );
2227 }
2228 String::new()
2229 });
2230
2231 let mut writer = primary_stream;
2234 let mut buf = Vec::new();
2235
2236 let data = vec![0xAA; 64];
2237 let advertised_hash = [0xBB; 32];
2239
2240 encode_need_snapshot(&mut buf);
2241 std::io::Write::write_all(&mut writer, &buf).unwrap();
2242 buf.clear();
2243
2244 encode_snapshot_begin(data.len() as u64, 10, &advertised_hash, &mut buf);
2245 std::io::Write::write_all(&mut writer, &buf).unwrap();
2246 buf.clear();
2247
2248 encode_snapshot_chunk(&data, &mut buf);
2249 std::io::Write::write_all(&mut writer, &buf).unwrap();
2250 buf.clear();
2251
2252 let crc = crc32c::crc32c(&data);
2253 encode_snapshot_end(crc, &mut buf);
2254 std::io::Write::write_all(&mut writer, &buf).unwrap();
2255 std::io::Write::flush(&mut writer).unwrap();
2256
2257 let error_msg = receiver.join().unwrap();
2258 assert!(
2259 error_msg.contains("chain hash mismatch"),
2260 "expected chain hash mismatch error, got: {error_msg:?}"
2261 );
2262 }
2263
2264 #[test]
2267 fn primary_rejects_snapshot_with_invalid_magic() {
2268 let tmp = tempfile::tempdir().unwrap();
2269 let snap_path = tmp.path().join("test.snapshot");
2270
2271 let mut bad_snap = vec![0u8; 64];
2273 bad_snap[0..4].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes()); std::fs::write(&snap_path, &bad_snap).unwrap();
2276
2277 let snap_data = std::fs::read(&snap_path).unwrap();
2279 assert!(
2280 snap_data.len() >= 48,
2281 "file should be big enough for header"
2282 );
2283
2284 let magic = u32::from_le_bytes(snap_data[0..4].try_into().unwrap());
2285 assert_ne!(magic, 0x534E_4150);
2286 assert_eq!(magic, 0xDEAD_BEEF);
2287 }
2288
2289 #[test]
2292 fn primary_rejects_snapshot_too_small_for_header() {
2293 let tmp = tempfile::tempdir().unwrap();
2294 let snap_path = tmp.path().join("test.snapshot");
2295
2296 std::fs::write(&snap_path, [0u8; 20]).unwrap();
2298
2299 let snap_data = std::fs::read(&snap_path).unwrap();
2300 assert!(
2301 snap_data.len() < 48,
2302 "file must be too small for header validation"
2303 );
2304 }
2305
2306 #[test]
2307 fn decode_snapshot_begin_too_short() {
2308 let payload = [MSG_SNAPSHOT_BEGIN, 0x01, 0x02, 0x03];
2311 let err = decode_primary_message(&payload).unwrap_err();
2312 assert!(
2313 err.to_string().contains("SnapshotBegin too short"),
2314 "expected 'SnapshotBegin too short', got: {err}"
2315 );
2316 }
2317
2318 #[test]
2319 fn decode_snapshot_end_too_short() {
2320 let payload = [MSG_SNAPSHOT_END];
2322 let err = decode_primary_message(&payload).unwrap_err();
2323 assert!(
2324 err.to_string().contains("SnapshotEnd too short"),
2325 "expected 'SnapshotEnd too short', got: {err}"
2326 );
2327 }
2328
2329 #[test]
2330 fn decode_snapshot_chunk_empty_data() {
2331 let payload = [MSG_SNAPSHOT_CHUNK];
2333 let msg = decode_primary_message(&payload).unwrap();
2334 match msg {
2335 PrimaryMessage::SnapshotChunk(data) => {
2336 assert!(data.is_empty());
2337 }
2338 _ => panic!("expected SnapshotChunk"),
2339 }
2340 }
2341
2342 fn make_journal_cursor(val: u64) -> melin_pipeline::padding::Sequence {
2345 melin_pipeline::padding::Sequence::new(AtomicU64::new(val))
2346 }
2347
2348 #[test]
2349 fn pending_ack_queue_push_and_pop_ready() {
2350 let mut q = PendingAckQueue::new(8);
2351 assert!(q.is_empty());
2352 assert!(!q.is_full());
2353
2354 q.push(10, 100);
2355 q.push(20, 200);
2356 assert!(!q.is_empty());
2357
2358 let cursor = make_journal_cursor(5);
2360 assert!(q.pop_ready(&cursor).is_none());
2361
2362 cursor.get().store(15, Ordering::Relaxed);
2364 assert_eq!(q.pop_ready(&cursor), Some(100));
2365 assert!(!q.is_empty());
2367
2368 cursor.get().store(25, Ordering::Relaxed);
2370 assert_eq!(q.pop_ready(&cursor), Some(200));
2371 assert!(q.is_empty());
2372 }
2373
2374 #[test]
2375 fn pending_ack_queue_pop_ready_returns_highest_sequence() {
2376 let mut q = PendingAckQueue::new(8);
2380 q.push(10, 100);
2381 q.push(20, 200);
2382 q.push(30, 300);
2383
2384 let cursor = make_journal_cursor(30);
2385 assert_eq!(q.pop_ready(&cursor), Some(300));
2386 assert!(q.is_empty());
2387 }
2388
2389 #[test]
2390 fn pending_ack_queue_capacity_and_full() {
2391 let mut q = PendingAckQueue::new(8);
2392 for i in 0..8 {
2393 assert!(!q.is_full());
2394 q.push(i as u64 + 1, (i + 1) as u64 * 100);
2395 }
2396 assert!(q.is_full());
2397 }
2398
2399 #[test]
2400 fn pending_ack_queue_pop_oldest_blocking() {
2401 let mut q = PendingAckQueue::new(8);
2402 q.push(10, 100);
2403 q.push(20, 200);
2404
2405 let cursor = make_journal_cursor(25);
2408 let seq = q.pop_oldest_blocking(&cursor, true, &AtomicBool::new(false));
2409 assert_eq!(seq, Some(200));
2411 assert!(q.is_empty());
2412 }
2413
2414 #[test]
2417 fn pending_ack_queue_blocking_wait_aborts_on_journal_failure() {
2418 let mut q = PendingAckQueue::new(8);
2419 q.push(10, 100);
2420
2421 let cursor = make_journal_cursor(5);
2423 let abort = AtomicBool::new(true);
2424 assert_eq!(q.pop_oldest_blocking(&cursor, true, &abort), None);
2425 assert!(!q.is_empty(), "aborted wait must not pop the entry");
2426 assert_eq!(q.pop_all_blocking(&cursor, true, &abort), None);
2427 }
2428
2429 #[test]
2438 fn shutdown_pipeline_surfaces_journal_error_kind() {
2439 let flag = AtomicBool::new(false);
2440 let matching_joined = Arc::new(AtomicBool::new(false));
2441 let mj = Arc::clone(&matching_joined);
2442
2443 let journal = std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> {
2444 Err(melin_journal::JournalError::ReplicaChainDivergence {
2445 sequence: 42,
2446 expected: [1u8; 32],
2447 actual: [2u8; 32],
2448 })
2449 });
2450 let matching = std::thread::spawn(move || {
2451 mj.store(true, Ordering::Release);
2452 7u64
2453 });
2454 let drain = std::thread::spawn(|| {});
2455
2456 let outcome = shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None);
2457 assert!(flag.load(Ordering::Acquire), "shutdown flag must be set");
2458 assert!(
2459 matching_joined.load(Ordering::Acquire),
2460 "matching thread must be joined even when the journal failed"
2461 );
2462 match outcome {
2463 TeardownOutcome::JournalFailed(
2464 melin_journal::JournalError::ReplicaChainDivergence { sequence, .. },
2465 ) => assert_eq!(sequence, 42),
2466 _ => panic!("expected JournalFailed(ReplicaChainDivergence)"),
2467 }
2468 }
2469
2470 #[test]
2471 fn shutdown_pipeline_clean_returns_both_states() {
2472 let flag = AtomicBool::new(false);
2473 let journal =
2474 std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> { Ok(11u32) });
2475 let matching = std::thread::spawn(|| 7u64);
2476 let drain = std::thread::spawn(|| {});
2477
2478 match shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None) {
2479 TeardownOutcome::Clean(app, writer) => {
2480 assert_eq!(app, 7);
2481 assert_eq!(writer, 11);
2482 }
2483 _ => panic!("expected Clean"),
2484 }
2485 }
2486
2487 #[test]
2488 fn shutdown_pipeline_panicked_stage_reports_panicked() {
2489 let flag = AtomicBool::new(false);
2490 let journal =
2491 std::thread::spawn(|| -> Result<u32, melin_journal::JournalError> { Ok(11u32) });
2492 let matching = std::thread::spawn(|| -> u64 { panic!("matching stage died") });
2493 let drain = std::thread::spawn(|| {});
2494
2495 assert!(matches!(
2496 shutdown_pipeline::<u64, u32>(&flag, journal, matching, drain, None),
2497 TeardownOutcome::Panicked
2498 ));
2499 }
2500
2501 #[test]
2502 fn pending_ack_queue_wraps_around() {
2503 let mut q = PendingAckQueue::new(8);
2504 let cursor = make_journal_cursor(100);
2505
2506 for round in 0..3 {
2508 for i in 0..8 {
2509 let target = (round * 8 + i) as u64 + 1;
2510 q.push(target, target * 10);
2511 }
2512 assert!(q.is_full());
2513 let seq = q.pop_ready(&cursor).expect("should be ready");
2514 assert_eq!(seq, (round * 8 + 8) as u64 * 10);
2515 assert!(q.is_empty());
2516 }
2517 }
2518
2519 #[test]
2520 fn pending_ack_queue_pop_all_blocking_empty() {
2521 let mut q = PendingAckQueue::new(8);
2522 let cursor = make_journal_cursor(0);
2523 assert!(
2524 q.pop_all_blocking(&cursor, true, &AtomicBool::new(false))
2525 .is_none()
2526 );
2527 }
2528
2529 #[test]
2532 fn dual_track_returns_none_when_both_tracks_idle() {
2533 let mut q = PendingAckQueue::new(8);
2534 let cursor = make_journal_cursor(0);
2535 assert!(
2536 try_flush_dual_track(&mut q, &cursor, 0, 0, 0).is_none(),
2537 "no advance on either track → no ack"
2538 );
2539 }
2540
2541 #[test]
2542 fn dual_track_fires_on_persisted_advance_only() {
2543 let mut q = PendingAckQueue::new(8);
2548 q.push(10, 100);
2549 let cursor = make_journal_cursor(20);
2550 let ack = try_flush_dual_track(&mut q, &cursor, 50, 0, 50).expect("persisted advanced");
2551 assert_eq!(ack.acked_sequence, 100);
2552 assert_eq!(ack.in_memory_sequence, 50);
2553 }
2554
2555 #[test]
2556 fn dual_track_fires_on_in_memory_advance_only() {
2557 let mut q = PendingAckQueue::new(8);
2560 let cursor = make_journal_cursor(0);
2561 let ack = try_flush_dual_track(&mut q, &cursor, 200, 100, 100).expect("in-memory advanced");
2562 assert_eq!(ack.acked_sequence, 100);
2563 assert_eq!(ack.in_memory_sequence, 200);
2564 }
2565
2566 #[test]
2567 fn dual_track_coalesces_until_caller_updates_trackers() {
2568 let mut q = PendingAckQueue::new(8);
2574 q.push(10, 100);
2575 let cursor = make_journal_cursor(20);
2576 let ack1 = try_flush_dual_track(&mut q, &cursor, 50, 0, 50).expect("call 1 fires");
2577 assert_eq!(ack1.acked_sequence, 100);
2578 let ack2 = try_flush_dual_track(&mut q, &cursor, 80, 0, 50)
2580 .expect("call 2 fires on in-memory advance");
2581 assert_eq!(
2582 ack2.acked_sequence, 0,
2583 "no new persisted pop → unwrap_or(last_sent)"
2584 );
2585 assert_eq!(ack2.in_memory_sequence, 80);
2586 }
2587
2588 #[test]
2589 fn dual_track_no_duplicate_ack_after_backpressure_drain() {
2590 let mut q = PendingAckQueue::new(4);
2608 for i in 1..=4u64 {
2611 q.push(i * 10, i * 100);
2612 }
2613 assert!(q.is_full());
2614
2615 let cursor = make_journal_cursor(40);
2618 let drained = q.pop_ready(&cursor).expect("all entries durable");
2619 assert_eq!(drained, 400);
2620 assert!(q.is_empty());
2621
2622 let mut last_sent_acked = drained;
2625 let mut last_sent_in_mem = 450u64;
2626
2627 assert!(
2632 try_flush_dual_track(
2633 &mut q,
2634 &cursor,
2635 last_sent_in_mem,
2636 last_sent_acked,
2637 last_sent_in_mem,
2638 )
2639 .is_none(),
2640 "no advance on either track after backpressure drain → no duplicate ack",
2641 );
2642
2643 q.push(50, 500);
2646 let cursor2 = make_journal_cursor(50);
2647 let ack = try_flush_dual_track(&mut q, &cursor2, 500, last_sent_acked, last_sent_in_mem)
2648 .expect("fresh batch after drain fires a new ack");
2649 assert!(
2650 ack.acked_sequence >= last_sent_acked,
2651 "regression: drain sent {last_sent_acked} but next ack carries {}",
2652 ack.acked_sequence,
2653 );
2654 assert_eq!(ack.acked_sequence, 500);
2655 assert_eq!(ack.in_memory_sequence, 500);
2656
2657 last_sent_acked = ack.acked_sequence;
2658 last_sent_in_mem = ack.in_memory_sequence;
2659
2660 assert!(
2664 try_flush_dual_track(
2665 &mut q,
2666 &cursor2,
2667 last_sent_in_mem,
2668 last_sent_acked,
2669 last_sent_in_mem,
2670 )
2671 .is_none(),
2672 "post-resume idle → no further ack",
2673 );
2674 }
2675
2676 #[test]
2679 fn disconnect_resets_cursor_to_max() {
2680 let cursor = Arc::new(AtomicU64::new(42));
2683 let replicas_connected = Arc::new(AtomicU32::new(1));
2684
2685 replicas_connected.fetch_sub(1, Ordering::Release);
2687
2688 if replicas_connected.load(Ordering::Relaxed) == 0 {
2690 cursor.store(PipelineCursors::NO_REPLICA, Ordering::Release);
2691 }
2692
2693 assert_eq!(cursor.load(Ordering::Relaxed), PipelineCursors::NO_REPLICA);
2694 }
2695
2696 #[test]
2697 fn cursor_not_reset_when_replica_still_connected() {
2698 let cursor = Arc::new(AtomicU64::new(42));
2699 let replicas_connected = Arc::new(AtomicU32::new(2));
2700
2701 replicas_connected.fetch_sub(1, Ordering::Release);
2703
2704 if replicas_connected.load(Ordering::Relaxed) == 0 {
2705 cursor.store(PipelineCursors::NO_REPLICA, Ordering::Release);
2706 }
2707
2708 assert_eq!(cursor.load(Ordering::Relaxed), 42);
2710 }
2711}