1use std::io::{self, Write};
10use std::net::{SocketAddr, TcpStream};
11use std::os::unix::io::RawFd;
12use std::sync::Arc;
13use std::sync::atomic::{AtomicBool, Ordering};
14
15use tracing::{info, warn};
16
17use melin_app::Application;
18use melin_journal::BufferedWriter;
19
20use super::auth::authenticate_with_primary;
21use super::receiver_transport::{
22 ControlFrameSource, ReceiverTransport, SessionExit, streaming_loop,
23};
24use super::{
25 ReplicaPipelineHandles, ResumePoint, ResyncDecision, StreamStartVerdict,
26 build_replica_pipeline_with_threads, handle_resync_verdict, handle_session_exit,
27 journal_failed_while_disconnected, recover_replica_state, sleep_then_double_backoff,
28 take_pipeline_for_promotion, teardown_replica_pipeline,
29};
30use crate::uring_teardown::{DrainBackoff, wake_pending_ops};
31use melin_transport_core::replication::protocol::{
32 Ack, Handshake, MAX_CONTROL_FRAME, MAX_DATA_FRAME, PrimaryMessage, decode_primary_message,
33 encode_ack, encode_handshake, read_frame,
34};
35
36#[inline]
46fn arm_tcp_quickack(fd: RawFd) {
47 let on: libc::c_int = 1;
48 let rc = unsafe {
52 libc::setsockopt(
53 fd,
54 libc::IPPROTO_TCP,
55 libc::TCP_QUICKACK,
56 &on as *const libc::c_int as *const libc::c_void,
57 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
58 )
59 };
60 if rc != 0 {
61 debug_assert!(
67 std::io::Error::last_os_error().raw_os_error() != Some(libc::EFAULT),
68 "TCP_QUICKACK setsockopt got EFAULT — bad option pointer"
69 );
70 }
71}
72
73const TOKEN_RECV: u64 = 0;
78const TOKEN_SEND: u64 = 1;
79const TOKEN_PROVIDE: u64 = 2;
80
81const NUM_RECV_BUFFERS: u16 = 16;
83const RECV_BUF_SIZE: usize = MAX_DATA_FRAME;
84const RECV_BUF_GROUP_ID: u16 = 0;
85
86const IORING_CQE_F_BUFFER: u32 = 1 << 0;
88const IORING_CQE_F_MORE: u32 = 1 << 1;
89const IORING_CQE_BUFFER_SHIFT: u32 = 16;
90
91struct UringTransport {
97 ring: io_uring::IoUring,
98 tcp_fd: RawFd,
103 recv_pool: Vec<u8>,
109 pool_ptr: *mut u8,
110 multishot_active: bool,
111 connected: bool,
112 ack_buf: Vec<u8>,
113 ack_offset: usize,
114 ack_in_flight: bool,
115 pending_ack: Option<Ack>,
125 acked_since_recv: bool,
130}
131
132impl UringTransport {
133 fn new(tcp_stream: &TcpStream) -> io::Result<Self> {
134 use io_uring::{IoUring, opcode, types};
135 use std::os::unix::io::AsRawFd;
136
137 let tcp_fd = tcp_stream.as_raw_fd();
138
139 let mut ring: IoUring = IoUring::builder()
140 .setup_single_issuer()
141 .build(64)
142 .map_err(|e| io::Error::other(format!("io_uring init failed: {e}")))?;
143
144 ring.submitter()
145 .register_files(&[tcp_fd])
146 .map_err(|e| io::Error::other(format!("io_uring register_files failed: {e}")))?;
147
148 arm_tcp_quickack(tcp_fd);
149
150 {
152 let mut cpuset: libc::cpu_set_t = unsafe { std::mem::zeroed() };
153 unsafe { libc::CPU_SET(0, &mut cpuset) };
154 let _ = ring.submitter().register_iowq_aff(&cpuset);
155 }
156
157 let mut recv_pool: Vec<u8> = vec![0u8; NUM_RECV_BUFFERS as usize * RECV_BUF_SIZE];
158 let pool_ptr = recv_pool.as_mut_ptr();
159
160 {
162 let sqe = opcode::ProvideBuffers::new(
163 pool_ptr,
164 RECV_BUF_SIZE as i32,
165 NUM_RECV_BUFFERS,
166 RECV_BUF_GROUP_ID,
167 0,
168 )
169 .build()
170 .user_data(TOKEN_PROVIDE);
171 unsafe { ring.submission().push(&sqe).expect("SQ full") };
172 ring.submit_and_wait(1)?;
173 let cqe = ring
174 .completion()
175 .next()
176 .ok_or_else(|| io::Error::other("no cqe after ProvideBuffers"))?;
177 if cqe.result() < 0 {
178 return Err(io::Error::other(format!(
179 "ProvideBuffers failed: {}",
180 cqe.result()
181 )));
182 }
183 }
184
185 let sqe = opcode::RecvMulti::new(types::Fd(tcp_fd), RECV_BUF_GROUP_ID)
187 .build()
188 .user_data(TOKEN_RECV);
189 unsafe { ring.submission().push(&sqe).expect("SQ full") };
190
191 Ok(UringTransport {
192 ring,
193 tcp_fd,
194 recv_pool,
195 pool_ptr,
196 multishot_active: true,
197 connected: true,
198 ack_buf: Vec::with_capacity(64),
199 ack_offset: 0,
200 ack_in_flight: false,
201 pending_ack: None,
202 acked_since_recv: false,
203 })
204 }
205
206 fn push_ack_send(&mut self, ack: &Ack) {
212 use io_uring::{opcode, types};
213
214 debug_assert!(!self.ack_in_flight);
215 self.ack_buf.clear();
216 encode_ack(ack, &mut self.ack_buf);
217 let sqe = opcode::Send::new(
218 types::Fixed(0),
219 self.ack_buf.as_ptr(),
220 self.ack_buf.len() as u32,
221 )
222 .build()
223 .user_data(TOKEN_SEND);
224 unsafe { self.ring.submission().push(&sqe).expect("SQ full") };
228 self.ack_in_flight = true;
229 self.ack_offset = 0;
230 self.acked_since_recv = true;
231 }
232
233 fn settle_unprocessed(&mut self, rest: &[(u64, i32, u32)]) {
243 for &(token, _result, flags) in rest {
244 match token {
245 TOKEN_RECV if (flags & IORING_CQE_F_MORE) == 0 => self.multishot_active = false,
246 TOKEN_SEND => self.ack_in_flight = false,
247 _ => {}
248 }
249 }
250 }
251
252 fn drain(&mut self) -> bool {
265 loop {
271 match self.ring.submit() {
272 Ok(_) => break,
273 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
274 Err(_) => return false,
275 }
276 }
277 let mut backoff = DrainBackoff::new();
278 while self.multishot_active || self.ack_in_flight {
279 for cqe in self.ring.completion() {
280 match cqe.user_data() {
281 TOKEN_RECV if (cqe.flags() & IORING_CQE_F_MORE) == 0 => {
287 self.multishot_active = false;
288 }
289 TOKEN_SEND => self.ack_in_flight = false,
293 _ => {}
297 }
298 }
299 if (self.multishot_active || self.ack_in_flight) && !backoff.wait() {
300 return false;
301 }
302 }
303 true
304 }
305}
306
307impl Drop for UringTransport {
308 fn drop(&mut self) {
309 wake_pending_ops(self.tcp_fd);
310 if !self.drain() {
311 std::mem::forget(std::mem::take(&mut self.recv_pool));
317 std::mem::forget(std::mem::take(&mut self.ack_buf));
318 tracing::warn!(
319 multishot_active = self.multishot_active,
320 ack_in_flight = self.ack_in_flight,
321 "io_uring teardown drain did not complete; leaking receiver buffers"
322 );
323 }
324 }
325}
326
327impl ReceiverTransport for UringTransport {
328 fn poll_recv(&mut self, recv_buf: &mut Vec<u8>) -> io::Result<bool> {
329 use io_uring::{opcode, types};
330
331 if !self.connected {
332 return Err(io::Error::other("not connected"));
333 }
334
335 let pending = self.ring.submission().len();
338 if pending > 0 {
339 self.ring.submit()?;
340 }
341
342 let mut cqes: [(u64, i32, u32); 16] = [(0, 0, 0); 16];
344 let mut cqe_count = 0;
345 {
346 let mut cq = self.ring.completion();
347 while cqe_count < cqes.len() {
348 match cq.next() {
349 Some(cqe) => {
350 cqes[cqe_count] = (cqe.user_data(), cqe.result(), cqe.flags());
351 cqe_count += 1;
352 }
353 None => break,
354 }
355 }
356 }
357
358 let mut any_recv = false;
359 let mut submit_chained_ack = false;
363
364 for (idx, &(token, result, flags)) in cqes[..cqe_count].iter().enumerate() {
365 match token {
370 TOKEN_RECV => {
371 if (flags & IORING_CQE_F_MORE) == 0 {
372 self.multishot_active = false;
373 }
374 if result < 0 {
375 if result == -libc::ENOBUFS {
376 tracing::debug!("recv multishot ENOBUFS — pool exhausted");
377 continue;
378 }
379 self.connected = false;
380 self.settle_unprocessed(&cqes[idx + 1..cqe_count]);
381 return Err(io::Error::other(format!(
382 "primary disconnected (recv returned {result})"
383 )));
384 }
385 if result == 0 {
386 self.connected = false;
387 self.settle_unprocessed(&cqes[idx + 1..cqe_count]);
388 return Err(io::Error::other("primary disconnected (recv returned 0)"));
389 }
390 if (flags & IORING_CQE_F_BUFFER) == 0 {
391 self.connected = false;
392 self.settle_unprocessed(&cqes[idx + 1..cqe_count]);
393 return Err(io::Error::other("recv cqe missing F_BUFFER flag"));
394 }
395
396 let n = result as usize;
397 let buf_id = (flags >> IORING_CQE_BUFFER_SHIFT) as usize;
398 let buf_ptr = unsafe { self.pool_ptr.add(buf_id * RECV_BUF_SIZE) };
401 let slice = unsafe { std::slice::from_raw_parts(buf_ptr, n) };
402 recv_buf.extend_from_slice(slice);
403 any_recv = true;
404
405 let provide_sqe = opcode::ProvideBuffers::new(
407 buf_ptr,
408 RECV_BUF_SIZE as i32,
409 1,
410 RECV_BUF_GROUP_ID,
411 buf_id as u16,
412 )
413 .build()
414 .user_data(TOKEN_PROVIDE);
415 unsafe { self.ring.submission().push(&provide_sqe).expect("SQ full") };
416 }
417
418 TOKEN_PROVIDE if result < 0 => {
419 tracing::debug!("ProvideBuffers re-provision failed: {result}");
420 }
421
422 TOKEN_SEND => {
423 if result < 0 {
424 self.connected = false;
425 self.ack_in_flight = false;
428 self.settle_unprocessed(&cqes[idx + 1..cqe_count]);
429 return Err(io::Error::other(format!(
430 "ack send error (returned {result})"
431 )));
432 }
433 let sent = result as usize;
434 self.ack_offset += sent;
435 if self.ack_offset >= self.ack_buf.len() {
436 self.ack_buf.clear();
437 self.ack_offset = 0;
438 self.ack_in_flight = false;
439 if let Some(ack) = self.pending_ack.take() {
446 self.push_ack_send(&ack);
447 submit_chained_ack = true;
448 }
449 } else {
450 let sqe = opcode::Send::new(
452 types::Fixed(0),
453 self.ack_buf[self.ack_offset..].as_ptr(),
454 (self.ack_buf.len() - self.ack_offset) as u32,
455 )
456 .build()
457 .user_data(TOKEN_SEND);
458 unsafe { self.ring.submission().push(&sqe).expect("SQ full") };
459 }
460 }
461
462 _ => {}
463 }
464 }
465
466 if !self.multishot_active {
468 let sqe = opcode::RecvMulti::new(types::Fd(self.tcp_fd), RECV_BUF_GROUP_ID)
469 .build()
470 .user_data(TOKEN_RECV);
471 unsafe { self.ring.submission().push(&sqe).expect("SQ full") };
472 self.multishot_active = true;
473 }
474
475 if submit_chained_ack {
478 self.ring.submit()?;
479 }
480
481 if any_recv {
482 if self.acked_since_recv {
489 self.acked_since_recv = false;
490 } else {
491 arm_tcp_quickack(self.tcp_fd);
492 }
493 }
494
495 Ok(any_recv)
496 }
497
498 fn send_ack(&mut self, ack: &Ack) -> io::Result<bool> {
499 if self.ack_in_flight {
500 self.pending_ack = Some(*ack);
504 return Ok(true);
505 }
506 self.push_ack_send(ack);
507 Ok(true)
508 }
509
510 fn ack_in_flight(&self) -> bool {
511 self.ack_in_flight || self.pending_ack.is_some()
516 }
517
518 fn is_connected(&mut self) -> bool {
519 self.connected
520 }
521}
522
523struct TcpFrameSource<'a> {
531 reader: &'a mut TcpStream,
532}
533
534impl ControlFrameSource for TcpFrameSource<'_> {
535 fn next_frame(
536 &mut self,
537 max_size: usize,
538 ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
539 Ok(read_frame(self.reader, max_size)?)
540 }
541}
542
543pub type ReceiverResult<A, W> = Result<Option<(A, W)>, Box<dyn std::error::Error>>;
545
546#[allow(clippy::too_many_arguments)]
547pub fn run_receiver<A>(
548 primary_addr: SocketAddr,
549 journal_path: &std::path::Path,
550 signing_key: &ed25519_dalek::SigningKey,
551 shutdown: &AtomicBool,
552 control: &super::ReplicaControlPlane,
556 snapshot_interval_ms: u64,
557 snapshot_path: std::path::PathBuf,
558 cores: crate::layout::PipelineCores,
559 staging_mode: melin_journal::StagingMode,
562 group_commit_delay: std::time::Duration,
563 pipeline_depth: usize,
564 fence_state: std::sync::Arc<melin_transport_core::fence::FenceState>,
565 sizing: &A::Sizing,
568) -> ReceiverResult<A, BufferedWriter<A::Event>>
569where
570 A: Application + Send + 'static,
571 A::Event: Send + Sync + 'static,
572 A::Report: Send + 'static,
573 A::QueryResponse: Send + 'static,
574{
575 let super::ReplicaControlPlane {
576 promote,
577 tip_ready,
578 journal_tip,
579 primary_link_up,
580 primary_observed: _,
582 lineage_ack_policy,
583 pipeline_healthy,
584 } = control;
585 let (mut app, mut journal_writer, mut last_sequence, mut chain_hash) =
589 recover_replica_state::<A, BufferedWriter<A::Event>>(
590 journal_path,
591 &snapshot_path,
592 &fence_state,
593 lineage_ack_policy,
594 sizing,
595 )?;
596 journal_tip.advance(melin_transport_core::WireSeq::new(last_sequence));
602 tip_ready.store(true, Ordering::Release);
603
604 let mut backoff = std::time::Duration::from_secs(1);
605
606 let mut divergence_resyncs: u32 = 0;
609
610 let mut send_buf = Vec::with_capacity(64);
611 let mut pipeline: Option<ReplicaPipelineHandles<A, BufferedWriter<A::Event>>> = None;
612
613 loop {
615 primary_link_up.store(false, Ordering::Release);
618
619 if let Some(p) = pipeline.as_ref() {
620 match p
630 .settled_resume_point(|| shutdown.load(Ordering::Relaxed) || promote.is_requested())
631 {
632 ResumePoint::Settled(fsync_state) => {
633 last_sequence = fsync_state.journal_seq.get();
634 chain_hash = fsync_state.chain_hash;
635 }
636 ResumePoint::Interrupted => {}
638 ResumePoint::JournalFailed => {
641 let after = handle_session_exit(
642 journal_failed_while_disconnected(),
643 &mut pipeline,
644 &mut divergence_resyncs,
645 &mut backoff,
646 last_sequence,
647 journal_path,
648 &snapshot_path,
649 &fence_state,
650 lineage_ack_policy,
651 shutdown,
652 promote,
653 || {},
655 sizing,
656 );
657 if let Some(r) = after.adopt(
660 &mut app,
661 &mut journal_writer,
662 &mut last_sequence,
663 &mut chain_hash,
664 ) {
665 return r;
666 }
667 continue;
668 }
669 }
670 }
671
672 if shutdown.load(Ordering::Relaxed) {
677 if let Some(p) = pipeline.take() {
678 let _ = teardown_replica_pipeline::<A, BufferedWriter<A::Event>>(p);
679 }
680 return Ok(None);
681 }
682 if promote.is_requested() {
683 info!("promotion triggered while disconnected");
684 return take_pipeline_for_promotion(&mut pipeline, &mut app, &mut journal_writer);
685 }
686
687 info!(primary = %primary_addr, "connecting to primary as replica");
689
690 let stream = match TcpStream::connect(primary_addr) {
691 Ok(s) => s,
692 Err(e) => {
693 warn!(
694 error = %e,
695 backoff_secs = backoff.as_secs(),
696 "failed to connect to primary — retrying"
697 );
698 sleep_then_double_backoff(&mut backoff, shutdown, promote);
699 continue;
700 }
701 };
702 if let Err(e) = stream.set_nodelay(true) {
703 warn!(error = %e, "failed to set TCP_NODELAY on replica receive socket");
704 }
705 {
706 use std::os::unix::io::AsRawFd;
707 let busy_poll: libc::c_int = 50;
708 let rc = unsafe {
709 libc::setsockopt(
710 stream.as_raw_fd(),
711 libc::SOL_SOCKET,
712 libc::SO_BUSY_POLL,
713 &busy_poll as *const libc::c_int as *const libc::c_void,
714 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
715 )
716 };
717 if rc != 0 {
718 let err = std::io::Error::last_os_error();
719 warn!(error = %err, "failed to set SO_BUSY_POLL on replica receive socket");
720 }
721 }
722 stream.set_read_timeout(Some(std::time::Duration::from_secs(5)))?;
723
724 let mut reader = stream.try_clone()?;
725 let mut tcp_writer = stream;
726
727 if let Err(e) = authenticate_with_primary(&mut reader, signing_key) {
730 if matches!(
737 e.kind(),
738 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
739 ) {
740 warn!(error = %e, "no auth challenge from primary (not serving yet?) — retrying");
741 } else {
742 warn!(error = %e, "authentication failed — retrying");
743 }
744 drop(reader);
749 drop(tcp_writer);
750 sleep_then_double_backoff(&mut backoff, shutdown, promote);
753 continue;
754 }
755 info!("authenticated with primary");
756 primary_link_up.store(true, Ordering::Release);
759
760 let handshake = Handshake {
765 last_sequence,
766 chain_hash,
767 epoch: fence_state.epoch(),
768 };
769 send_buf.clear();
770 encode_handshake(&handshake, &mut send_buf);
771 tcp_writer.write_all(&send_buf)?;
772 tcp_writer.flush()?;
773 send_buf.clear();
774
775 let response_frame = read_frame(&mut reader, MAX_CONTROL_FRAME)?;
782 let response = decode_primary_message(&response_frame)?;
783 let (stream_lineage, session_start) = match response {
784 PrimaryMessage::StreamStart {
785 start_sequence,
786 segment_start_sequence,
787 anchor_hash,
788 genesis_entries,
789 epoch,
790 } => match super::accept_stream_start(
791 super::StreamStart {
792 start_sequence,
793 lineage: (segment_start_sequence, anchor_hash, genesis_entries),
794 epoch,
795 },
796 "tcp",
797 &pipeline,
798 &mut journal_writer,
799 &fence_state,
800 control,
801 )? {
802 StreamStartVerdict::Follow(lineage) => (lineage, last_sequence),
803 StreamStartVerdict::StalePrimary => {
806 drop(reader);
809 drop(tcp_writer);
810 sleep_then_double_backoff(&mut backoff, shutdown, promote);
811 continue;
812 }
813 },
814 ref resync @ (PrimaryMessage::NeedSnapshot | PrimaryMessage::HashMismatch) => {
815 let divergent = matches!(resync, PrimaryMessage::HashMismatch);
816 let decision = handle_resync_verdict(
820 divergent,
821 &mut TcpFrameSource {
822 reader: &mut reader,
823 },
824 &mut pipeline,
825 &mut app,
826 &mut journal_writer,
827 journal_path,
828 &snapshot_path,
829 &fence_state,
830 control,
831 &mut last_sequence,
832 &mut chain_hash,
833 )
834 .map_err(|e| -> Box<dyn std::error::Error> { e })?;
835 match decision {
836 ResyncDecision::Ready {
837 segment_start_sequence,
838 anchor_hash,
839 genesis_entries,
840 resume_sequence,
841 } => (
842 (segment_start_sequence, anchor_hash, genesis_entries),
843 resume_sequence,
844 ),
845 ResyncDecision::Retry => {
846 drop(reader);
849 drop(tcp_writer);
850 sleep_then_double_backoff(&mut backoff, shutdown, promote);
851 continue;
852 }
853 }
854 }
855 _ => {
856 return Err(format!("unexpected response: {response:?}").into());
857 }
858 };
859
860 if pipeline.is_none() && journal_writer.is_none() {
871 let (lineage_start, lineage_anchor, lineage_genesis) = stream_lineage;
872 let writer = BufferedWriter::create_continuing(
873 journal_path,
874 lineage_start,
875 lineage_anchor,
876 lineage_genesis,
877 )?;
878 app = Some(A::default());
879 journal_writer = Some(writer);
880 }
881
882 if pipeline.is_none() {
884 let cur_app = app.take().expect("application initialized");
885 let cur_writer = journal_writer.take().expect("journal_writer initialized");
886 pipeline = Some(build_replica_pipeline_with_threads::<A>(
887 cur_app,
888 cur_writer,
889 cores,
890 staging_mode,
891 snapshot_interval_ms,
892 snapshot_path.clone(),
893 group_commit_delay,
894 Arc::clone(&fence_state),
895 lineage_ack_policy,
896 Arc::clone(pipeline_healthy),
897 sizing,
898 )?);
899 }
900
901 let result = {
903 let p = pipeline.as_mut().expect("pipeline must exist by here");
904 let input_producer = &mut p.input_producer;
905 let journal_cursor = p.journal_cursor.as_ref();
906 let stream_marks = &p.stream_marks;
907 let journal_failed = &p.journal_failed;
908 std::thread::scope(|s| {
909 let receiver = || {
910 melin_app::affinity::pin_thread("replica-receiver", cores.reader.core);
911 let mut transport = match UringTransport::new(&tcp_writer) {
912 Ok(t) => t,
913 Err(e) => {
914 tracing::error!(error = %e, "UringTransport init failed");
915 return super::receiver_transport::StreamingResult {
921 exit: SessionExit::Disconnected,
922 heard_from_primary: false,
923 };
924 }
925 };
926 streaming_loop::<UringTransport, A::Event>(
927 &mut transport,
928 input_producer,
929 journal_cursor,
930 shutdown,
931 control,
932 pipeline_depth,
933 cores.reader.wait,
934 session_start,
935 Vec::with_capacity(MAX_DATA_FRAME + 4),
936 None,
937 stream_marks,
938 journal_failed,
939 )
940 };
941 let handle = melin_app::affinity::with_child_context(
944 "replica-receiver",
945 cores.reader.core,
946 || {
947 std::thread::Builder::new()
948 .name("replica-receiver".into())
949 .spawn_scoped(s, receiver)
950 },
951 )
952 .expect("spawn replica-receiver thread");
953 handle.join().expect("replica-receiver thread panicked")
954 })
955 };
956
957 primary_link_up.store(false, Ordering::Release);
958 let after = handle_session_exit(
959 result,
960 &mut pipeline,
961 &mut divergence_resyncs,
962 &mut backoff,
963 last_sequence,
964 journal_path,
965 &snapshot_path,
966 &fence_state,
967 lineage_ack_policy,
968 shutdown,
969 promote,
970 || {},
974 sizing,
975 );
976 if let Some(r) = after.adopt(
977 &mut app,
978 &mut journal_writer,
979 &mut last_sequence,
980 &mut chain_hash,
981 ) {
982 return r;
983 }
984 }
985}
986
987#[cfg(test)]
992mod tests {
993 use super::*;
994 use crate::uring_teardown::DRAIN_TIMEOUT;
995 use std::io::Read;
996 use std::net::TcpListener;
997 use std::time::{Duration, Instant};
998
999 use melin_transport_core::replication::protocol::{ReplicaMessage, decode_replica_message};
1000 use melin_wire_protocol::framing::split_frame_limited;
1001
1002 fn socket_pair() -> (TcpStream, TcpStream) {
1004 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1005 let addr = listener.local_addr().unwrap();
1006 let client = TcpStream::connect(addr).unwrap();
1007 let (server, _) = listener.accept().unwrap();
1008 (client, server)
1009 }
1010
1011 fn ack(seq: u64) -> Ack {
1012 Ack {
1013 acked_sequence: seq,
1014 in_memory_sequence: seq,
1015 }
1016 }
1017
1018 fn flush_acks(transport: &mut UringTransport) {
1020 let mut recv_buf = Vec::new();
1021 let deadline = Instant::now() + Duration::from_secs(5);
1022 while transport.ack_in_flight() {
1023 transport.poll_recv(&mut recv_buf).unwrap();
1024 assert!(Instant::now() < deadline, "ack send never completed");
1025 std::hint::spin_loop();
1026 }
1027 }
1028
1029 fn read_acks(peer: &mut TcpStream, expect: usize) -> Vec<Ack> {
1031 peer.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
1032 let mut buf = Vec::new();
1033 let mut chunk = [0u8; 256];
1034 let mut acks = Vec::new();
1035 while acks.len() < expect {
1036 let n = peer.read(&mut chunk).expect("peer read");
1037 assert!(n > 0, "peer closed before all acks arrived");
1038 buf.extend_from_slice(&chunk[..n]);
1039 while let Some(frame) = split_frame_limited(&buf, MAX_CONTROL_FRAME).unwrap() {
1041 match decode_replica_message(frame.payload).expect("decodable frame") {
1042 ReplicaMessage::Ack(a) => acks.push(a),
1043 other => panic!("expected Ack frame, got {other:?}"),
1044 }
1045 let consumed = frame.consumed();
1046 buf.drain(..consumed);
1047 }
1048 }
1049 assert!(buf.is_empty(), "trailing bytes after expected acks");
1050 acks
1051 }
1052
1053 #[test]
1054 fn coalesces_to_latest_ack_while_send_in_flight() {
1055 let (client, mut peer) = socket_pair();
1056 let mut transport = UringTransport::new(&client).unwrap();
1057
1058 assert!(transport.send_ack(&ack(1)).unwrap());
1063 assert!(transport.ack_in_flight());
1064 assert!(transport.send_ack(&ack(2)).unwrap());
1065 assert!(transport.send_ack(&ack(3)).unwrap());
1066
1067 flush_acks(&mut transport);
1068
1069 let acks = read_acks(&mut peer, 2);
1071 let seqs: Vec<u64> = acks.iter().map(|a| a.acked_sequence).collect();
1072 assert_eq!(seqs, vec![1, 3]);
1073 }
1074
1075 #[test]
1076 fn sequential_acks_all_reach_the_wire_in_order() {
1077 let (client, mut peer) = socket_pair();
1078 let mut transport = UringTransport::new(&client).unwrap();
1079
1080 for seq in 1..=3 {
1081 assert!(transport.send_ack(&ack(seq)).unwrap());
1082 flush_acks(&mut transport);
1083 }
1084
1085 let acks = read_acks(&mut peer, 3);
1086 let seqs: Vec<u64> = acks.iter().map(|a| a.acked_sequence).collect();
1087 assert_eq!(seqs, vec![1, 2, 3]);
1088 }
1089
1090 #[test]
1098 fn drop_quiesces_the_multishot_recv() {
1099 let (client, _peer) = socket_pair();
1100 let mut transport = UringTransport::new(&client).unwrap();
1101 let mut recv_buf = Vec::new();
1104 transport.poll_recv(&mut recv_buf).unwrap();
1105 assert!(transport.multishot_active);
1106
1107 let started = Instant::now();
1108 drop(transport);
1109 let elapsed = started.elapsed();
1110 assert!(
1111 elapsed < DRAIN_TIMEOUT / 4,
1112 "teardown took {elapsed:?}, close to the {DRAIN_TIMEOUT:?} deadline"
1113 );
1114 }
1115
1116 #[test]
1123 fn abandoned_cqes_still_settle_kernel_ownership() {
1124 let (client, _peer) = socket_pair();
1125 let mut transport = UringTransport::new(&client).unwrap();
1126 let mut recv_buf = Vec::new();
1127 transport.poll_recv(&mut recv_buf).unwrap();
1128
1129 transport.ack_in_flight = true;
1132 transport.settle_unprocessed(&[(TOKEN_SEND, 13, 0)]);
1133 assert!(
1134 !transport.ack_in_flight,
1135 "an abandoned SEND completion must release `ack_buf`"
1136 );
1137
1138 transport.settle_unprocessed(&[(TOKEN_RECV, 128, IORING_CQE_F_MORE)]);
1141 assert!(
1142 transport.multishot_active,
1143 "F_MORE must leave the multishot armed"
1144 );
1145 transport.settle_unprocessed(&[(TOKEN_RECV, 0, 0)]);
1147 assert!(!transport.multishot_active);
1148
1149 transport.multishot_active = true;
1153 }
1154
1155 #[test]
1159 fn drop_quiesces_a_pending_ack_send() {
1160 let (client, _peer) = socket_pair();
1161 let mut transport = UringTransport::new(&client).unwrap();
1162 assert!(transport.send_ack(&ack(7)).unwrap());
1163 assert!(transport.ack_in_flight());
1164
1165 let started = Instant::now();
1166 drop(transport);
1167 let elapsed = started.elapsed();
1168 assert!(
1169 elapsed < DRAIN_TIMEOUT / 4,
1170 "teardown took {elapsed:?}, close to the {DRAIN_TIMEOUT:?} deadline"
1171 );
1172 }
1173
1174 #[cfg(not(feature = "no-persist"))]
1179 mod scripted {
1180 use super::*;
1181 use melin_app::{AppEvent, Application, ApplyCtx, CodecError, QueryCtx, RejectReason};
1182
1183 pub(super) use melin_transport_core::pipeline::InputSlot;
1186
1187 #[derive(Debug, Clone, Copy, PartialEq)]
1188 pub(super) struct EvtAdd(pub(super) u64);
1189
1190 impl AppEvent for EvtAdd {
1191 const MAX_ENCODED_SIZE: usize = 8;
1192
1193 fn encoded_size(&self) -> usize {
1194 8
1195 }
1196 fn encode(&self, buf: &mut [u8]) -> usize {
1197 buf[..8].copy_from_slice(&self.0.to_le_bytes());
1198 8
1199 }
1200 fn decode(buf: &[u8]) -> Result<Self, CodecError> {
1201 if buf.len() < 8 {
1202 return Err(CodecError::Truncated);
1203 }
1204 Ok(EvtAdd(u64::from_le_bytes(buf[..8].try_into().expect("8"))))
1205 }
1206 fn is_query(&self) -> bool {
1207 false
1208 }
1209 }
1210
1211 #[derive(Debug, Clone, Copy)]
1212 pub(super) struct Rpt;
1213
1214 #[derive(Default)]
1215 pub(super) struct App;
1216
1217 impl Application for App {
1218 type Event = EvtAdd;
1219 type Report = Rpt;
1220 type QueryResponse = Rpt;
1221 type Sizing = ();
1222 const APP_VERSION: u16 = 1;
1223
1224 fn apply(&mut self, _e: EvtAdd, _ctx: &ApplyCtx, _out: &mut Vec<Rpt>) {}
1225 fn query(&self, _e: EvtAdd, _ctx: &QueryCtx) -> Option<Rpt> {
1226 None
1227 }
1228 fn tick(&mut self, _now_ns: u64, _out: &mut Vec<Rpt>) {}
1229 fn build_reject(_e: &EvtAdd, _r: RejectReason) -> Rpt {
1230 Rpt
1231 }
1232 fn snapshot<W: std::io::Write>(&self, _w: &mut W) -> std::io::Result<()> {
1233 Ok(())
1234 }
1235 fn restore<R: std::io::Read>(_r: &mut R) -> std::io::Result<Self> {
1236 Ok(App)
1237 }
1238 }
1239
1240 pub(super) const REPLICA_KEY: [u8; 32] = [0xFC; 32];
1243
1244 pub(super) fn replica_auth() -> melin_app::auth::AuthorizedKeys {
1247 let key = ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY);
1248 let pub_b64 = base64::Engine::encode(
1249 &base64::engine::general_purpose::STANDARD,
1250 key.verifying_key().to_bytes(),
1251 );
1252 melin_app::auth::AuthorizedKeys::parse::<melin_app::auth::NoRoles>(&format!(
1253 "replication {pub_b64} test-replica\n"
1254 ))
1255 .expect("parse keys")
1256 }
1257
1258 pub(super) fn read_replica_msg(stream: &mut TcpStream) -> ReplicaMessage {
1260 let mut len_buf = [0u8; 4];
1261 stream.read_exact(&mut len_buf).expect("frame length");
1262 let len = u32::from_le_bytes(len_buf) as usize;
1263 let mut payload = vec![0u8; len];
1264 stream.read_exact(&mut payload).expect("frame payload");
1265 decode_replica_message(&payload).expect("decodable replica frame")
1266 }
1267
1268 pub(super) fn wait_for_ack(stream: &mut TcpStream, seq: u64) {
1270 loop {
1271 if let ReplicaMessage::Ack(a) = read_replica_msg(stream)
1272 && a.acked_sequence >= seq
1273 {
1274 return;
1275 }
1276 }
1277 }
1278
1279 pub(super) fn accept_within(listener: &std::net::TcpListener, secs: u64) -> TcpStream {
1282 let deadline = Instant::now() + Duration::from_secs(secs);
1283 loop {
1284 match listener.accept() {
1285 Ok((s, _)) => {
1286 s.set_read_timeout(Some(Duration::from_secs(10)))
1287 .expect("read timeout");
1288 return s;
1289 }
1290 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
1291 assert!(Instant::now() < deadline, "replica never connected");
1292 std::thread::sleep(Duration::from_millis(10));
1293 }
1294 Err(e) => panic!("accept failed: {e}"),
1295 }
1296 }
1297 }
1298 }
1299
1300 #[cfg(all(feature = "hash-chain", not(feature = "no-persist")))]
1306 mod divergence_resync {
1307 use super::super::super::auth::authenticate_replica;
1308 use super::scripted::*;
1309 use super::*;
1310 use melin_journal::{
1311 AckPolicyByte, AckPolicyChange, BufferedWriter, JournalEvent, JournalWrite,
1312 };
1313 use melin_transport_core::cursors::WireSeq;
1314 use melin_transport_core::replication::catchup::{lineage_origin, snapshot_transfer_with};
1315 use melin_transport_core::replication::protocol::{
1316 PrimaryMessage, decode_primary_message, encode_hash_mismatch, encode_rotate,
1317 encode_stream_start,
1318 };
1319 use std::sync::Arc;
1320 use std::sync::atomic::AtomicBool;
1321
1322 const DIVERGENT_POLICY: AckPolicyByte = AckPolicyByte(1);
1325
1326 const PRIMARY_POLICY: AckPolicyByte = AckPolicyByte(0);
1329
1330 const PRIMARY_LINEAGE_POLICY: melin_transport_core::lineage::LineagePolicy =
1333 melin_transport_core::lineage::LineagePolicy {
1334 change: AckPolicyChange {
1335 policy: PRIMARY_POLICY,
1336 unwitnessed: true,
1337 },
1338 wire_seq: WireSeq::new(3),
1339 previous: Some(AckPolicyByte(2)),
1340 };
1341
1342 const SHADOW_INTERVAL_MS: u64 = 50;
1344
1345 #[test]
1354 fn mid_stream_divergence_resyncs_in_process() {
1355 let dir = tempfile::tempdir().expect("tempdir");
1356
1357 let primary_journal = dir.path().join("primary.journal");
1361 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1362 let mut chain_at_4 = [0u8; 32];
1363 for v in 1..=5u64 {
1364 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1365 if v == 2 {
1366 w.rotate_segment().expect("rotate");
1367 }
1368 if v == 4 {
1369 chain_at_4 = w.chain_hash().expect("chain");
1370 }
1371 }
1372 drop(w);
1373 melin_transport_core::snapshot::save::<App>(
1377 &App,
1378 WireSeq::new(4),
1379 chain_at_4,
1380 melin_transport_core::lineage::Lineage {
1381 epoch: 0,
1382 ack_policy: Some(PRIMARY_LINEAGE_POLICY),
1383 },
1384 Some(0),
1385 &primary_journal.with_extension("snapshot"),
1386 )
1387 .expect("save snapshot");
1388 let origin = lineage_origin(&primary_journal).expect("lineage");
1389 let (lineage_start, lineage_anchor, lineage_genesis) = (
1390 origin.starting_sequence,
1391 origin.anchor_hash,
1392 origin.genesis_entries,
1393 );
1394
1395 let authorized_keys = replica_auth();
1397
1398 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1399 listener.set_nonblocking(true).expect("nonblocking");
1400 let addr = listener.local_addr().expect("addr");
1401
1402 let replica_journal = dir.path().join("replica.journal");
1404 let replica_snapshot = dir.path().join("replica.snapshot");
1405 let replica_snapshot_check = replica_snapshot.clone();
1406 let shutdown = Arc::new(AtomicBool::new(false));
1407 let control = crate::replication::ReplicaControlPlane::new();
1408 let cores = crate::layout::PipelineCores::unpinned();
1409 let replica = {
1410 let journal = replica_journal.clone();
1411 let shutdown = Arc::clone(&shutdown);
1412 let control = control.clone();
1413 std::thread::spawn(move || -> Result<bool, String> {
1414 run_receiver::<App>(
1415 addr,
1416 &journal,
1417 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1418 &shutdown,
1419 &control,
1420 SHADOW_INTERVAL_MS,
1424 replica_snapshot,
1425 cores,
1426 melin_journal::StagingMode::ZeroFill,
1427 Duration::ZERO,
1428 64,
1429 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1430 &(),
1431 )
1432 .map(|state| state.is_none())
1434 .map_err(|e| e.to_string())
1435 })
1436 };
1437
1438 let mut s1 = accept_within(&listener, 30);
1441 let mut s1r = s1.try_clone().expect("clone");
1442 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1443 match read_replica_msg(&mut s1) {
1444 ReplicaMessage::Handshake(h) => {
1445 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
1446 }
1447 other => panic!("expected Handshake, got {other:?}"),
1448 }
1449 let mut buf = Vec::new();
1450 encode_stream_start(
1451 0,
1452 lineage_start,
1453 lineage_anchor,
1454 lineage_genesis,
1455 0,
1456 &mut buf,
1457 );
1458 s1.write_all(&buf).expect("StreamStart 1");
1459 buf.clear();
1460 melin_transport_core::replication_wire::encode_input_batch(
1464 &[InputSlot::<EvtAdd> {
1465 connection_id: 0,
1466 key_hash: 0,
1467 sequence: 1,
1468 timestamp_ns: 1,
1469 event: JournalEvent::AckPolicyChange(AckPolicyChange {
1470 policy: DIVERGENT_POLICY,
1471 unwitnessed: false,
1472 }),
1473 publish_ts: Default::default(),
1475 recv_ts: Default::default(),
1476 }],
1477 &mut buf,
1478 )
1479 .expect("encode InputBatch");
1480 s1.write_all(&buf).expect("InputBatch");
1481 wait_for_ack(&mut s1, 1);
1482
1483 buf.clear();
1486 encode_rotate(1, &[0xEE; 32], &mut buf);
1487 s1.write_all(&buf).expect("Rotate");
1488
1489 let mut s2 = accept_within(&listener, 30);
1492 let mut s2r = s2.try_clone().expect("clone");
1493 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1494 match read_replica_msg(&mut s2) {
1495 ReplicaMessage::Handshake(h) => assert_eq!(
1496 h.last_sequence, 1,
1497 "post-divergence handshake must carry the recovered journal position"
1498 ),
1499 other => panic!("expected Handshake, got {other:?}"),
1500 }
1501 assert!(
1507 !control.pipeline_healthy.load(Ordering::Acquire),
1508 "the divergence must have latched the health mirror false"
1509 );
1510 assert_eq!(
1514 control.lineage_ack_policy.load(),
1515 Some(melin_transport_core::lineage::LineagePolicy {
1516 change: AckPolicyChange {
1517 policy: DIVERGENT_POLICY,
1518 unwitnessed: false,
1519 },
1520 wire_seq: WireSeq::new(1),
1521 previous: None,
1522 }),
1523 "recovery must store the forked journal's policy"
1524 );
1525 buf.clear();
1526 encode_hash_mismatch(&mut buf);
1527 s2.write_all(&buf).expect("HashMismatch");
1528 let transfer_shutdown = AtomicBool::new(false);
1529 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1530 s2.write_all(b)?;
1531 s2.flush()
1532 };
1533 let end = snapshot_transfer_with::<EvtAdd>(
1536 &primary_journal,
1537 &mut publish,
1538 &transfer_shutdown,
1539 )
1540 .expect("snapshot transfer");
1541 assert_eq!(
1542 end,
1543 melin_transport_core::replication::catchup::CatchUpResult::Ok(5)
1544 );
1545 let mut s2_acks = s2.try_clone().expect("clone");
1546 wait_for_ack(&mut s2_acks, 5);
1547
1548 assert!(
1551 control.pipeline_healthy.load(Ordering::Acquire),
1552 "the resync rebuild must reset the health mirror true"
1553 );
1554 assert_eq!(
1558 control.lineage_ack_policy.load(),
1559 Some(PRIMARY_LINEAGE_POLICY),
1560 "the resync must adopt the primary snapshot's policy and drop the divergent tail's"
1561 );
1562 let deadline = std::time::Instant::now() + Duration::from_secs(30);
1566 let shadow_lineage = loop {
1567 if let Ok((_, header)) =
1569 melin_transport_core::snapshot::load::<App>(&replica_snapshot_check)
1570 && header.sequence == 5
1571 {
1572 break header.lineage;
1573 }
1574 assert!(
1575 std::time::Instant::now() < deadline,
1576 "the rebuilt shadow never snapshotted entry 5"
1577 );
1578 std::thread::sleep(Duration::from_millis(10));
1579 };
1580 assert_eq!(
1581 shadow_lineage.ack_policy,
1582 Some(PRIMARY_LINEAGE_POLICY),
1583 "the replica's shadow must seed its lineage from the adopted policy"
1584 );
1585
1586 shutdown.store(true, Ordering::Relaxed);
1589 let result = replica.join().expect("replica thread panicked");
1590 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
1591
1592 assert!(
1595 dir.path().join("replica.journal.divergent.0").exists(),
1596 "divergent lineage must be archived"
1597 );
1598 assert!(replica_journal.exists(), "re-seeded live journal");
1599 }
1600
1601 #[test]
1620 fn resync_transfer_failure_rebuilds_clean_not_over_stale_writer() {
1621 let dir = tempfile::tempdir().expect("tempdir");
1622
1623 let primary_journal = dir.path().join("primary.journal");
1628 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1629 let mut chain_at_2 = [0u8; 32];
1630 for v in 1..=3u64 {
1631 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1632 if v == 2 {
1633 chain_at_2 = w.chain_hash().expect("chain");
1634 }
1635 }
1636 drop(w);
1637 melin_transport_core::snapshot::save::<App>(
1638 &App,
1639 WireSeq::new(2),
1640 chain_at_2,
1641 melin_transport_core::lineage::Lineage {
1642 epoch: 0,
1643 ack_policy: Some(PRIMARY_LINEAGE_POLICY),
1644 },
1645 Some(0),
1646 &primary_journal.with_extension("snapshot"),
1647 )
1648 .expect("save snapshot");
1649 let origin = lineage_origin(&primary_journal).expect("lineage");
1650 let (lineage_start, lineage_anchor, lineage_genesis) = (
1651 origin.starting_sequence,
1652 origin.anchor_hash,
1653 origin.genesis_entries,
1654 );
1655
1656 let authorized_keys = replica_auth();
1657
1658 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1659 listener.set_nonblocking(true).expect("nonblocking");
1660 let addr = listener.local_addr().expect("addr");
1661
1662 let replica_journal = dir.path().join("replica.journal");
1663 let replica_snapshot = dir.path().join("replica.snapshot");
1664 let shutdown = Arc::new(AtomicBool::new(false));
1665 let control = crate::replication::ReplicaControlPlane::new();
1666 let cores = crate::layout::PipelineCores::unpinned();
1667 let replica = {
1668 let journal = replica_journal.clone();
1669 let shutdown = Arc::clone(&shutdown);
1670 let control = control.clone();
1671 std::thread::spawn(move || -> Result<bool, String> {
1672 run_receiver::<App>(
1673 addr,
1674 &journal,
1675 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1676 &shutdown,
1677 &control,
1678 3_600_000,
1679 replica_snapshot,
1680 cores,
1681 melin_journal::StagingMode::ZeroFill,
1682 Duration::ZERO,
1683 64,
1684 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1685 &(),
1686 )
1687 .map(|state| state.is_none())
1688 .map_err(|e| e.to_string())
1689 })
1690 };
1691
1692 let stream_one = |s: &mut TcpStream, buf: &mut Vec<u8>| {
1695 buf.clear();
1696 encode_stream_start(0, lineage_start, lineage_anchor, lineage_genesis, 0, buf);
1697 s.write_all(buf).expect("StreamStart");
1698 buf.clear();
1699 melin_transport_core::replication_wire::encode_input_batch(
1700 &[InputSlot::<EvtAdd> {
1701 connection_id: 0,
1702 key_hash: 0,
1703 sequence: 1,
1704 timestamp_ns: 1,
1705 event: JournalEvent::App(EvtAdd(1)),
1706 publish_ts: Default::default(),
1707 recv_ts: Default::default(),
1708 }],
1709 buf,
1710 )
1711 .expect("encode InputBatch");
1712 s.write_all(buf).expect("InputBatch");
1713 };
1714
1715 let mut buf = Vec::new();
1720 let mut s1 = accept_within(&listener, 30);
1721 let mut s1r = s1.try_clone().expect("clone");
1722 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1723 match read_replica_msg(&mut s1) {
1724 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0, "fresh"),
1725 other => panic!("expected Handshake, got {other:?}"),
1726 }
1727 stream_one(&mut s1, &mut buf);
1728 wait_for_ack(&mut s1, 1);
1729 buf.clear();
1730 encode_rotate(1, &[0xEE; 32], &mut buf);
1731 s1.write_all(&buf).expect("Rotate");
1732
1733 let mut s2 = accept_within(&listener, 30);
1740 let mut s2r = s2.try_clone().expect("clone");
1741 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1742 match read_replica_msg(&mut s2) {
1743 ReplicaMessage::Handshake(h) => assert_eq!(
1744 h.last_sequence, 1,
1745 "reconnect carries the recovered forked position"
1746 ),
1747 other => panic!("expected Handshake, got {other:?}"),
1748 }
1749 buf.clear();
1750 encode_hash_mismatch(&mut buf);
1751 s2.write_all(&buf).expect("HashMismatch");
1752 s2.flush().expect("flush");
1753 {
1754 let transfer_shutdown = AtomicBool::new(false);
1755 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1759 if matches!(
1760 decode_primary_message(&b[4..]),
1761 Ok(PrimaryMessage::SegmentSeedBegin { .. })
1762 ) {
1763 return Err(std::io::Error::other("scripted drop before the seed"));
1764 }
1765 s2.write_all(b)?;
1766 s2.flush()
1767 };
1768 snapshot_transfer_with::<EvtAdd>(
1769 &primary_journal,
1770 &mut publish,
1771 &transfer_shutdown,
1772 )
1773 .expect_err("the scripted primary drops before the seed");
1774 }
1775 drop(s2);
1779 drop(s2r);
1780
1781 let mut s3 = accept_within(&listener, 30);
1786 let mut s3r = s3.try_clone().expect("clone");
1787 authenticate_replica(&mut s3r, &authorized_keys).expect("auth 3");
1788 match read_replica_msg(&mut s3) {
1789 ReplicaMessage::Handshake(h) => assert_eq!(
1790 h.last_sequence, 0,
1791 "after a failed transfer the retry handshakes as fresh"
1792 ),
1793 other => panic!("expected Handshake, got {other:?}"),
1794 }
1795 assert_eq!(
1799 control.lineage_ack_policy.load(),
1800 None,
1801 "a failed transfer must not adopt the snapshot's policy"
1802 );
1803 stream_one(&mut s3, &mut buf);
1804 wait_for_ack(&mut s3, 1);
1805
1806 shutdown.store(true, Ordering::Relaxed);
1807 let result = replica.join().expect("replica thread panicked");
1808 assert_eq!(result, Ok(true), "receiver exits cleanly via shutdown");
1809
1810 assert!(
1815 dir.path().join("replica.journal.divergent.0").exists(),
1816 "forked lineage archived, never deleted"
1817 );
1818 assert!(
1819 replica_journal.exists(),
1820 "fresh live journal recreated after the failed transfer"
1821 );
1822 }
1823
1824 #[test]
1831 fn second_mid_stream_divergence_exits_hard() {
1832 let dir = tempfile::tempdir().expect("tempdir");
1833
1834 let primary_journal = dir.path().join("primary.journal");
1835 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1836 let mut chain_at_4 = [0u8; 32];
1837 for v in 1..=5u64 {
1838 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1839 if v == 2 {
1840 w.rotate_segment().expect("rotate");
1841 }
1842 if v == 4 {
1843 chain_at_4 = w.chain_hash().expect("chain");
1844 }
1845 }
1846 drop(w);
1847 melin_transport_core::snapshot::save::<App>(
1848 &App,
1849 WireSeq::new(4),
1850 chain_at_4,
1851 melin_transport_core::lineage::Lineage::GENESIS,
1852 Some(0),
1853 &primary_journal.with_extension("snapshot"),
1854 )
1855 .expect("save snapshot");
1856 let origin = lineage_origin(&primary_journal).expect("lineage");
1857 let (lineage_start, lineage_anchor, lineage_genesis) = (
1858 origin.starting_sequence,
1859 origin.anchor_hash,
1860 origin.genesis_entries,
1861 );
1862
1863 let authorized_keys = replica_auth();
1864
1865 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1866 listener.set_nonblocking(true).expect("nonblocking");
1867 let addr = listener.local_addr().expect("addr");
1868
1869 let replica_journal = dir.path().join("replica.journal");
1870 let replica_snapshot = dir.path().join("replica.snapshot");
1871 let shutdown = Arc::new(AtomicBool::new(false));
1872 let control = crate::replication::ReplicaControlPlane::new();
1873 let cores = crate::layout::PipelineCores::unpinned();
1874 let replica = {
1875 let journal = replica_journal.clone();
1876 let shutdown = Arc::clone(&shutdown);
1877 let control = control.clone();
1878 std::thread::spawn(move || -> Result<bool, String> {
1879 run_receiver::<App>(
1880 addr,
1881 &journal,
1882 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1883 &shutdown,
1884 &control,
1885 3_600_000,
1886 replica_snapshot,
1887 cores,
1888 melin_journal::StagingMode::ZeroFill,
1889 Duration::ZERO,
1890 64,
1891 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1892 &(),
1893 )
1894 .map(|state| state.is_none())
1895 .map_err(|e| e.to_string())
1896 })
1897 };
1898
1899 let mut s1 = accept_within(&listener, 30);
1901 let mut s1r = s1.try_clone().expect("clone");
1902 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1903 match read_replica_msg(&mut s1) {
1904 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0),
1905 other => panic!("expected Handshake, got {other:?}"),
1906 }
1907 let mut buf = Vec::new();
1908 encode_stream_start(
1909 0,
1910 lineage_start,
1911 lineage_anchor,
1912 lineage_genesis,
1913 0,
1914 &mut buf,
1915 );
1916 s1.write_all(&buf).expect("StreamStart 1");
1917 buf.clear();
1918 melin_transport_core::replication_wire::encode_input_batch(
1919 &[InputSlot::<EvtAdd> {
1920 connection_id: 0,
1921 key_hash: 0,
1922 sequence: 1,
1923 timestamp_ns: 1,
1924 event: JournalEvent::App(EvtAdd(1)),
1925 publish_ts: Default::default(),
1926 recv_ts: Default::default(),
1927 }],
1928 &mut buf,
1929 )
1930 .expect("encode InputBatch");
1931 s1.write_all(&buf).expect("InputBatch");
1932 wait_for_ack(&mut s1, 1);
1933 buf.clear();
1934 encode_rotate(1, &[0xEE; 32], &mut buf);
1935 s1.write_all(&buf).expect("Rotate 1");
1936
1937 let mut s2 = accept_within(&listener, 30);
1940 let mut s2r = s2.try_clone().expect("clone");
1941 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1942 match read_replica_msg(&mut s2) {
1943 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 1),
1944 other => panic!("expected Handshake, got {other:?}"),
1945 }
1946 buf.clear();
1947 encode_hash_mismatch(&mut buf);
1948 s2.write_all(&buf).expect("HashMismatch");
1949 let transfer_shutdown = AtomicBool::new(false);
1950 {
1951 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1952 s2.write_all(b)?;
1953 s2.flush()
1954 };
1955 snapshot_transfer_with::<EvtAdd>(
1956 &primary_journal,
1957 &mut publish,
1958 &transfer_shutdown,
1959 )
1960 .expect("snapshot transfer");
1961 }
1962 buf.clear();
1963 melin_transport_core::replication_wire::encode_input_batch(
1964 &[InputSlot::<EvtAdd> {
1965 connection_id: 0,
1966 key_hash: 0,
1967 sequence: 6,
1968 timestamp_ns: 6,
1969 event: JournalEvent::App(EvtAdd(6)),
1970 publish_ts: Default::default(),
1971 recv_ts: Default::default(),
1972 }],
1973 &mut buf,
1974 )
1975 .expect("encode InputBatch");
1976 s2.write_all(&buf).expect("InputBatch 6");
1977 let mut s2_acks = s2.try_clone().expect("clone");
1978 wait_for_ack(&mut s2_acks, 6);
1979 buf.clear();
1980 encode_rotate(6, &[0xEE; 32], &mut buf);
1981 s2.write_all(&buf).expect("Rotate 2");
1982
1983 let result = replica.join().expect("replica thread panicked");
1986 let err = result.expect_err("second divergence must exit hard");
1987 assert!(
1988 err.contains("divergence recurred"),
1989 "error must name the recurrence: {err}"
1990 );
1991 }
1992
1993 type PromoteHandles =
2002 crate::replication::ReplicaPipelineHandles<App, BufferedWriter<EvtAdd>>;
2003
2004 #[test]
2005 fn promotion_hands_back_local_state_when_present() {
2006 let dir = tempfile::tempdir().unwrap();
2007 let mut pipeline: Option<PromoteHandles> = None;
2008 let mut app = Some(App);
2009 let mut journal_writer =
2010 Some(BufferedWriter::<EvtAdd>::create(&dir.path().join("p.journal")).unwrap());
2011
2012 let result = crate::replication::take_pipeline_for_promotion(
2013 &mut pipeline,
2014 &mut app,
2015 &mut journal_writer,
2016 );
2017
2018 assert!(matches!(result, Ok(Some(_))), "warm state must be promoted");
2019 assert!(
2020 app.is_none() && journal_writer.is_none(),
2021 "state must be moved into the result"
2022 );
2023 }
2024
2025 #[test]
2026 fn promotion_errs_when_no_local_state() {
2027 let mut pipeline: Option<PromoteHandles> = None;
2028 let mut app: Option<App> = None;
2029 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
2030
2031 let result = crate::replication::take_pipeline_for_promotion(
2032 &mut pipeline,
2033 &mut app,
2034 &mut journal_writer,
2035 );
2036
2037 let err = match result {
2039 Err(e) => e,
2040 Ok(_) => panic!("a promote with nothing to promote must error"),
2041 };
2042 assert!(
2043 err.to_string().contains("no local state available"),
2044 "{err}"
2045 );
2046 }
2047
2048 #[test]
2049 fn promotion_errs_on_partial_state() {
2050 let mut pipeline: Option<PromoteHandles> = None;
2052 let mut app = Some(App);
2053 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
2054
2055 let result = crate::replication::take_pipeline_for_promotion(
2056 &mut pipeline,
2057 &mut app,
2058 &mut journal_writer,
2059 );
2060
2061 assert!(result.is_err(), "partial state is not a valid promotion");
2062 }
2063 }
2064
2065 #[cfg(not(feature = "no-persist"))]
2071 mod backoff_shutdown {
2072 use super::super::super::auth::authenticate_replica;
2073 use super::scripted::*;
2074 use super::*;
2075 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
2076 use melin_transport_core::replication::catchup::lineage_origin;
2077 use melin_transport_core::replication::protocol::encode_stream_start;
2078 use std::sync::Arc;
2079 use std::sync::atomic::AtomicBool;
2080
2081 fn live_thread_names() -> Vec<String> {
2084 std::fs::read_dir("/proc/self/task")
2085 .expect("read /proc/self/task")
2086 .filter_map(|entry| {
2087 let entry = entry.ok()?;
2090 let name = std::fs::read_to_string(entry.path().join("comm")).ok()?;
2091 Some(name.trim().to_string())
2092 })
2093 .collect()
2094 }
2095
2096 const ISOLATED: &str = "MELIN_TEST_BACKOFF_SHUTDOWN_ISOLATED";
2098
2099 #[test]
2112 fn shutdown_during_reconnect_backoff_joins_pipeline_threads() {
2113 if std::env::var_os(ISOLATED).is_some() {
2114 return backoff_shutdown_body();
2115 }
2116 let module = module_path!();
2119 let path = module.split_once("::").map_or(module, |(_, rest)| rest);
2120 let test = format!("{path}::shutdown_during_reconnect_backoff_joins_pipeline_threads");
2121 let output = std::process::Command::new(std::env::current_exe().expect("current_exe"))
2122 .args(["--exact", &test, "--test-threads=1", "--nocapture"])
2123 .env(ISOLATED, "1")
2124 .output()
2125 .expect("run the test in a process of its own");
2126 let stdout = String::from_utf8_lossy(&output.stdout);
2127 let stderr = String::from_utf8_lossy(&output.stderr);
2128 assert!(
2131 output.status.success() && stdout.contains("1 passed"),
2132 "the isolated run failed ({}):\n{stdout}\n{stderr}",
2133 output.status
2134 );
2135 }
2136
2137 fn backoff_shutdown_body() {
2138 let dir = tempfile::tempdir().expect("tempdir");
2139
2140 let primary_journal = dir.path().join("primary.journal");
2143 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
2144 w.append(&JournalEvent::App(EvtAdd(1))).expect("append");
2145 drop(w);
2146 let origin = lineage_origin(&primary_journal).expect("lineage");
2147 let (lineage_start, lineage_anchor, lineage_genesis) = (
2148 origin.starting_sequence,
2149 origin.anchor_hash,
2150 origin.genesis_entries,
2151 );
2152
2153 let authorized_keys = replica_auth();
2154 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2155 listener.set_nonblocking(true).expect("nonblocking");
2156 let addr = listener.local_addr().expect("addr");
2157
2158 let shutdown = Arc::new(AtomicBool::new(false));
2159 let control = crate::replication::ReplicaControlPlane::new();
2160 let replica = {
2161 let journal = dir.path().join("replica.journal");
2162 let snapshot = dir.path().join("replica.snapshot");
2163 let shutdown = Arc::clone(&shutdown);
2164 let control = control.clone();
2165 std::thread::spawn(move || -> Result<bool, String> {
2166 run_receiver::<App>(
2167 addr,
2168 &journal,
2169 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
2170 &shutdown,
2171 &control,
2172 3_600_000,
2173 snapshot,
2174 crate::layout::PipelineCores::unpinned(),
2175 melin_journal::StagingMode::ZeroFill,
2176 Duration::ZERO,
2177 64,
2178 Arc::new(melin_transport_core::fence::FenceState::new(0)),
2179 &(),
2180 )
2181 .map(|state| state.is_none())
2183 .map_err(|e| e.to_string())
2184 })
2185 };
2186
2187 let mut s1 = accept_within(&listener, 30);
2191 let mut s1r = s1.try_clone().expect("clone");
2192 authenticate_replica(&mut s1r, &authorized_keys).expect("auth");
2193 match read_replica_msg(&mut s1) {
2194 ReplicaMessage::Handshake(h) => {
2195 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
2196 }
2197 other => panic!("expected Handshake, got {other:?}"),
2198 }
2199 let mut buf = Vec::new();
2200 encode_stream_start(
2201 0,
2202 lineage_start,
2203 lineage_anchor,
2204 lineage_genesis,
2205 0,
2206 &mut buf,
2207 );
2208 s1.write_all(&buf).expect("StreamStart");
2209 buf.clear();
2210 melin_transport_core::replication_wire::encode_input_batch(
2211 &[InputSlot::<EvtAdd> {
2212 connection_id: 0,
2213 key_hash: 0,
2214 sequence: 1,
2215 timestamp_ns: 1,
2216 event: JournalEvent::App(EvtAdd(1)),
2217 publish_ts: Default::default(),
2218 recv_ts: Default::default(),
2219 }],
2220 &mut buf,
2221 )
2222 .expect("encode InputBatch");
2223 s1.write_all(&buf).expect("InputBatch");
2224 wait_for_ack(&mut s1, 1);
2225
2226 drop(s1);
2230 drop(s1r);
2231 drop(listener);
2232 std::thread::sleep(Duration::from_millis(1500));
2237
2238 shutdown.store(true, Ordering::Relaxed);
2239 let deadline = Instant::now() + Duration::from_secs(10);
2240 while !replica.is_finished() {
2241 assert!(
2242 Instant::now() < deadline,
2243 "receiver did not exit after shutdown"
2244 );
2245 std::thread::sleep(Duration::from_millis(20));
2246 }
2247 let result = replica.join().expect("replica thread panicked");
2248 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
2249
2250 let deadline = Instant::now() + Duration::from_secs(10);
2255 loop {
2256 let names = live_thread_names();
2257 if !names
2258 .iter()
2259 .any(|n| n == "journal-seq" || n == "matching" || n == "drain")
2260 {
2261 break;
2262 }
2263 assert!(
2264 Instant::now() < deadline,
2265 "pipeline threads still alive after shutdown: {names:?}"
2266 );
2267 std::thread::sleep(Duration::from_millis(50));
2268 }
2269 }
2270 }
2271
2272 #[cfg(not(feature = "no-persist"))]
2278 mod reconnect_resume {
2279 use super::super::super::auth::authenticate_replica;
2280 use super::scripted::*;
2281 use super::*;
2282 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
2283 use melin_transport_core::replication::catchup::lineage_origin;
2284 use melin_transport_core::replication::protocol::encode_stream_start;
2285 use std::sync::Arc;
2286 use std::sync::atomic::AtomicBool;
2287
2288 fn primary_journal(
2291 dir: &std::path::Path,
2292 ) -> (std::path::PathBuf, melin_journal::FileHeaderInfo) {
2293 let path = dir.join("primary.journal");
2294 let mut w = BufferedWriter::<EvtAdd>::create(&path).expect("create");
2295 w.append(&JournalEvent::App(EvtAdd(1))).expect("append");
2296 drop(w);
2297 let lineage = lineage_origin(&path).expect("lineage");
2298 (path, lineage)
2299 }
2300
2301 fn spawn_replica(
2304 dir: &std::path::Path,
2305 addr: std::net::SocketAddr,
2306 group_commit_delay: Duration,
2307 shutdown: &Arc<AtomicBool>,
2308 ) -> std::thread::JoinHandle<Result<bool, String>> {
2309 let journal = dir.join("replica.journal");
2310 let snapshot = dir.join("replica.snapshot");
2311 let shutdown = Arc::clone(shutdown);
2312 std::thread::spawn(move || -> Result<bool, String> {
2313 run_receiver::<App>(
2314 addr,
2315 &journal,
2316 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
2317 &shutdown,
2318 &crate::replication::ReplicaControlPlane::new(),
2319 3_600_000,
2320 snapshot,
2321 crate::layout::PipelineCores::unpinned(),
2322 melin_journal::StagingMode::ZeroFill,
2323 group_commit_delay,
2324 64,
2325 Arc::new(melin_transport_core::fence::FenceState::new(0)),
2326 &(),
2327 )
2328 .map(|state| state.is_none())
2330 .map_err(|e| e.to_string())
2331 })
2332 }
2333
2334 fn next_handshake(
2337 listener: &std::net::TcpListener,
2338 ) -> (TcpStream, TcpStream, crate::replication::Handshake) {
2339 let mut s = accept_within(listener, 30);
2340 let mut sr = s.try_clone().expect("clone");
2341 authenticate_replica(&mut sr, &replica_auth()).expect("auth");
2342 match read_replica_msg(&mut s) {
2343 ReplicaMessage::Handshake(h) => (s, sr, h),
2344 other => panic!("expected Handshake, got {other:?}"),
2345 }
2346 }
2347
2348 fn stream_start(
2350 stream: &mut TcpStream,
2351 start: u64,
2352 lineage: melin_journal::FileHeaderInfo,
2353 ) {
2354 let mut buf = Vec::new();
2355 encode_stream_start(
2356 start,
2357 lineage.starting_sequence,
2358 lineage.anchor_hash,
2359 lineage.genesis_entries,
2360 0,
2361 &mut buf,
2362 );
2363 stream.write_all(&buf).expect("StreamStart");
2364 }
2365
2366 fn entry(sequence: u64) -> InputSlot<EvtAdd> {
2368 InputSlot::<EvtAdd> {
2369 connection_id: 0,
2370 key_hash: 0,
2371 sequence,
2372 timestamp_ns: sequence,
2373 event: JournalEvent::App(EvtAdd(sequence)),
2374 publish_ts: Default::default(),
2375 recv_ts: Default::default(),
2376 }
2377 }
2378
2379 fn send_entries(stream: &mut TcpStream, sequences: &[u64]) {
2380 let slots: Vec<_> = sequences.iter().map(|&s| entry(s)).collect();
2381 let mut buf = Vec::new();
2382 melin_transport_core::replication_wire::encode_input_batch(&slots, &mut buf)
2383 .expect("encode InputBatch");
2384 stream.write_all(&buf).expect("InputBatch");
2385 }
2386
2387 fn stop(replica: std::thread::JoinHandle<Result<bool, String>>, shutdown: &AtomicBool) {
2388 shutdown.store(true, Ordering::Relaxed);
2389 let deadline = Instant::now() + Duration::from_secs(10);
2390 while !replica.is_finished() {
2391 assert!(
2392 Instant::now() < deadline,
2393 "receiver did not exit after shutdown"
2394 );
2395 std::thread::sleep(Duration::from_millis(20));
2396 }
2397 let result = replica.join().expect("replica thread panicked");
2398 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
2399 }
2400
2401 #[test]
2407 fn a_reconnect_claims_entries_received_before_the_drop() {
2408 let dir = tempfile::tempdir().expect("tempdir");
2409 let (_, lineage) = primary_journal(dir.path());
2410 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2411 listener.set_nonblocking(true).expect("nonblocking");
2412 let shutdown = Arc::new(AtomicBool::new(false));
2413 let delay = Duration::from_millis(2_500);
2417 let replica = spawn_replica(
2418 dir.path(),
2419 listener.local_addr().expect("addr"),
2420 delay,
2421 &shutdown,
2422 );
2423
2424 let (mut s1, _s1r, h1) = next_handshake(&listener);
2425 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2426 stream_start(&mut s1, 0, lineage);
2427 send_entries(&mut s1, &[1]);
2430 wait_for_ack(&mut s1, 1);
2431 send_entries(&mut s1, &[2, 3, 5]);
2434
2435 let (mut s2, _s2r, h2) = next_handshake(&listener);
2436 assert_eq!(
2437 h2.last_sequence, 3,
2438 "the reconnect must claim the entries received before the drop"
2439 );
2440 stream_start(&mut s2, h2.last_sequence, lineage);
2443 stop(replica, &shutdown);
2444 }
2445
2446 #[test]
2450 fn a_failed_resync_transfer_closes_the_socket_before_backing_off() {
2451 use melin_transport_core::replication::protocol::encode_need_snapshot;
2452
2453 let dir = tempfile::tempdir().expect("tempdir");
2454 let (_, lineage) = primary_journal(dir.path());
2455 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2456 listener.set_nonblocking(true).expect("nonblocking");
2457 let shutdown = Arc::new(AtomicBool::new(false));
2458 let replica = spawn_replica(
2459 dir.path(),
2460 listener.local_addr().expect("addr"),
2461 Duration::ZERO,
2462 &shutdown,
2463 );
2464
2465 let (mut s1, _s1r, _) = next_handshake(&listener);
2466 let mut buf = Vec::new();
2469 encode_need_snapshot(&mut buf);
2470 encode_need_snapshot(&mut buf);
2471 s1.write_all(&buf).expect("NeedSnapshot x2");
2472
2473 s1.set_read_timeout(Some(Duration::from_millis(700)))
2476 .expect("read timeout");
2477 let mut byte = [0u8; 1];
2478 match s1.read(&mut byte) {
2479 Ok(0) => {}
2480 Err(e) if e.kind() == std::io::ErrorKind::ConnectionReset => {}
2481 other => panic!("socket still open during the backoff: {other:?}"),
2482 }
2483
2484 let (mut s2, _s2r, h2) = next_handshake(&listener);
2486 assert_eq!(h2.last_sequence, 0, "retry handshakes as fresh");
2487 stream_start(&mut s2, 0, lineage);
2488 stop(replica, &shutdown);
2489 }
2490
2491 #[cfg(feature = "hash-chain")]
2505 #[test]
2506 fn a_journal_that_dies_while_disconnected_is_torn_down_before_dialing() {
2507 use melin_transport_core::replication::protocol::encode_chain_check;
2508
2509 let dir = tempfile::tempdir().expect("tempdir");
2510 let (_, lineage) = primary_journal(dir.path());
2511 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2512 listener.set_nonblocking(true).expect("nonblocking");
2513 let shutdown = Arc::new(AtomicBool::new(false));
2514 let delay = Duration::from_millis(2_500);
2517 let replica = spawn_replica(
2518 dir.path(),
2519 listener.local_addr().expect("addr"),
2520 delay,
2521 &shutdown,
2522 );
2523
2524 let (mut s1, _s1r, h1) = next_handshake(&listener);
2525 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2526 stream_start(&mut s1, 0, lineage);
2527 send_entries(&mut s1, &[1]);
2528 wait_for_ack(&mut s1, 1);
2529 send_entries(&mut s1, &[2]);
2533 let mut buf = Vec::new();
2534 encode_chain_check(2, &[0xEE; 32], &mut buf);
2535 s1.write_all(&buf).expect("ChainCheck");
2536 send_entries(&mut s1, &[3]);
2537 send_entries(&mut s1, &[5]);
2538
2539 let (mut s2, _s2r, h2) = next_handshake(&listener);
2540 assert_eq!(
2541 h2.last_sequence, 1,
2542 "the first dial after the journal died claims what is on disk"
2543 );
2544 stream_start(&mut s2, h2.last_sequence, lineage);
2549 send_entries(&mut s2, &[2]);
2550 wait_for_ack(&mut s2, 2);
2551 stop(replica, &shutdown);
2552 }
2553
2554 #[test]
2559 fn a_reconnect_after_a_quiet_session_claims_the_recovered_journal() {
2560 let dir = tempfile::tempdir().expect("tempdir");
2561 let (primary, lineage) = primary_journal(dir.path());
2562 std::fs::copy(&primary, dir.path().join("replica.journal")).expect("copy");
2564 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2565 listener.set_nonblocking(true).expect("nonblocking");
2566 let shutdown = Arc::new(AtomicBool::new(false));
2567 let replica = spawn_replica(
2568 dir.path(),
2569 listener.local_addr().expect("addr"),
2570 Duration::ZERO,
2571 &shutdown,
2572 );
2573
2574 let (mut s1, s1r, h1) = next_handshake(&listener);
2575 assert_eq!(h1.last_sequence, 1, "recovered replica handshake");
2576 stream_start(&mut s1, 1, lineage);
2577 drop(s1);
2579 drop(s1r);
2580
2581 let (mut s2, _s2r, h2) = next_handshake(&listener);
2582 assert_eq!(
2583 (h2.last_sequence, h2.chain_hash),
2584 (h1.last_sequence, h1.chain_hash),
2585 "the reconnect must claim the journal the pipeline was built over"
2586 );
2587 stream_start(&mut s2, h2.last_sequence, lineage);
2589 stop(replica, &shutdown);
2590 }
2591
2592 #[test]
2599 fn a_damaged_entry_is_refused_and_fetched_again() {
2600 let dir = tempfile::tempdir().expect("tempdir");
2601 let (_, lineage) = primary_journal(dir.path());
2602 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2603 listener.set_nonblocking(true).expect("nonblocking");
2604 let shutdown = Arc::new(AtomicBool::new(false));
2605 let replica = spawn_replica(
2606 dir.path(),
2607 listener.local_addr().expect("addr"),
2608 Duration::ZERO,
2609 &shutdown,
2610 );
2611
2612 let (mut s1, _s1r, h1) = next_handshake(&listener);
2613 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2614 stream_start(&mut s1, 0, lineage);
2615 send_entries(&mut s1, &[1]);
2616 wait_for_ack(&mut s1, 1);
2617
2618 let mut buf = Vec::new();
2621 melin_transport_core::replication_wire::encode_input_batch(&[entry(2)], &mut buf)
2622 .expect("encode InputBatch");
2623 let payload_at = buf.len() - melin_journal::codec::CRC_SIZE - 8;
2624 buf[payload_at] ^= 0x01;
2625 s1.write_all(&buf).expect("damaged InputBatch");
2626
2627 loop {
2630 let mut len_buf = [0u8; 4];
2631 if std::io::Read::read_exact(&mut s1, &mut len_buf).is_err() {
2632 break;
2633 }
2634 let mut payload = vec![0u8; u32::from_le_bytes(len_buf) as usize];
2635 if std::io::Read::read_exact(&mut s1, &mut payload).is_err() {
2636 break;
2637 }
2638 if let Ok(ReplicaMessage::Ack(a)) = decode_replica_message(&payload) {
2639 assert!(
2640 a.in_memory_sequence < 2 && a.acked_sequence < 2,
2641 "the damaged entry was acknowledged: {a:?}"
2642 );
2643 }
2644 }
2645
2646 let (mut s2, _s2r, h2) = next_handshake(&listener);
2647 assert_eq!(
2648 h2.last_sequence, 1,
2649 "the reconnect must claim only the entries that passed the check"
2650 );
2651 stream_start(&mut s2, 1, lineage);
2652 send_entries(&mut s2, &[2]);
2653 wait_for_ack(&mut s2, 2);
2654 stop(replica, &shutdown);
2655
2656 let mut reader =
2657 melin_journal::JournalReader::<EvtAdd>::open(&dir.path().join("replica.journal"))
2658 .expect("open replica journal");
2659 let mut journaled = Vec::new();
2660 while let Some(e) = reader.next_entry().expect("read entry") {
2661 if let JournalEvent::App(EvtAdd(v)) = e.event {
2662 journaled.push(v);
2663 }
2664 }
2665 assert_eq!(journaled, vec![1, 2], "only the intact entry is journaled");
2666 }
2667
2668 fn with_genesis(
2671 lineage: melin_journal::FileHeaderInfo,
2672 genesis_entries: Option<u64>,
2673 ) -> melin_journal::FileHeaderInfo {
2674 melin_journal::FileHeaderInfo {
2675 genesis_entries,
2676 ..lineage
2677 }
2678 }
2679
2680 fn replica_genesis(dir: &std::path::Path) -> Option<u64> {
2681 melin_journal::segment::read_header_info(&dir.join("replica.journal"))
2682 .expect("replica journal header")
2683 .genesis_entries
2684 }
2685
2686 #[test]
2691 fn a_replica_learns_the_genesis_length_and_keeps_it_across_restarts() {
2692 let dir = tempfile::tempdir().expect("tempdir");
2693 let (_, base) = primary_journal(dir.path());
2694 let lineage = with_genesis(base, Some(3));
2695 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2696 listener.set_nonblocking(true).expect("nonblocking");
2697 let addr = listener.local_addr().expect("addr");
2698
2699 let shutdown = Arc::new(AtomicBool::new(false));
2700 let replica = spawn_replica(dir.path(), addr, Duration::ZERO, &shutdown);
2701 let (mut s1, _s1r, h1) = next_handshake(&listener);
2702 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2703 stream_start(&mut s1, 0, lineage);
2704 send_entries(&mut s1, &[1, 2]);
2705 wait_for_ack(&mut s1, 2);
2706 assert_eq!(replica_genesis(dir.path()), Some(3), "learned at creation");
2707 stop(replica, &shutdown);
2708
2709 let shutdown = Arc::new(AtomicBool::new(false));
2710 let replica = spawn_replica(dir.path(), addr, Duration::ZERO, &shutdown);
2711 let (mut s2, _s2r, h2) = next_handshake(&listener);
2712 assert_eq!(h2.last_sequence, 2, "restarted replica handshake");
2713 stream_start(&mut s2, 2, lineage);
2714 send_entries(&mut s2, &[3]);
2715 wait_for_ack(&mut s2, 3);
2716 stop(replica, &shutdown);
2717 assert_eq!(
2718 replica_genesis(dir.path()),
2719 Some(3),
2720 "kept across a restart"
2721 );
2722 }
2723
2724 #[test]
2731 fn an_empty_old_format_replica_journal_learns_the_genesis_length() {
2732 let dir = tempfile::tempdir().expect("tempdir");
2733 let (_, base) = primary_journal(dir.path());
2734 assert_eq!(base.starting_sequence, 1);
2735 drop(
2736 melin_journal::BufferedWriter::<EvtAdd>::create_continuing(
2737 &dir.path().join("replica.journal"),
2738 1,
2739 base.anchor_hash,
2740 None,
2741 )
2742 .expect("old-format empty replica journal"),
2743 );
2744 assert_eq!(replica_genesis(dir.path()), None);
2745 let lineage = with_genesis(base, Some(3));
2746 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2747 listener.set_nonblocking(true).expect("nonblocking");
2748 let shutdown = Arc::new(AtomicBool::new(false));
2749 let replica = spawn_replica(
2750 dir.path(),
2751 listener.local_addr().expect("addr"),
2752 Duration::ZERO,
2753 &shutdown,
2754 );
2755
2756 let (mut s1, _s1r, h1) = next_handshake(&listener);
2757 assert_eq!(h1.last_sequence, 0, "empty replica handshake");
2758 stream_start(&mut s1, 0, lineage);
2759 send_entries(&mut s1, &[1, 2]);
2760 wait_for_ack(&mut s1, 2);
2761 stop(replica, &shutdown);
2762 assert_eq!(replica_genesis(dir.path()), Some(3));
2763 }
2764
2765 #[test]
2769 fn a_primary_advertising_another_genesis_length_is_refused() {
2770 let dir = tempfile::tempdir().expect("tempdir");
2771 let (primary, lineage) = primary_journal(dir.path());
2772 assert_eq!(lineage.genesis_entries, Some(0));
2773 std::fs::copy(&primary, dir.path().join("replica.journal")).expect("copy");
2776 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2777 listener.set_nonblocking(true).expect("nonblocking");
2778 let shutdown = Arc::new(AtomicBool::new(false));
2779 let replica = spawn_replica(
2780 dir.path(),
2781 listener.local_addr().expect("addr"),
2782 Duration::ZERO,
2783 &shutdown,
2784 );
2785
2786 let (mut s1, _s1r, h1) = next_handshake(&listener);
2787 assert_eq!(h1.last_sequence, 1);
2788 stream_start(&mut s1, 1, with_genesis(lineage, Some(4)));
2789 let result = replica.join().expect("replica thread panicked");
2790 let err = result.expect_err("the replica must refuse the lineage");
2791 assert!(err.contains("refusing to follow the primary"), "{err}");
2792 assert_eq!(replica_genesis(dir.path()), Some(0), "never overwritten");
2793 }
2794
2795 #[test]
2802 fn a_replica_promoted_mid_genesis_is_refused() {
2803 let dir = tempfile::tempdir().expect("tempdir");
2804 let (_, base) = primary_journal(dir.path());
2805 let lineage = with_genesis(base, Some(3));
2806 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2807 listener.set_nonblocking(true).expect("nonblocking");
2808 let addr = listener.local_addr().expect("addr");
2809 let shutdown = Arc::new(AtomicBool::new(false));
2810 let control = crate::replication::ReplicaControlPlane::new();
2811 let replica = {
2812 let journal = dir.path().join("replica.journal");
2813 let snapshot = dir.path().join("replica.snapshot");
2814 let shutdown = Arc::clone(&shutdown);
2815 let control = control.clone();
2816 std::thread::spawn(move || -> Result<Option<String>, String> {
2817 run_receiver::<App>(
2818 addr,
2819 &journal,
2820 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
2821 &shutdown,
2822 &control,
2823 3_600_000,
2824 snapshot,
2825 crate::layout::PipelineCores::unpinned(),
2826 melin_journal::StagingMode::ZeroFill,
2827 Duration::ZERO,
2828 64,
2829 Arc::new(melin_transport_core::fence::FenceState::new(0)),
2830 &(),
2831 )
2832 .map(|promoted| {
2835 promoted.map(
2836 |(_, writer)| match crate::server::check_promotable(&writer) {
2837 Ok(()) => "promotable".to_string(),
2838 Err(e) => e.to_string(),
2839 },
2840 )
2841 })
2842 .map_err(|e| e.to_string())
2843 })
2844 };
2845
2846 let (mut s1, s1r, h1) = next_handshake(&listener);
2847 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2848 stream_start(&mut s1, 0, lineage);
2849 send_entries(&mut s1, &[1, 2]);
2850 wait_for_ack(&mut s1, 2);
2851 drop(s1);
2853 drop(s1r);
2854 assert!(
2855 control
2856 .promote
2857 .request(crate::promotion::PromotionRequest::MANUAL),
2858 "promotion filed"
2859 );
2860
2861 let verdict = replica
2862 .join()
2863 .expect("replica thread panicked")
2864 .expect("the receiver hands its state over")
2865 .expect("promoted, not shut down");
2866 assert!(verdict.contains("refusing promotion"), "{verdict}");
2867 assert!(verdict.contains("holds 2 entries"), "{verdict}");
2868 }
2869 }
2870
2871 #[cfg(all(feature = "hash-chain", not(feature = "no-persist")))]
2878 mod rotation_adoption {
2879 use super::super::super::auth::authenticate_replica;
2880 use super::scripted::*;
2881 use super::*;
2882 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
2883 use melin_transport_core::replication::catchup::lineage_origin;
2884 use melin_transport_core::replication::protocol::{encode_rotate, encode_stream_start};
2885 use std::sync::Arc;
2886 use std::sync::atomic::AtomicBool;
2887
2888 fn payloads(path: &std::path::Path) -> Vec<u64> {
2890 let mut reader = melin_journal::JournalReader::<EvtAdd>::open(path).expect("open");
2891 let mut out = Vec::new();
2892 while let Some(entry) = reader.next_entry().expect("read entry") {
2893 if let JournalEvent::App(EvtAdd(v)) = entry.event {
2894 out.push(v);
2895 }
2896 }
2897 out
2898 }
2899
2900 fn boundary_adoption_rotates_and_streams_on(mode: melin_journal::StagingMode) {
2911 let _prealloc_guard =
2916 melin_journal::test_utils::PreallocOverrideGuard::new(1024 * 1024);
2917 let dir = tempfile::tempdir().expect("tempdir");
2918
2919 let primary_journal = dir.path().join("primary.journal");
2925 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
2926 for v in 1..=2u64 {
2927 w.batch_append_with_ts(&JournalEvent::App(EvtAdd(v)), v, 0)
2928 .expect("append");
2929 }
2930 w.flush_batch_sync().expect("flush");
2931 let chain_at_2 = w.chain_hash().expect("chain");
2932 drop(w);
2933 let origin = lineage_origin(&primary_journal).expect("lineage");
2934 let (lineage_start, lineage_anchor, lineage_genesis) = (
2935 origin.starting_sequence,
2936 origin.anchor_hash,
2937 origin.genesis_entries,
2938 );
2939
2940 let authorized_keys = replica_auth();
2941 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2942 listener.set_nonblocking(true).expect("nonblocking");
2943 let addr = listener.local_addr().expect("addr");
2944
2945 let replica_journal = dir.path().join("replica.journal");
2946 let shutdown = Arc::new(AtomicBool::new(false));
2947 let control = crate::replication::ReplicaControlPlane::new();
2948 let replica = {
2949 let journal = replica_journal.clone();
2950 let snapshot = dir.path().join("replica.snapshot");
2951 let shutdown = Arc::clone(&shutdown);
2952 let control = control.clone();
2953 std::thread::spawn(move || -> Result<bool, String> {
2954 run_receiver::<App>(
2955 addr,
2956 &journal,
2957 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
2958 &shutdown,
2959 &control,
2960 3_600_000,
2961 snapshot,
2962 crate::layout::PipelineCores::unpinned(),
2963 mode,
2964 Duration::ZERO,
2965 64,
2966 Arc::new(melin_transport_core::fence::FenceState::new(0)),
2967 &(),
2968 )
2969 .map(|state| state.is_none())
2971 .map_err(|e| e.to_string())
2972 })
2973 };
2974
2975 let mut s1 = accept_within(&listener, 30);
2976 let mut s1r = s1.try_clone().expect("clone");
2977 authenticate_replica(&mut s1r, &authorized_keys).expect("auth");
2978 match read_replica_msg(&mut s1) {
2979 ReplicaMessage::Handshake(h) => {
2980 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
2981 }
2982 other => panic!("expected Handshake, got {other:?}"),
2983 }
2984 let mut buf = Vec::new();
2985 encode_stream_start(
2986 0,
2987 lineage_start,
2988 lineage_anchor,
2989 lineage_genesis,
2990 0,
2991 &mut buf,
2992 );
2993 s1.write_all(&buf).expect("StreamStart");
2994
2995 let send_events = |s: &mut TcpStream, range: std::ops::RangeInclusive<u64>| {
2996 let slots: Vec<InputSlot<EvtAdd>> = range
2997 .map(|v| InputSlot {
2998 connection_id: 0,
2999 key_hash: 0,
3000 sequence: v,
3001 timestamp_ns: v,
3002 event: JournalEvent::App(EvtAdd(v)),
3003 publish_ts: Default::default(),
3005 recv_ts: Default::default(),
3006 })
3007 .collect();
3008 let mut buf = Vec::new();
3009 melin_transport_core::replication_wire::encode_input_batch(&slots, &mut buf)
3010 .expect("encode InputBatch");
3011 s.write_all(&buf).expect("InputBatch");
3012 };
3013
3014 send_events(&mut s1, 1..=2);
3015 wait_for_ack(&mut s1, 2);
3016
3017 let staging = {
3024 let mut s = replica_journal.clone().into_os_string();
3028 s.push(".next-staging");
3029 std::path::PathBuf::from(s)
3030 };
3031 let staged_len: u64 = (1 + 8) * 1024 * 1024;
3032 let deadline = Instant::now() + Duration::from_secs(10);
3033 loop {
3034 if std::fs::metadata(&staging).is_ok_and(|m| m.len() == staged_len) {
3035 break;
3036 }
3037 assert!(
3038 Instant::now() < deadline,
3039 "preparer produced no full-size staged segment in {mode:?} mode"
3040 );
3041 std::thread::sleep(Duration::from_millis(10));
3042 }
3043 std::thread::sleep(Duration::from_millis(300));
3051
3052 buf.clear();
3055 encode_rotate(2, &chain_at_2, &mut buf);
3056 s1.write_all(&buf).expect("Rotate");
3057
3058 send_events(&mut s1, 3..=4);
3062 wait_for_ack(&mut s1, 4);
3063
3064 shutdown.store(true, Ordering::Relaxed);
3065 let result = replica.join().expect("replica thread panicked");
3066 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
3067
3068 let archived = dir.path().join("replica.journal.000001");
3072 assert!(archived.exists(), "sealed segment must be archived");
3073 assert_eq!(payloads(&archived), vec![1, 2]);
3074 assert_eq!(payloads(&replica_journal), vec![3, 4]);
3075 }
3076
3077 #[test]
3078 fn boundary_adoption_zero_fill() {
3079 boundary_adoption_rotates_and_streams_on(melin_journal::StagingMode::ZeroFill);
3080 }
3081
3082 #[test]
3083 fn boundary_adoption_allocate() {
3084 boundary_adoption_rotates_and_streams_on(melin_journal::StagingMode::Allocate);
3085 }
3086 }
3087}