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_ack_policy: _,
582 pipeline_healthy,
583 } = control;
584 let (mut app, mut journal_writer, mut last_sequence, mut chain_hash) =
588 recover_replica_state::<A, BufferedWriter<A::Event>>(
589 journal_path,
590 &snapshot_path,
591 &fence_state,
592 sizing,
593 )?;
594 journal_tip.advance(melin_transport_core::WireSeq::new(last_sequence));
600 tip_ready.store(true, Ordering::Release);
601
602 let mut backoff = std::time::Duration::from_secs(1);
603
604 let mut divergence_resyncs: u32 = 0;
607
608 let mut send_buf = Vec::with_capacity(64);
609 let mut pipeline: Option<ReplicaPipelineHandles<A, BufferedWriter<A::Event>>> = None;
610
611 loop {
613 primary_link_up.store(false, Ordering::Release);
616
617 if let Some(p) = pipeline.as_ref() {
618 match p
628 .settled_resume_point(|| shutdown.load(Ordering::Relaxed) || promote.is_requested())
629 {
630 ResumePoint::Settled(fsync_state) => {
631 last_sequence = fsync_state.journal_seq.get();
632 chain_hash = fsync_state.chain_hash;
633 }
634 ResumePoint::Interrupted => {}
636 ResumePoint::JournalFailed => {
639 let after = handle_session_exit(
640 journal_failed_while_disconnected(),
641 &mut pipeline,
642 &mut divergence_resyncs,
643 &mut backoff,
644 last_sequence,
645 journal_path,
646 &snapshot_path,
647 &fence_state,
648 shutdown,
649 promote,
650 || {},
652 sizing,
653 );
654 if let Some(r) = after.adopt(
657 &mut app,
658 &mut journal_writer,
659 &mut last_sequence,
660 &mut chain_hash,
661 ) {
662 return r;
663 }
664 continue;
665 }
666 }
667 }
668
669 if shutdown.load(Ordering::Relaxed) {
674 if let Some(p) = pipeline.take() {
675 let _ = teardown_replica_pipeline::<A, BufferedWriter<A::Event>>(p);
676 }
677 return Ok(None);
678 }
679 if promote.is_requested() {
680 info!("promotion triggered while disconnected");
681 return take_pipeline_for_promotion(&mut pipeline, &mut app, &mut journal_writer);
682 }
683
684 info!(primary = %primary_addr, "connecting to primary as replica");
686
687 let stream = match TcpStream::connect(primary_addr) {
688 Ok(s) => s,
689 Err(e) => {
690 warn!(
691 error = %e,
692 backoff_secs = backoff.as_secs(),
693 "failed to connect to primary — retrying"
694 );
695 sleep_then_double_backoff(&mut backoff, shutdown, promote);
696 continue;
697 }
698 };
699 if let Err(e) = stream.set_nodelay(true) {
700 warn!(error = %e, "failed to set TCP_NODELAY on replica receive socket");
701 }
702 {
703 use std::os::unix::io::AsRawFd;
704 let busy_poll: libc::c_int = 50;
705 let rc = unsafe {
706 libc::setsockopt(
707 stream.as_raw_fd(),
708 libc::SOL_SOCKET,
709 libc::SO_BUSY_POLL,
710 &busy_poll as *const libc::c_int as *const libc::c_void,
711 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
712 )
713 };
714 if rc != 0 {
715 let err = std::io::Error::last_os_error();
716 warn!(error = %err, "failed to set SO_BUSY_POLL on replica receive socket");
717 }
718 }
719 stream.set_read_timeout(Some(std::time::Duration::from_secs(5)))?;
720
721 let mut reader = stream.try_clone()?;
722 let mut tcp_writer = stream;
723
724 if let Err(e) = authenticate_with_primary(&mut reader, signing_key) {
727 if matches!(
734 e.kind(),
735 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
736 ) {
737 warn!(error = %e, "no auth challenge from primary (not serving yet?) — retrying");
738 } else {
739 warn!(error = %e, "authentication failed — retrying");
740 }
741 drop(reader);
746 drop(tcp_writer);
747 sleep_then_double_backoff(&mut backoff, shutdown, promote);
750 continue;
751 }
752 info!("authenticated with primary");
753 primary_link_up.store(true, Ordering::Release);
756
757 let handshake = Handshake {
762 last_sequence,
763 chain_hash,
764 epoch: fence_state.epoch(),
765 };
766 send_buf.clear();
767 encode_handshake(&handshake, &mut send_buf);
768 tcp_writer.write_all(&send_buf)?;
769 tcp_writer.flush()?;
770 send_buf.clear();
771
772 let response_frame = read_frame(&mut reader, MAX_CONTROL_FRAME)?;
779 let response = decode_primary_message(&response_frame)?;
780 let (stream_lineage, session_start) = match response {
781 PrimaryMessage::StreamStart {
782 start_sequence,
783 segment_start_sequence,
784 anchor_hash,
785 genesis_entries,
786 epoch,
787 ack_policy,
788 } => match super::accept_stream_start(
789 super::StreamStart {
790 start_sequence,
791 lineage: (segment_start_sequence, anchor_hash, genesis_entries),
792 epoch,
793 ack_policy,
794 },
795 "tcp",
796 &pipeline,
797 &mut journal_writer,
798 &fence_state,
799 control,
800 )? {
801 StreamStartVerdict::Follow(lineage) => (lineage, last_sequence),
802 StreamStartVerdict::StalePrimary => {
805 drop(reader);
808 drop(tcp_writer);
809 sleep_then_double_backoff(&mut backoff, shutdown, promote);
810 continue;
811 }
812 },
813 ref resync @ (PrimaryMessage::NeedSnapshot | PrimaryMessage::HashMismatch) => {
814 let divergent = matches!(resync, PrimaryMessage::HashMismatch);
815 let decision = handle_resync_verdict(
819 divergent,
820 &mut TcpFrameSource {
821 reader: &mut reader,
822 },
823 &mut pipeline,
824 &mut app,
825 &mut journal_writer,
826 journal_path,
827 &snapshot_path,
828 &fence_state,
829 control,
830 &mut last_sequence,
831 &mut chain_hash,
832 )
833 .map_err(|e| -> Box<dyn std::error::Error> { e })?;
834 match decision {
835 ResyncDecision::Ready {
836 segment_start_sequence,
837 anchor_hash,
838 genesis_entries,
839 resume_sequence,
840 } => (
841 (segment_start_sequence, anchor_hash, genesis_entries),
842 resume_sequence,
843 ),
844 ResyncDecision::Retry => {
845 drop(reader);
848 drop(tcp_writer);
849 sleep_then_double_backoff(&mut backoff, shutdown, promote);
850 continue;
851 }
852 }
853 }
854 _ => {
855 return Err(format!("unexpected response: {response:?}").into());
856 }
857 };
858
859 if pipeline.is_none() && journal_writer.is_none() {
870 let (lineage_start, lineage_anchor, lineage_genesis) = stream_lineage;
871 let writer = BufferedWriter::create_continuing(
872 journal_path,
873 lineage_start,
874 lineage_anchor,
875 lineage_genesis,
876 )?;
877 app = Some(A::default());
878 journal_writer = Some(writer);
879 }
880
881 if pipeline.is_none() {
883 let cur_app = app.take().expect("application initialized");
884 let cur_writer = journal_writer.take().expect("journal_writer initialized");
885 pipeline = Some(build_replica_pipeline_with_threads::<A>(
886 cur_app,
887 cur_writer,
888 cores,
889 staging_mode,
890 snapshot_interval_ms,
891 snapshot_path.clone(),
892 group_commit_delay,
893 Arc::clone(&fence_state),
894 Arc::clone(pipeline_healthy),
895 sizing,
896 )?);
897 }
898
899 let result = {
901 let p = pipeline.as_mut().expect("pipeline must exist by here");
902 let input_producer = &mut p.input_producer;
903 let journal_cursor = p.journal_cursor.as_ref();
904 let stream_marks = &p.stream_marks;
905 let journal_failed = &p.journal_failed;
906 std::thread::scope(|s| {
907 let handle = std::thread::Builder::new()
908 .name("replica-receiver".into())
909 .spawn_scoped(s, || {
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 .expect("spawn replica-receiver thread");
942 handle.join().expect("replica-receiver thread panicked")
943 })
944 };
945
946 primary_link_up.store(false, Ordering::Release);
947 let after = handle_session_exit(
948 result,
949 &mut pipeline,
950 &mut divergence_resyncs,
951 &mut backoff,
952 last_sequence,
953 journal_path,
954 &snapshot_path,
955 &fence_state,
956 shutdown,
957 promote,
958 || {},
962 sizing,
963 );
964 if let Some(r) = after.adopt(
965 &mut app,
966 &mut journal_writer,
967 &mut last_sequence,
968 &mut chain_hash,
969 ) {
970 return r;
971 }
972 }
973}
974
975#[cfg(test)]
980mod tests {
981 use super::*;
982 use crate::uring_teardown::DRAIN_TIMEOUT;
983 use std::io::Read;
984 use std::net::TcpListener;
985 use std::time::{Duration, Instant};
986
987 use melin_transport_core::replication::protocol::{ReplicaMessage, decode_replica_message};
988 use melin_wire_protocol::framing::split_frame_limited;
989
990 fn socket_pair() -> (TcpStream, TcpStream) {
992 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
993 let addr = listener.local_addr().unwrap();
994 let client = TcpStream::connect(addr).unwrap();
995 let (server, _) = listener.accept().unwrap();
996 (client, server)
997 }
998
999 fn ack(seq: u64) -> Ack {
1000 Ack {
1001 acked_sequence: seq,
1002 in_memory_sequence: seq,
1003 }
1004 }
1005
1006 fn flush_acks(transport: &mut UringTransport) {
1008 let mut recv_buf = Vec::new();
1009 let deadline = Instant::now() + Duration::from_secs(5);
1010 while transport.ack_in_flight() {
1011 transport.poll_recv(&mut recv_buf).unwrap();
1012 assert!(Instant::now() < deadline, "ack send never completed");
1013 std::hint::spin_loop();
1014 }
1015 }
1016
1017 fn read_acks(peer: &mut TcpStream, expect: usize) -> Vec<Ack> {
1019 peer.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
1020 let mut buf = Vec::new();
1021 let mut chunk = [0u8; 256];
1022 let mut acks = Vec::new();
1023 while acks.len() < expect {
1024 let n = peer.read(&mut chunk).expect("peer read");
1025 assert!(n > 0, "peer closed before all acks arrived");
1026 buf.extend_from_slice(&chunk[..n]);
1027 while let Some(frame) = split_frame_limited(&buf, MAX_CONTROL_FRAME).unwrap() {
1029 match decode_replica_message(frame.payload).expect("decodable frame") {
1030 ReplicaMessage::Ack(a) => acks.push(a),
1031 other => panic!("expected Ack frame, got {other:?}"),
1032 }
1033 let consumed = frame.consumed();
1034 buf.drain(..consumed);
1035 }
1036 }
1037 assert!(buf.is_empty(), "trailing bytes after expected acks");
1038 acks
1039 }
1040
1041 #[test]
1042 fn coalesces_to_latest_ack_while_send_in_flight() {
1043 let (client, mut peer) = socket_pair();
1044 let mut transport = UringTransport::new(&client).unwrap();
1045
1046 assert!(transport.send_ack(&ack(1)).unwrap());
1051 assert!(transport.ack_in_flight());
1052 assert!(transport.send_ack(&ack(2)).unwrap());
1053 assert!(transport.send_ack(&ack(3)).unwrap());
1054
1055 flush_acks(&mut transport);
1056
1057 let acks = read_acks(&mut peer, 2);
1059 let seqs: Vec<u64> = acks.iter().map(|a| a.acked_sequence).collect();
1060 assert_eq!(seqs, vec![1, 3]);
1061 }
1062
1063 #[test]
1064 fn sequential_acks_all_reach_the_wire_in_order() {
1065 let (client, mut peer) = socket_pair();
1066 let mut transport = UringTransport::new(&client).unwrap();
1067
1068 for seq in 1..=3 {
1069 assert!(transport.send_ack(&ack(seq)).unwrap());
1070 flush_acks(&mut transport);
1071 }
1072
1073 let acks = read_acks(&mut peer, 3);
1074 let seqs: Vec<u64> = acks.iter().map(|a| a.acked_sequence).collect();
1075 assert_eq!(seqs, vec![1, 2, 3]);
1076 }
1077
1078 #[test]
1086 fn drop_quiesces_the_multishot_recv() {
1087 let (client, _peer) = socket_pair();
1088 let mut transport = UringTransport::new(&client).unwrap();
1089 let mut recv_buf = Vec::new();
1092 transport.poll_recv(&mut recv_buf).unwrap();
1093 assert!(transport.multishot_active);
1094
1095 let started = Instant::now();
1096 drop(transport);
1097 let elapsed = started.elapsed();
1098 assert!(
1099 elapsed < DRAIN_TIMEOUT / 4,
1100 "teardown took {elapsed:?}, close to the {DRAIN_TIMEOUT:?} deadline"
1101 );
1102 }
1103
1104 #[test]
1111 fn abandoned_cqes_still_settle_kernel_ownership() {
1112 let (client, _peer) = socket_pair();
1113 let mut transport = UringTransport::new(&client).unwrap();
1114 let mut recv_buf = Vec::new();
1115 transport.poll_recv(&mut recv_buf).unwrap();
1116
1117 transport.ack_in_flight = true;
1120 transport.settle_unprocessed(&[(TOKEN_SEND, 13, 0)]);
1121 assert!(
1122 !transport.ack_in_flight,
1123 "an abandoned SEND completion must release `ack_buf`"
1124 );
1125
1126 transport.settle_unprocessed(&[(TOKEN_RECV, 128, IORING_CQE_F_MORE)]);
1129 assert!(
1130 transport.multishot_active,
1131 "F_MORE must leave the multishot armed"
1132 );
1133 transport.settle_unprocessed(&[(TOKEN_RECV, 0, 0)]);
1135 assert!(!transport.multishot_active);
1136
1137 transport.multishot_active = true;
1141 }
1142
1143 #[test]
1147 fn drop_quiesces_a_pending_ack_send() {
1148 let (client, _peer) = socket_pair();
1149 let mut transport = UringTransport::new(&client).unwrap();
1150 assert!(transport.send_ack(&ack(7)).unwrap());
1151 assert!(transport.ack_in_flight());
1152
1153 let started = Instant::now();
1154 drop(transport);
1155 let elapsed = started.elapsed();
1156 assert!(
1157 elapsed < DRAIN_TIMEOUT / 4,
1158 "teardown took {elapsed:?}, close to the {DRAIN_TIMEOUT:?} deadline"
1159 );
1160 }
1161
1162 #[cfg(not(feature = "no-persist"))]
1167 mod scripted {
1168 use super::*;
1169 use melin_app::{AppEvent, Application, ApplyCtx, CodecError, QueryCtx, RejectReason};
1170
1171 pub(super) use melin_transport_core::pipeline::InputSlot;
1174
1175 #[derive(Debug, Clone, Copy, PartialEq)]
1176 pub(super) struct EvtAdd(pub(super) u64);
1177
1178 impl AppEvent for EvtAdd {
1179 const MAX_ENCODED_SIZE: usize = 8;
1180
1181 fn encoded_size(&self) -> usize {
1182 8
1183 }
1184 fn encode(&self, buf: &mut [u8]) -> usize {
1185 buf[..8].copy_from_slice(&self.0.to_le_bytes());
1186 8
1187 }
1188 fn decode(buf: &[u8]) -> Result<Self, CodecError> {
1189 if buf.len() < 8 {
1190 return Err(CodecError::Truncated);
1191 }
1192 Ok(EvtAdd(u64::from_le_bytes(buf[..8].try_into().expect("8"))))
1193 }
1194 fn is_query(&self) -> bool {
1195 false
1196 }
1197 }
1198
1199 #[derive(Debug, Clone, Copy)]
1200 pub(super) struct Rpt;
1201
1202 #[derive(Default)]
1203 pub(super) struct App;
1204
1205 impl Application for App {
1206 type Event = EvtAdd;
1207 type Report = Rpt;
1208 type QueryResponse = Rpt;
1209 type Sizing = ();
1210 const APP_VERSION: u16 = 1;
1211
1212 fn apply(&mut self, _e: EvtAdd, _ctx: &ApplyCtx, _out: &mut Vec<Rpt>) {}
1213 fn query(&self, _e: EvtAdd, _ctx: &QueryCtx) -> Option<Rpt> {
1214 None
1215 }
1216 fn tick(&mut self, _now_ns: u64, _out: &mut Vec<Rpt>) {}
1217 fn build_reject(_e: &EvtAdd, _r: RejectReason) -> Rpt {
1218 Rpt
1219 }
1220 fn snapshot<W: std::io::Write>(&self, _w: &mut W) -> std::io::Result<()> {
1221 Ok(())
1222 }
1223 fn restore<R: std::io::Read>(_r: &mut R) -> std::io::Result<Self> {
1224 Ok(App)
1225 }
1226 }
1227
1228 pub(super) const REPLICA_KEY: [u8; 32] = [0xFC; 32];
1231
1232 pub(super) fn replica_auth() -> melin_app::auth::AuthorizedKeys {
1235 let key = ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY);
1236 let pub_b64 = base64::Engine::encode(
1237 &base64::engine::general_purpose::STANDARD,
1238 key.verifying_key().to_bytes(),
1239 );
1240 melin_app::auth::AuthorizedKeys::parse::<melin_app::auth::NoRoles>(&format!(
1241 "replication {pub_b64} test-replica\n"
1242 ))
1243 .expect("parse keys")
1244 }
1245
1246 pub(super) fn read_replica_msg(stream: &mut TcpStream) -> ReplicaMessage {
1248 let mut len_buf = [0u8; 4];
1249 stream.read_exact(&mut len_buf).expect("frame length");
1250 let len = u32::from_le_bytes(len_buf) as usize;
1251 let mut payload = vec![0u8; len];
1252 stream.read_exact(&mut payload).expect("frame payload");
1253 decode_replica_message(&payload).expect("decodable replica frame")
1254 }
1255
1256 pub(super) fn wait_for_ack(stream: &mut TcpStream, seq: u64) {
1258 loop {
1259 if let ReplicaMessage::Ack(a) = read_replica_msg(stream)
1260 && a.acked_sequence >= seq
1261 {
1262 return;
1263 }
1264 }
1265 }
1266
1267 pub(super) fn accept_within(listener: &std::net::TcpListener, secs: u64) -> TcpStream {
1270 let deadline = Instant::now() + Duration::from_secs(secs);
1271 loop {
1272 match listener.accept() {
1273 Ok((s, _)) => {
1274 s.set_read_timeout(Some(Duration::from_secs(10)))
1275 .expect("read timeout");
1276 return s;
1277 }
1278 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
1279 assert!(Instant::now() < deadline, "replica never connected");
1280 std::thread::sleep(Duration::from_millis(10));
1281 }
1282 Err(e) => panic!("accept failed: {e}"),
1283 }
1284 }
1285 }
1286 }
1287
1288 #[cfg(all(feature = "hash-chain", not(feature = "no-persist")))]
1294 mod divergence_resync {
1295 use super::super::super::auth::authenticate_replica;
1296 use super::scripted::*;
1297 use super::*;
1298 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
1299 use melin_transport_core::cursors::WireSeq;
1300 use melin_transport_core::replication::catchup::{lineage_origin, snapshot_transfer_with};
1301 use melin_transport_core::replication::protocol::{
1302 encode_hash_mismatch, encode_rotate, encode_stream_start,
1303 };
1304 use std::sync::Arc;
1305 use std::sync::atomic::AtomicBool;
1306
1307 #[test]
1316 fn mid_stream_divergence_resyncs_in_process() {
1317 let dir = tempfile::tempdir().expect("tempdir");
1318
1319 let primary_journal = dir.path().join("primary.journal");
1323 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1324 let mut chain_at_4 = [0u8; 32];
1325 for v in 1..=5u64 {
1326 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1327 if v == 2 {
1328 w.rotate_segment().expect("rotate");
1329 }
1330 if v == 4 {
1331 chain_at_4 = w.chain_hash().expect("chain");
1332 }
1333 }
1334 drop(w);
1335 melin_transport_core::snapshot::save::<App>(
1336 &App,
1337 WireSeq::new(4),
1338 chain_at_4,
1339 0,
1340 Some(0),
1341 &primary_journal.with_extension("snapshot"),
1342 )
1343 .expect("save snapshot");
1344 let origin = lineage_origin(&primary_journal).expect("lineage");
1345 let (lineage_start, lineage_anchor, lineage_genesis) = (
1346 origin.starting_sequence,
1347 origin.anchor_hash,
1348 origin.genesis_entries,
1349 );
1350
1351 let authorized_keys = replica_auth();
1353
1354 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1355 listener.set_nonblocking(true).expect("nonblocking");
1356 let addr = listener.local_addr().expect("addr");
1357
1358 let replica_journal = dir.path().join("replica.journal");
1360 let replica_snapshot = dir.path().join("replica.snapshot");
1361 let shutdown = Arc::new(AtomicBool::new(false));
1362 let control = crate::replication::ReplicaControlPlane::new();
1363 let cores = crate::layout::PipelineCores::unpinned();
1364 let replica = {
1365 let journal = replica_journal.clone();
1366 let shutdown = Arc::clone(&shutdown);
1367 let control = control.clone();
1368 std::thread::spawn(move || -> Result<bool, String> {
1369 run_receiver::<App>(
1370 addr,
1371 &journal,
1372 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1373 &shutdown,
1374 &control,
1375 3_600_000,
1376 replica_snapshot,
1377 cores,
1378 melin_journal::StagingMode::ZeroFill,
1379 Duration::ZERO,
1380 64,
1381 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1382 &(),
1383 )
1384 .map(|state| state.is_none())
1386 .map_err(|e| e.to_string())
1387 })
1388 };
1389
1390 let mut s1 = accept_within(&listener, 30);
1393 let mut s1r = s1.try_clone().expect("clone");
1394 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1395 match read_replica_msg(&mut s1) {
1396 ReplicaMessage::Handshake(h) => {
1397 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
1398 }
1399 other => panic!("expected Handshake, got {other:?}"),
1400 }
1401 let mut buf = Vec::new();
1402 encode_stream_start(
1403 0,
1404 lineage_start,
1405 lineage_anchor,
1406 lineage_genesis,
1407 0,
1408 1,
1409 &mut buf,
1410 );
1411 s1.write_all(&buf).expect("StreamStart 1");
1412 buf.clear();
1413 melin_transport_core::replication_wire::encode_input_batch(
1414 &[InputSlot::<EvtAdd> {
1415 connection_id: 0,
1416 key_hash: 0,
1417 sequence: 1,
1418 timestamp_ns: 1,
1419 event: JournalEvent::App(EvtAdd(1)),
1420 publish_ts: Default::default(),
1422 recv_ts: Default::default(),
1423 }],
1424 &mut buf,
1425 )
1426 .expect("encode InputBatch");
1427 s1.write_all(&buf).expect("InputBatch");
1428 wait_for_ack(&mut s1, 1);
1429
1430 buf.clear();
1433 encode_rotate(1, &[0xEE; 32], &mut buf);
1434 s1.write_all(&buf).expect("Rotate");
1435
1436 let mut s2 = accept_within(&listener, 30);
1439 let mut s2r = s2.try_clone().expect("clone");
1440 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1441 match read_replica_msg(&mut s2) {
1442 ReplicaMessage::Handshake(h) => assert_eq!(
1443 h.last_sequence, 1,
1444 "post-divergence handshake must carry the recovered journal position"
1445 ),
1446 other => panic!("expected Handshake, got {other:?}"),
1447 }
1448 assert!(
1454 !control.pipeline_healthy.load(Ordering::Acquire),
1455 "the divergence must have latched the health mirror false"
1456 );
1457 buf.clear();
1458 encode_hash_mismatch(&mut buf);
1459 s2.write_all(&buf).expect("HashMismatch");
1460 let transfer_shutdown = AtomicBool::new(false);
1461 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1462 s2.write_all(b)?;
1463 s2.flush()
1464 };
1465 let end = snapshot_transfer_with::<EvtAdd>(
1468 &primary_journal,
1469 &mut publish,
1470 &transfer_shutdown,
1471 1,
1472 )
1473 .expect("snapshot transfer");
1474 assert_eq!(
1475 end,
1476 melin_transport_core::replication::catchup::CatchUpResult::Ok(5)
1477 );
1478 let mut s2_acks = s2.try_clone().expect("clone");
1479 wait_for_ack(&mut s2_acks, 5);
1480
1481 assert!(
1484 control.pipeline_healthy.load(Ordering::Acquire),
1485 "the resync rebuild must reset the health mirror true"
1486 );
1487
1488 shutdown.store(true, Ordering::Relaxed);
1491 let result = replica.join().expect("replica thread panicked");
1492 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
1493
1494 assert!(
1497 dir.path().join("replica.journal.divergent.0").exists(),
1498 "divergent lineage must be archived"
1499 );
1500 assert!(replica_journal.exists(), "re-seeded live journal");
1501 }
1502
1503 #[test]
1517 fn resync_transfer_failure_rebuilds_clean_not_over_stale_writer() {
1518 let dir = tempfile::tempdir().expect("tempdir");
1519
1520 let primary_journal = dir.path().join("primary.journal");
1524 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1525 for v in 1..=2u64 {
1526 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1527 }
1528 drop(w);
1529 let origin = lineage_origin(&primary_journal).expect("lineage");
1530 let (lineage_start, lineage_anchor, lineage_genesis) = (
1531 origin.starting_sequence,
1532 origin.anchor_hash,
1533 origin.genesis_entries,
1534 );
1535
1536 let authorized_keys = replica_auth();
1537
1538 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1539 listener.set_nonblocking(true).expect("nonblocking");
1540 let addr = listener.local_addr().expect("addr");
1541
1542 let replica_journal = dir.path().join("replica.journal");
1543 let replica_snapshot = dir.path().join("replica.snapshot");
1544 let shutdown = Arc::new(AtomicBool::new(false));
1545 let control = crate::replication::ReplicaControlPlane::new();
1546 let cores = crate::layout::PipelineCores::unpinned();
1547 let replica = {
1548 let journal = replica_journal.clone();
1549 let shutdown = Arc::clone(&shutdown);
1550 let control = control.clone();
1551 std::thread::spawn(move || -> Result<bool, String> {
1552 run_receiver::<App>(
1553 addr,
1554 &journal,
1555 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1556 &shutdown,
1557 &control,
1558 3_600_000,
1559 replica_snapshot,
1560 cores,
1561 melin_journal::StagingMode::ZeroFill,
1562 Duration::ZERO,
1563 64,
1564 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1565 &(),
1566 )
1567 .map(|state| state.is_none())
1568 .map_err(|e| e.to_string())
1569 })
1570 };
1571
1572 let stream_one = |s: &mut TcpStream, buf: &mut Vec<u8>| {
1575 buf.clear();
1576 encode_stream_start(0, lineage_start, lineage_anchor, lineage_genesis, 0, 1, buf);
1577 s.write_all(buf).expect("StreamStart");
1578 buf.clear();
1579 melin_transport_core::replication_wire::encode_input_batch(
1580 &[InputSlot::<EvtAdd> {
1581 connection_id: 0,
1582 key_hash: 0,
1583 sequence: 1,
1584 timestamp_ns: 1,
1585 event: JournalEvent::App(EvtAdd(1)),
1586 publish_ts: Default::default(),
1587 recv_ts: Default::default(),
1588 }],
1589 buf,
1590 )
1591 .expect("encode InputBatch");
1592 s.write_all(buf).expect("InputBatch");
1593 };
1594
1595 let mut buf = Vec::new();
1600 let mut s1 = accept_within(&listener, 30);
1601 let mut s1r = s1.try_clone().expect("clone");
1602 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1603 match read_replica_msg(&mut s1) {
1604 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0, "fresh"),
1605 other => panic!("expected Handshake, got {other:?}"),
1606 }
1607 stream_one(&mut s1, &mut buf);
1608 wait_for_ack(&mut s1, 1);
1609 buf.clear();
1610 encode_rotate(1, &[0xEE; 32], &mut buf);
1611 s1.write_all(&buf).expect("Rotate");
1612
1613 let mut s2 = accept_within(&listener, 30);
1618 let mut s2r = s2.try_clone().expect("clone");
1619 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1620 match read_replica_msg(&mut s2) {
1621 ReplicaMessage::Handshake(h) => assert_eq!(
1622 h.last_sequence, 1,
1623 "reconnect carries the recovered forked position"
1624 ),
1625 other => panic!("expected Handshake, got {other:?}"),
1626 }
1627 buf.clear();
1628 encode_hash_mismatch(&mut buf);
1629 s2.write_all(&buf).expect("HashMismatch");
1630 s2.flush().expect("flush");
1631 drop(s2);
1634 drop(s2r);
1635
1636 let mut s3 = accept_within(&listener, 30);
1641 let mut s3r = s3.try_clone().expect("clone");
1642 authenticate_replica(&mut s3r, &authorized_keys).expect("auth 3");
1643 match read_replica_msg(&mut s3) {
1644 ReplicaMessage::Handshake(h) => assert_eq!(
1645 h.last_sequence, 0,
1646 "after a failed transfer the retry handshakes as fresh"
1647 ),
1648 other => panic!("expected Handshake, got {other:?}"),
1649 }
1650 stream_one(&mut s3, &mut buf);
1651 wait_for_ack(&mut s3, 1);
1652
1653 shutdown.store(true, Ordering::Relaxed);
1654 let result = replica.join().expect("replica thread panicked");
1655 assert_eq!(result, Ok(true), "receiver exits cleanly via shutdown");
1656
1657 assert!(
1662 dir.path().join("replica.journal.divergent.0").exists(),
1663 "forked lineage archived, never deleted"
1664 );
1665 assert!(
1666 replica_journal.exists(),
1667 "fresh live journal recreated after the failed transfer"
1668 );
1669 }
1670
1671 #[test]
1678 fn second_mid_stream_divergence_exits_hard() {
1679 let dir = tempfile::tempdir().expect("tempdir");
1680
1681 let primary_journal = dir.path().join("primary.journal");
1682 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1683 let mut chain_at_4 = [0u8; 32];
1684 for v in 1..=5u64 {
1685 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1686 if v == 2 {
1687 w.rotate_segment().expect("rotate");
1688 }
1689 if v == 4 {
1690 chain_at_4 = w.chain_hash().expect("chain");
1691 }
1692 }
1693 drop(w);
1694 melin_transport_core::snapshot::save::<App>(
1695 &App,
1696 WireSeq::new(4),
1697 chain_at_4,
1698 0,
1699 Some(0),
1700 &primary_journal.with_extension("snapshot"),
1701 )
1702 .expect("save snapshot");
1703 let origin = lineage_origin(&primary_journal).expect("lineage");
1704 let (lineage_start, lineage_anchor, lineage_genesis) = (
1705 origin.starting_sequence,
1706 origin.anchor_hash,
1707 origin.genesis_entries,
1708 );
1709
1710 let authorized_keys = replica_auth();
1711
1712 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1713 listener.set_nonblocking(true).expect("nonblocking");
1714 let addr = listener.local_addr().expect("addr");
1715
1716 let replica_journal = dir.path().join("replica.journal");
1717 let replica_snapshot = dir.path().join("replica.snapshot");
1718 let shutdown = Arc::new(AtomicBool::new(false));
1719 let control = crate::replication::ReplicaControlPlane::new();
1720 let cores = crate::layout::PipelineCores::unpinned();
1721 let replica = {
1722 let journal = replica_journal.clone();
1723 let shutdown = Arc::clone(&shutdown);
1724 let control = control.clone();
1725 std::thread::spawn(move || -> Result<bool, String> {
1726 run_receiver::<App>(
1727 addr,
1728 &journal,
1729 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1730 &shutdown,
1731 &control,
1732 3_600_000,
1733 replica_snapshot,
1734 cores,
1735 melin_journal::StagingMode::ZeroFill,
1736 Duration::ZERO,
1737 64,
1738 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1739 &(),
1740 )
1741 .map(|state| state.is_none())
1742 .map_err(|e| e.to_string())
1743 })
1744 };
1745
1746 let mut s1 = accept_within(&listener, 30);
1748 let mut s1r = s1.try_clone().expect("clone");
1749 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1750 match read_replica_msg(&mut s1) {
1751 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0),
1752 other => panic!("expected Handshake, got {other:?}"),
1753 }
1754 let mut buf = Vec::new();
1755 encode_stream_start(
1756 0,
1757 lineage_start,
1758 lineage_anchor,
1759 lineage_genesis,
1760 0,
1761 1,
1762 &mut buf,
1763 );
1764 s1.write_all(&buf).expect("StreamStart 1");
1765 buf.clear();
1766 melin_transport_core::replication_wire::encode_input_batch(
1767 &[InputSlot::<EvtAdd> {
1768 connection_id: 0,
1769 key_hash: 0,
1770 sequence: 1,
1771 timestamp_ns: 1,
1772 event: JournalEvent::App(EvtAdd(1)),
1773 publish_ts: Default::default(),
1774 recv_ts: Default::default(),
1775 }],
1776 &mut buf,
1777 )
1778 .expect("encode InputBatch");
1779 s1.write_all(&buf).expect("InputBatch");
1780 wait_for_ack(&mut s1, 1);
1781 buf.clear();
1782 encode_rotate(1, &[0xEE; 32], &mut buf);
1783 s1.write_all(&buf).expect("Rotate 1");
1784
1785 let mut s2 = accept_within(&listener, 30);
1788 let mut s2r = s2.try_clone().expect("clone");
1789 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1790 match read_replica_msg(&mut s2) {
1791 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 1),
1792 other => panic!("expected Handshake, got {other:?}"),
1793 }
1794 buf.clear();
1795 encode_hash_mismatch(&mut buf);
1796 s2.write_all(&buf).expect("HashMismatch");
1797 let transfer_shutdown = AtomicBool::new(false);
1798 {
1799 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1800 s2.write_all(b)?;
1801 s2.flush()
1802 };
1803 snapshot_transfer_with::<EvtAdd>(
1804 &primary_journal,
1805 &mut publish,
1806 &transfer_shutdown,
1807 1,
1808 )
1809 .expect("snapshot transfer");
1810 }
1811 buf.clear();
1812 melin_transport_core::replication_wire::encode_input_batch(
1813 &[InputSlot::<EvtAdd> {
1814 connection_id: 0,
1815 key_hash: 0,
1816 sequence: 6,
1817 timestamp_ns: 6,
1818 event: JournalEvent::App(EvtAdd(6)),
1819 publish_ts: Default::default(),
1820 recv_ts: Default::default(),
1821 }],
1822 &mut buf,
1823 )
1824 .expect("encode InputBatch");
1825 s2.write_all(&buf).expect("InputBatch 6");
1826 let mut s2_acks = s2.try_clone().expect("clone");
1827 wait_for_ack(&mut s2_acks, 6);
1828 buf.clear();
1829 encode_rotate(6, &[0xEE; 32], &mut buf);
1830 s2.write_all(&buf).expect("Rotate 2");
1831
1832 let result = replica.join().expect("replica thread panicked");
1835 let err = result.expect_err("second divergence must exit hard");
1836 assert!(
1837 err.contains("divergence recurred"),
1838 "error must name the recurrence: {err}"
1839 );
1840 }
1841
1842 type PromoteHandles =
1851 crate::replication::ReplicaPipelineHandles<App, BufferedWriter<EvtAdd>>;
1852
1853 #[test]
1854 fn promotion_hands_back_local_state_when_present() {
1855 let dir = tempfile::tempdir().unwrap();
1856 let mut pipeline: Option<PromoteHandles> = None;
1857 let mut app = Some(App);
1858 let mut journal_writer =
1859 Some(BufferedWriter::<EvtAdd>::create(&dir.path().join("p.journal")).unwrap());
1860
1861 let result = crate::replication::take_pipeline_for_promotion(
1862 &mut pipeline,
1863 &mut app,
1864 &mut journal_writer,
1865 );
1866
1867 assert!(matches!(result, Ok(Some(_))), "warm state must be promoted");
1868 assert!(
1869 app.is_none() && journal_writer.is_none(),
1870 "state must be moved into the result"
1871 );
1872 }
1873
1874 #[test]
1875 fn promotion_errs_when_no_local_state() {
1876 let mut pipeline: Option<PromoteHandles> = None;
1877 let mut app: Option<App> = None;
1878 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
1879
1880 let result = crate::replication::take_pipeline_for_promotion(
1881 &mut pipeline,
1882 &mut app,
1883 &mut journal_writer,
1884 );
1885
1886 let err = match result {
1888 Err(e) => e,
1889 Ok(_) => panic!("a promote with nothing to promote must error"),
1890 };
1891 assert!(
1892 err.to_string().contains("no local state available"),
1893 "{err}"
1894 );
1895 }
1896
1897 #[test]
1898 fn promotion_errs_on_partial_state() {
1899 let mut pipeline: Option<PromoteHandles> = None;
1901 let mut app = Some(App);
1902 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
1903
1904 let result = crate::replication::take_pipeline_for_promotion(
1905 &mut pipeline,
1906 &mut app,
1907 &mut journal_writer,
1908 );
1909
1910 assert!(result.is_err(), "partial state is not a valid promotion");
1911 }
1912 }
1913
1914 #[cfg(not(feature = "no-persist"))]
1920 mod backoff_shutdown {
1921 use super::super::super::auth::authenticate_replica;
1922 use super::scripted::*;
1923 use super::*;
1924 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
1925 use melin_transport_core::replication::catchup::lineage_origin;
1926 use melin_transport_core::replication::protocol::encode_stream_start;
1927 use std::sync::Arc;
1928 use std::sync::atomic::AtomicBool;
1929
1930 fn live_thread_names() -> Vec<String> {
1933 std::fs::read_dir("/proc/self/task")
1934 .expect("read /proc/self/task")
1935 .filter_map(|entry| {
1936 let entry = entry.ok()?;
1939 let name = std::fs::read_to_string(entry.path().join("comm")).ok()?;
1940 Some(name.trim().to_string())
1941 })
1942 .collect()
1943 }
1944
1945 const ISOLATED: &str = "MELIN_TEST_BACKOFF_SHUTDOWN_ISOLATED";
1947
1948 #[test]
1961 fn shutdown_during_reconnect_backoff_joins_pipeline_threads() {
1962 if std::env::var_os(ISOLATED).is_some() {
1963 return backoff_shutdown_body();
1964 }
1965 let module = module_path!();
1968 let path = module.split_once("::").map_or(module, |(_, rest)| rest);
1969 let test = format!("{path}::shutdown_during_reconnect_backoff_joins_pipeline_threads");
1970 let output = std::process::Command::new(std::env::current_exe().expect("current_exe"))
1971 .args(["--exact", &test, "--test-threads=1", "--nocapture"])
1972 .env(ISOLATED, "1")
1973 .output()
1974 .expect("run the test in a process of its own");
1975 let stdout = String::from_utf8_lossy(&output.stdout);
1976 let stderr = String::from_utf8_lossy(&output.stderr);
1977 assert!(
1980 output.status.success() && stdout.contains("1 passed"),
1981 "the isolated run failed ({}):\n{stdout}\n{stderr}",
1982 output.status
1983 );
1984 }
1985
1986 fn backoff_shutdown_body() {
1987 let dir = tempfile::tempdir().expect("tempdir");
1988
1989 let primary_journal = dir.path().join("primary.journal");
1992 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1993 w.append(&JournalEvent::App(EvtAdd(1))).expect("append");
1994 drop(w);
1995 let origin = lineage_origin(&primary_journal).expect("lineage");
1996 let (lineage_start, lineage_anchor, lineage_genesis) = (
1997 origin.starting_sequence,
1998 origin.anchor_hash,
1999 origin.genesis_entries,
2000 );
2001
2002 let authorized_keys = replica_auth();
2003 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2004 listener.set_nonblocking(true).expect("nonblocking");
2005 let addr = listener.local_addr().expect("addr");
2006
2007 let shutdown = Arc::new(AtomicBool::new(false));
2008 let control = crate::replication::ReplicaControlPlane::new();
2009 let replica = {
2010 let journal = dir.path().join("replica.journal");
2011 let snapshot = dir.path().join("replica.snapshot");
2012 let shutdown = Arc::clone(&shutdown);
2013 let control = control.clone();
2014 std::thread::spawn(move || -> Result<bool, String> {
2015 run_receiver::<App>(
2016 addr,
2017 &journal,
2018 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
2019 &shutdown,
2020 &control,
2021 3_600_000,
2022 snapshot,
2023 crate::layout::PipelineCores::unpinned(),
2024 melin_journal::StagingMode::ZeroFill,
2025 Duration::ZERO,
2026 64,
2027 Arc::new(melin_transport_core::fence::FenceState::new(0)),
2028 &(),
2029 )
2030 .map(|state| state.is_none())
2032 .map_err(|e| e.to_string())
2033 })
2034 };
2035
2036 let mut s1 = accept_within(&listener, 30);
2040 let mut s1r = s1.try_clone().expect("clone");
2041 authenticate_replica(&mut s1r, &authorized_keys).expect("auth");
2042 match read_replica_msg(&mut s1) {
2043 ReplicaMessage::Handshake(h) => {
2044 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
2045 }
2046 other => panic!("expected Handshake, got {other:?}"),
2047 }
2048 let mut buf = Vec::new();
2049 encode_stream_start(
2050 0,
2051 lineage_start,
2052 lineage_anchor,
2053 lineage_genesis,
2054 0,
2055 1,
2056 &mut buf,
2057 );
2058 s1.write_all(&buf).expect("StreamStart");
2059 buf.clear();
2060 melin_transport_core::replication_wire::encode_input_batch(
2061 &[InputSlot::<EvtAdd> {
2062 connection_id: 0,
2063 key_hash: 0,
2064 sequence: 1,
2065 timestamp_ns: 1,
2066 event: JournalEvent::App(EvtAdd(1)),
2067 publish_ts: Default::default(),
2068 recv_ts: Default::default(),
2069 }],
2070 &mut buf,
2071 )
2072 .expect("encode InputBatch");
2073 s1.write_all(&buf).expect("InputBatch");
2074 wait_for_ack(&mut s1, 1);
2075
2076 drop(s1);
2080 drop(s1r);
2081 drop(listener);
2082 std::thread::sleep(Duration::from_millis(1500));
2087
2088 shutdown.store(true, Ordering::Relaxed);
2089 let deadline = Instant::now() + Duration::from_secs(10);
2090 while !replica.is_finished() {
2091 assert!(
2092 Instant::now() < deadline,
2093 "receiver did not exit after shutdown"
2094 );
2095 std::thread::sleep(Duration::from_millis(20));
2096 }
2097 let result = replica.join().expect("replica thread panicked");
2098 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
2099
2100 let deadline = Instant::now() + Duration::from_secs(10);
2105 loop {
2106 let names = live_thread_names();
2107 if !names
2108 .iter()
2109 .any(|n| n == "journal-seq" || n == "matching" || n == "drain")
2110 {
2111 break;
2112 }
2113 assert!(
2114 Instant::now() < deadline,
2115 "pipeline threads still alive after shutdown: {names:?}"
2116 );
2117 std::thread::sleep(Duration::from_millis(50));
2118 }
2119 }
2120 }
2121
2122 #[cfg(not(feature = "no-persist"))]
2128 mod reconnect_resume {
2129 use super::super::super::auth::authenticate_replica;
2130 use super::scripted::*;
2131 use super::*;
2132 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
2133 use melin_transport_core::replication::catchup::lineage_origin;
2134 use melin_transport_core::replication::protocol::encode_stream_start;
2135 use std::sync::Arc;
2136 use std::sync::atomic::AtomicBool;
2137
2138 fn primary_journal(
2141 dir: &std::path::Path,
2142 ) -> (std::path::PathBuf, melin_journal::FileHeaderInfo) {
2143 let path = dir.join("primary.journal");
2144 let mut w = BufferedWriter::<EvtAdd>::create(&path).expect("create");
2145 w.append(&JournalEvent::App(EvtAdd(1))).expect("append");
2146 drop(w);
2147 let lineage = lineage_origin(&path).expect("lineage");
2148 (path, lineage)
2149 }
2150
2151 fn spawn_replica(
2154 dir: &std::path::Path,
2155 addr: std::net::SocketAddr,
2156 group_commit_delay: Duration,
2157 shutdown: &Arc<AtomicBool>,
2158 ) -> std::thread::JoinHandle<Result<bool, String>> {
2159 let journal = dir.join("replica.journal");
2160 let snapshot = dir.join("replica.snapshot");
2161 let shutdown = Arc::clone(shutdown);
2162 std::thread::spawn(move || -> Result<bool, String> {
2163 run_receiver::<App>(
2164 addr,
2165 &journal,
2166 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
2167 &shutdown,
2168 &crate::replication::ReplicaControlPlane::new(),
2169 3_600_000,
2170 snapshot,
2171 crate::layout::PipelineCores::unpinned(),
2172 melin_journal::StagingMode::ZeroFill,
2173 group_commit_delay,
2174 64,
2175 Arc::new(melin_transport_core::fence::FenceState::new(0)),
2176 &(),
2177 )
2178 .map(|state| state.is_none())
2180 .map_err(|e| e.to_string())
2181 })
2182 }
2183
2184 fn next_handshake(
2187 listener: &std::net::TcpListener,
2188 ) -> (TcpStream, TcpStream, crate::replication::Handshake) {
2189 let mut s = accept_within(listener, 30);
2190 let mut sr = s.try_clone().expect("clone");
2191 authenticate_replica(&mut sr, &replica_auth()).expect("auth");
2192 match read_replica_msg(&mut s) {
2193 ReplicaMessage::Handshake(h) => (s, sr, h),
2194 other => panic!("expected Handshake, got {other:?}"),
2195 }
2196 }
2197
2198 fn stream_start(
2200 stream: &mut TcpStream,
2201 start: u64,
2202 lineage: melin_journal::FileHeaderInfo,
2203 ) {
2204 let mut buf = Vec::new();
2205 encode_stream_start(
2206 start,
2207 lineage.starting_sequence,
2208 lineage.anchor_hash,
2209 lineage.genesis_entries,
2210 0,
2211 1,
2212 &mut buf,
2213 );
2214 stream.write_all(&buf).expect("StreamStart");
2215 }
2216
2217 fn entry(sequence: u64) -> InputSlot<EvtAdd> {
2219 InputSlot::<EvtAdd> {
2220 connection_id: 0,
2221 key_hash: 0,
2222 sequence,
2223 timestamp_ns: sequence,
2224 event: JournalEvent::App(EvtAdd(sequence)),
2225 publish_ts: Default::default(),
2226 recv_ts: Default::default(),
2227 }
2228 }
2229
2230 fn send_entries(stream: &mut TcpStream, sequences: &[u64]) {
2231 let slots: Vec<_> = sequences.iter().map(|&s| entry(s)).collect();
2232 let mut buf = Vec::new();
2233 melin_transport_core::replication_wire::encode_input_batch(&slots, &mut buf)
2234 .expect("encode InputBatch");
2235 stream.write_all(&buf).expect("InputBatch");
2236 }
2237
2238 fn stop(replica: std::thread::JoinHandle<Result<bool, String>>, shutdown: &AtomicBool) {
2239 shutdown.store(true, Ordering::Relaxed);
2240 let deadline = Instant::now() + Duration::from_secs(10);
2241 while !replica.is_finished() {
2242 assert!(
2243 Instant::now() < deadline,
2244 "receiver did not exit after shutdown"
2245 );
2246 std::thread::sleep(Duration::from_millis(20));
2247 }
2248 let result = replica.join().expect("replica thread panicked");
2249 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
2250 }
2251
2252 #[test]
2258 fn a_reconnect_claims_entries_received_before_the_drop() {
2259 let dir = tempfile::tempdir().expect("tempdir");
2260 let (_, lineage) = primary_journal(dir.path());
2261 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2262 listener.set_nonblocking(true).expect("nonblocking");
2263 let shutdown = Arc::new(AtomicBool::new(false));
2264 let delay = Duration::from_millis(2_500);
2268 let replica = spawn_replica(
2269 dir.path(),
2270 listener.local_addr().expect("addr"),
2271 delay,
2272 &shutdown,
2273 );
2274
2275 let (mut s1, _s1r, h1) = next_handshake(&listener);
2276 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2277 stream_start(&mut s1, 0, lineage);
2278 send_entries(&mut s1, &[1]);
2281 wait_for_ack(&mut s1, 1);
2282 send_entries(&mut s1, &[2, 3, 5]);
2285
2286 let (mut s2, _s2r, h2) = next_handshake(&listener);
2287 assert_eq!(
2288 h2.last_sequence, 3,
2289 "the reconnect must claim the entries received before the drop"
2290 );
2291 stream_start(&mut s2, h2.last_sequence, lineage);
2294 stop(replica, &shutdown);
2295 }
2296
2297 #[test]
2301 fn a_failed_resync_transfer_closes_the_socket_before_backing_off() {
2302 use melin_transport_core::replication::protocol::encode_need_snapshot;
2303
2304 let dir = tempfile::tempdir().expect("tempdir");
2305 let (_, lineage) = primary_journal(dir.path());
2306 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2307 listener.set_nonblocking(true).expect("nonblocking");
2308 let shutdown = Arc::new(AtomicBool::new(false));
2309 let replica = spawn_replica(
2310 dir.path(),
2311 listener.local_addr().expect("addr"),
2312 Duration::ZERO,
2313 &shutdown,
2314 );
2315
2316 let (mut s1, _s1r, _) = next_handshake(&listener);
2317 let mut buf = Vec::new();
2320 encode_need_snapshot(&mut buf);
2321 encode_need_snapshot(&mut buf);
2322 s1.write_all(&buf).expect("NeedSnapshot x2");
2323
2324 s1.set_read_timeout(Some(Duration::from_millis(700)))
2327 .expect("read timeout");
2328 let mut byte = [0u8; 1];
2329 match s1.read(&mut byte) {
2330 Ok(0) => {}
2331 Err(e) if e.kind() == std::io::ErrorKind::ConnectionReset => {}
2332 other => panic!("socket still open during the backoff: {other:?}"),
2333 }
2334
2335 let (mut s2, _s2r, h2) = next_handshake(&listener);
2337 assert_eq!(h2.last_sequence, 0, "retry handshakes as fresh");
2338 stream_start(&mut s2, 0, lineage);
2339 stop(replica, &shutdown);
2340 }
2341
2342 #[cfg(feature = "hash-chain")]
2356 #[test]
2357 fn a_journal_that_dies_while_disconnected_is_torn_down_before_dialing() {
2358 use melin_transport_core::replication::protocol::encode_chain_check;
2359
2360 let dir = tempfile::tempdir().expect("tempdir");
2361 let (_, lineage) = primary_journal(dir.path());
2362 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2363 listener.set_nonblocking(true).expect("nonblocking");
2364 let shutdown = Arc::new(AtomicBool::new(false));
2365 let delay = Duration::from_millis(2_500);
2368 let replica = spawn_replica(
2369 dir.path(),
2370 listener.local_addr().expect("addr"),
2371 delay,
2372 &shutdown,
2373 );
2374
2375 let (mut s1, _s1r, h1) = next_handshake(&listener);
2376 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2377 stream_start(&mut s1, 0, lineage);
2378 send_entries(&mut s1, &[1]);
2379 wait_for_ack(&mut s1, 1);
2380 send_entries(&mut s1, &[2]);
2384 let mut buf = Vec::new();
2385 encode_chain_check(2, &[0xEE; 32], &mut buf);
2386 s1.write_all(&buf).expect("ChainCheck");
2387 send_entries(&mut s1, &[3]);
2388 send_entries(&mut s1, &[5]);
2389
2390 let (mut s2, _s2r, h2) = next_handshake(&listener);
2391 assert_eq!(
2392 h2.last_sequence, 1,
2393 "the first dial after the journal died claims what is on disk"
2394 );
2395 stream_start(&mut s2, h2.last_sequence, lineage);
2400 send_entries(&mut s2, &[2]);
2401 wait_for_ack(&mut s2, 2);
2402 stop(replica, &shutdown);
2403 }
2404
2405 #[test]
2410 fn a_reconnect_after_a_quiet_session_claims_the_recovered_journal() {
2411 let dir = tempfile::tempdir().expect("tempdir");
2412 let (primary, lineage) = primary_journal(dir.path());
2413 std::fs::copy(&primary, dir.path().join("replica.journal")).expect("copy");
2415 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2416 listener.set_nonblocking(true).expect("nonblocking");
2417 let shutdown = Arc::new(AtomicBool::new(false));
2418 let replica = spawn_replica(
2419 dir.path(),
2420 listener.local_addr().expect("addr"),
2421 Duration::ZERO,
2422 &shutdown,
2423 );
2424
2425 let (mut s1, s1r, h1) = next_handshake(&listener);
2426 assert_eq!(h1.last_sequence, 1, "recovered replica handshake");
2427 stream_start(&mut s1, 1, lineage);
2428 drop(s1);
2430 drop(s1r);
2431
2432 let (mut s2, _s2r, h2) = next_handshake(&listener);
2433 assert_eq!(
2434 (h2.last_sequence, h2.chain_hash),
2435 (h1.last_sequence, h1.chain_hash),
2436 "the reconnect must claim the journal the pipeline was built over"
2437 );
2438 stream_start(&mut s2, h2.last_sequence, lineage);
2440 stop(replica, &shutdown);
2441 }
2442
2443 #[test]
2450 fn a_damaged_entry_is_refused_and_fetched_again() {
2451 let dir = tempfile::tempdir().expect("tempdir");
2452 let (_, lineage) = primary_journal(dir.path());
2453 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2454 listener.set_nonblocking(true).expect("nonblocking");
2455 let shutdown = Arc::new(AtomicBool::new(false));
2456 let replica = spawn_replica(
2457 dir.path(),
2458 listener.local_addr().expect("addr"),
2459 Duration::ZERO,
2460 &shutdown,
2461 );
2462
2463 let (mut s1, _s1r, h1) = next_handshake(&listener);
2464 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2465 stream_start(&mut s1, 0, lineage);
2466 send_entries(&mut s1, &[1]);
2467 wait_for_ack(&mut s1, 1);
2468
2469 let mut buf = Vec::new();
2472 melin_transport_core::replication_wire::encode_input_batch(&[entry(2)], &mut buf)
2473 .expect("encode InputBatch");
2474 let payload_at = buf.len() - melin_journal::codec::CRC_SIZE - 8;
2475 buf[payload_at] ^= 0x01;
2476 s1.write_all(&buf).expect("damaged InputBatch");
2477
2478 loop {
2481 let mut len_buf = [0u8; 4];
2482 if std::io::Read::read_exact(&mut s1, &mut len_buf).is_err() {
2483 break;
2484 }
2485 let mut payload = vec![0u8; u32::from_le_bytes(len_buf) as usize];
2486 if std::io::Read::read_exact(&mut s1, &mut payload).is_err() {
2487 break;
2488 }
2489 if let Ok(ReplicaMessage::Ack(a)) = decode_replica_message(&payload) {
2490 assert!(
2491 a.in_memory_sequence < 2 && a.acked_sequence < 2,
2492 "the damaged entry was acknowledged: {a:?}"
2493 );
2494 }
2495 }
2496
2497 let (mut s2, _s2r, h2) = next_handshake(&listener);
2498 assert_eq!(
2499 h2.last_sequence, 1,
2500 "the reconnect must claim only the entries that passed the check"
2501 );
2502 stream_start(&mut s2, 1, lineage);
2503 send_entries(&mut s2, &[2]);
2504 wait_for_ack(&mut s2, 2);
2505 stop(replica, &shutdown);
2506
2507 let mut reader =
2508 melin_journal::JournalReader::<EvtAdd>::open(&dir.path().join("replica.journal"))
2509 .expect("open replica journal");
2510 let mut journaled = Vec::new();
2511 while let Some(e) = reader.next_entry().expect("read entry") {
2512 if let JournalEvent::App(EvtAdd(v)) = e.event {
2513 journaled.push(v);
2514 }
2515 }
2516 assert_eq!(journaled, vec![1, 2], "only the intact entry is journaled");
2517 }
2518
2519 fn with_genesis(
2522 lineage: melin_journal::FileHeaderInfo,
2523 genesis_entries: Option<u64>,
2524 ) -> melin_journal::FileHeaderInfo {
2525 melin_journal::FileHeaderInfo {
2526 genesis_entries,
2527 ..lineage
2528 }
2529 }
2530
2531 fn replica_genesis(dir: &std::path::Path) -> Option<u64> {
2532 melin_journal::segment::read_header_info(&dir.join("replica.journal"))
2533 .expect("replica journal header")
2534 .genesis_entries
2535 }
2536
2537 #[test]
2542 fn a_replica_learns_the_genesis_length_and_keeps_it_across_restarts() {
2543 let dir = tempfile::tempdir().expect("tempdir");
2544 let (_, base) = primary_journal(dir.path());
2545 let lineage = with_genesis(base, Some(3));
2546 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2547 listener.set_nonblocking(true).expect("nonblocking");
2548 let addr = listener.local_addr().expect("addr");
2549
2550 let shutdown = Arc::new(AtomicBool::new(false));
2551 let replica = spawn_replica(dir.path(), addr, Duration::ZERO, &shutdown);
2552 let (mut s1, _s1r, h1) = next_handshake(&listener);
2553 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2554 stream_start(&mut s1, 0, lineage);
2555 send_entries(&mut s1, &[1, 2]);
2556 wait_for_ack(&mut s1, 2);
2557 assert_eq!(replica_genesis(dir.path()), Some(3), "learned at creation");
2558 stop(replica, &shutdown);
2559
2560 let shutdown = Arc::new(AtomicBool::new(false));
2561 let replica = spawn_replica(dir.path(), addr, Duration::ZERO, &shutdown);
2562 let (mut s2, _s2r, h2) = next_handshake(&listener);
2563 assert_eq!(h2.last_sequence, 2, "restarted replica handshake");
2564 stream_start(&mut s2, 2, lineage);
2565 send_entries(&mut s2, &[3]);
2566 wait_for_ack(&mut s2, 3);
2567 stop(replica, &shutdown);
2568 assert_eq!(
2569 replica_genesis(dir.path()),
2570 Some(3),
2571 "kept across a restart"
2572 );
2573 }
2574
2575 #[test]
2582 fn an_empty_old_format_replica_journal_learns_the_genesis_length() {
2583 let dir = tempfile::tempdir().expect("tempdir");
2584 let (_, base) = primary_journal(dir.path());
2585 assert_eq!(base.starting_sequence, 1);
2586 drop(
2587 melin_journal::BufferedWriter::<EvtAdd>::create_continuing(
2588 &dir.path().join("replica.journal"),
2589 1,
2590 base.anchor_hash,
2591 None,
2592 )
2593 .expect("old-format empty replica journal"),
2594 );
2595 assert_eq!(replica_genesis(dir.path()), None);
2596 let lineage = with_genesis(base, Some(3));
2597 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2598 listener.set_nonblocking(true).expect("nonblocking");
2599 let shutdown = Arc::new(AtomicBool::new(false));
2600 let replica = spawn_replica(
2601 dir.path(),
2602 listener.local_addr().expect("addr"),
2603 Duration::ZERO,
2604 &shutdown,
2605 );
2606
2607 let (mut s1, _s1r, h1) = next_handshake(&listener);
2608 assert_eq!(h1.last_sequence, 0, "empty replica handshake");
2609 stream_start(&mut s1, 0, lineage);
2610 send_entries(&mut s1, &[1, 2]);
2611 wait_for_ack(&mut s1, 2);
2612 stop(replica, &shutdown);
2613 assert_eq!(replica_genesis(dir.path()), Some(3));
2614 }
2615
2616 #[test]
2620 fn a_primary_advertising_another_genesis_length_is_refused() {
2621 let dir = tempfile::tempdir().expect("tempdir");
2622 let (primary, lineage) = primary_journal(dir.path());
2623 assert_eq!(lineage.genesis_entries, Some(0));
2624 std::fs::copy(&primary, dir.path().join("replica.journal")).expect("copy");
2627 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2628 listener.set_nonblocking(true).expect("nonblocking");
2629 let shutdown = Arc::new(AtomicBool::new(false));
2630 let replica = spawn_replica(
2631 dir.path(),
2632 listener.local_addr().expect("addr"),
2633 Duration::ZERO,
2634 &shutdown,
2635 );
2636
2637 let (mut s1, _s1r, h1) = next_handshake(&listener);
2638 assert_eq!(h1.last_sequence, 1);
2639 stream_start(&mut s1, 1, with_genesis(lineage, Some(4)));
2640 let result = replica.join().expect("replica thread panicked");
2641 let err = result.expect_err("the replica must refuse the lineage");
2642 assert!(err.contains("refusing to follow the primary"), "{err}");
2643 assert_eq!(replica_genesis(dir.path()), Some(0), "never overwritten");
2644 }
2645
2646 #[test]
2653 fn a_replica_promoted_mid_genesis_is_refused() {
2654 let dir = tempfile::tempdir().expect("tempdir");
2655 let (_, base) = primary_journal(dir.path());
2656 let lineage = with_genesis(base, Some(3));
2657 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2658 listener.set_nonblocking(true).expect("nonblocking");
2659 let addr = listener.local_addr().expect("addr");
2660 let shutdown = Arc::new(AtomicBool::new(false));
2661 let control = crate::replication::ReplicaControlPlane::new();
2662 let replica = {
2663 let journal = dir.path().join("replica.journal");
2664 let snapshot = dir.path().join("replica.snapshot");
2665 let shutdown = Arc::clone(&shutdown);
2666 let control = control.clone();
2667 std::thread::spawn(move || -> Result<Option<String>, String> {
2668 run_receiver::<App>(
2669 addr,
2670 &journal,
2671 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
2672 &shutdown,
2673 &control,
2674 3_600_000,
2675 snapshot,
2676 crate::layout::PipelineCores::unpinned(),
2677 melin_journal::StagingMode::ZeroFill,
2678 Duration::ZERO,
2679 64,
2680 Arc::new(melin_transport_core::fence::FenceState::new(0)),
2681 &(),
2682 )
2683 .map(|promoted| {
2686 promoted.map(
2687 |(_, writer)| match crate::server::check_promotable(&writer) {
2688 Ok(()) => "promotable".to_string(),
2689 Err(e) => e.to_string(),
2690 },
2691 )
2692 })
2693 .map_err(|e| e.to_string())
2694 })
2695 };
2696
2697 let (mut s1, s1r, h1) = next_handshake(&listener);
2698 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2699 stream_start(&mut s1, 0, lineage);
2700 send_entries(&mut s1, &[1, 2]);
2701 wait_for_ack(&mut s1, 2);
2702 drop(s1);
2704 drop(s1r);
2705 assert!(
2706 control
2707 .promote
2708 .request(crate::promotion::PromotionRequest::MANUAL),
2709 "promotion filed"
2710 );
2711
2712 let verdict = replica
2713 .join()
2714 .expect("replica thread panicked")
2715 .expect("the receiver hands its state over")
2716 .expect("promoted, not shut down");
2717 assert!(verdict.contains("refusing promotion"), "{verdict}");
2718 assert!(verdict.contains("holds 2 entries"), "{verdict}");
2719 }
2720 }
2721
2722 #[cfg(all(feature = "hash-chain", not(feature = "no-persist")))]
2729 mod rotation_adoption {
2730 use super::super::super::auth::authenticate_replica;
2731 use super::scripted::*;
2732 use super::*;
2733 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
2734 use melin_transport_core::replication::catchup::lineage_origin;
2735 use melin_transport_core::replication::protocol::{encode_rotate, encode_stream_start};
2736 use std::sync::Arc;
2737 use std::sync::atomic::AtomicBool;
2738
2739 fn payloads(path: &std::path::Path) -> Vec<u64> {
2741 let mut reader = melin_journal::JournalReader::<EvtAdd>::open(path).expect("open");
2742 let mut out = Vec::new();
2743 while let Some(entry) = reader.next_entry().expect("read entry") {
2744 if let JournalEvent::App(EvtAdd(v)) = entry.event {
2745 out.push(v);
2746 }
2747 }
2748 out
2749 }
2750
2751 fn boundary_adoption_rotates_and_streams_on(mode: melin_journal::StagingMode) {
2762 let _prealloc_guard =
2767 melin_journal::test_utils::PreallocOverrideGuard::new(1024 * 1024);
2768 let dir = tempfile::tempdir().expect("tempdir");
2769
2770 let primary_journal = dir.path().join("primary.journal");
2776 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
2777 for v in 1..=2u64 {
2778 w.batch_append_with_ts(&JournalEvent::App(EvtAdd(v)), v, 0)
2779 .expect("append");
2780 }
2781 w.flush_batch_sync().expect("flush");
2782 let chain_at_2 = w.chain_hash().expect("chain");
2783 drop(w);
2784 let origin = lineage_origin(&primary_journal).expect("lineage");
2785 let (lineage_start, lineage_anchor, lineage_genesis) = (
2786 origin.starting_sequence,
2787 origin.anchor_hash,
2788 origin.genesis_entries,
2789 );
2790
2791 let authorized_keys = replica_auth();
2792 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2793 listener.set_nonblocking(true).expect("nonblocking");
2794 let addr = listener.local_addr().expect("addr");
2795
2796 let replica_journal = dir.path().join("replica.journal");
2797 let shutdown = Arc::new(AtomicBool::new(false));
2798 let control = crate::replication::ReplicaControlPlane::new();
2799 let replica = {
2800 let journal = replica_journal.clone();
2801 let snapshot = dir.path().join("replica.snapshot");
2802 let shutdown = Arc::clone(&shutdown);
2803 let control = control.clone();
2804 std::thread::spawn(move || -> Result<bool, String> {
2805 run_receiver::<App>(
2806 addr,
2807 &journal,
2808 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
2809 &shutdown,
2810 &control,
2811 3_600_000,
2812 snapshot,
2813 crate::layout::PipelineCores::unpinned(),
2814 mode,
2815 Duration::ZERO,
2816 64,
2817 Arc::new(melin_transport_core::fence::FenceState::new(0)),
2818 &(),
2819 )
2820 .map(|state| state.is_none())
2822 .map_err(|e| e.to_string())
2823 })
2824 };
2825
2826 let mut s1 = accept_within(&listener, 30);
2827 let mut s1r = s1.try_clone().expect("clone");
2828 authenticate_replica(&mut s1r, &authorized_keys).expect("auth");
2829 match read_replica_msg(&mut s1) {
2830 ReplicaMessage::Handshake(h) => {
2831 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
2832 }
2833 other => panic!("expected Handshake, got {other:?}"),
2834 }
2835 let mut buf = Vec::new();
2836 encode_stream_start(
2837 0,
2838 lineage_start,
2839 lineage_anchor,
2840 lineage_genesis,
2841 0,
2842 1,
2843 &mut buf,
2844 );
2845 s1.write_all(&buf).expect("StreamStart");
2846
2847 let send_events = |s: &mut TcpStream, range: std::ops::RangeInclusive<u64>| {
2848 let slots: Vec<InputSlot<EvtAdd>> = range
2849 .map(|v| InputSlot {
2850 connection_id: 0,
2851 key_hash: 0,
2852 sequence: v,
2853 timestamp_ns: v,
2854 event: JournalEvent::App(EvtAdd(v)),
2855 publish_ts: Default::default(),
2857 recv_ts: Default::default(),
2858 })
2859 .collect();
2860 let mut buf = Vec::new();
2861 melin_transport_core::replication_wire::encode_input_batch(&slots, &mut buf)
2862 .expect("encode InputBatch");
2863 s.write_all(&buf).expect("InputBatch");
2864 };
2865
2866 send_events(&mut s1, 1..=2);
2867 wait_for_ack(&mut s1, 2);
2868
2869 let staging = {
2876 let mut s = replica_journal.clone().into_os_string();
2880 s.push(".next-staging");
2881 std::path::PathBuf::from(s)
2882 };
2883 let staged_len: u64 = (1 + 8) * 1024 * 1024;
2884 let deadline = Instant::now() + Duration::from_secs(10);
2885 loop {
2886 if std::fs::metadata(&staging).is_ok_and(|m| m.len() == staged_len) {
2887 break;
2888 }
2889 assert!(
2890 Instant::now() < deadline,
2891 "preparer produced no full-size staged segment in {mode:?} mode"
2892 );
2893 std::thread::sleep(Duration::from_millis(10));
2894 }
2895 std::thread::sleep(Duration::from_millis(300));
2903
2904 buf.clear();
2907 encode_rotate(2, &chain_at_2, &mut buf);
2908 s1.write_all(&buf).expect("Rotate");
2909
2910 send_events(&mut s1, 3..=4);
2914 wait_for_ack(&mut s1, 4);
2915
2916 shutdown.store(true, Ordering::Relaxed);
2917 let result = replica.join().expect("replica thread panicked");
2918 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
2919
2920 let archived = dir.path().join("replica.journal.000001");
2924 assert!(archived.exists(), "sealed segment must be archived");
2925 assert_eq!(payloads(&archived), vec![1, 2]);
2926 assert_eq!(payloads(&replica_journal), vec![3, 4]);
2927 }
2928
2929 #[test]
2930 fn boundary_adoption_zero_fill() {
2931 boundary_adoption_rotates_and_streams_on(melin_journal::StagingMode::ZeroFill);
2932 }
2933
2934 #[test]
2935 fn boundary_adoption_allocate() {
2936 boundary_adoption_rotates_and_streams_on(melin_journal::StagingMode::Allocate);
2937 }
2938 }
2939}