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;
19use melin_transport_core::node_store::NodeStore;
20
21use super::auth::authenticate_with_primary;
22use super::receiver_transport::{
23 ControlFrameSource, ReceiverTransport, SessionExit, streaming_loop,
24};
25use super::{
26 ReplicaPipelineHandles, ResumePoint, ResyncDecision, StreamStartVerdict,
27 build_replica_pipeline_with_threads, handle_resync_verdict, handle_session_exit,
28 journal_failed_while_disconnected, recover_replica_state, sleep_then_double_backoff,
29 take_pipeline_for_promotion, teardown_replica_pipeline,
30};
31use crate::uring_teardown::{DrainBackoff, wake_pending_ops};
32use melin_transport_core::replication::protocol::{
33 Ack, Handshake, MAX_CONTROL_FRAME, MAX_DATA_FRAME, PrimaryMessage, decode_primary_message,
34 encode_ack, encode_handshake, read_frame,
35};
36
37#[inline]
47fn arm_tcp_quickack(fd: RawFd) {
48 let on: libc::c_int = 1;
49 let rc = unsafe {
53 libc::setsockopt(
54 fd,
55 libc::IPPROTO_TCP,
56 libc::TCP_QUICKACK,
57 &on as *const libc::c_int as *const libc::c_void,
58 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
59 )
60 };
61 if rc != 0 {
62 debug_assert!(
68 std::io::Error::last_os_error().raw_os_error() != Some(libc::EFAULT),
69 "TCP_QUICKACK setsockopt got EFAULT — bad option pointer"
70 );
71 }
72}
73
74const TOKEN_RECV: u64 = 0;
79const TOKEN_SEND: u64 = 1;
80const TOKEN_PROVIDE: u64 = 2;
81
82const NUM_RECV_BUFFERS: u16 = 16;
84const RECV_BUF_SIZE: usize = MAX_DATA_FRAME;
85const RECV_BUF_GROUP_ID: u16 = 0;
86
87const IORING_CQE_F_BUFFER: u32 = 1 << 0;
89const IORING_CQE_F_MORE: u32 = 1 << 1;
90const IORING_CQE_BUFFER_SHIFT: u32 = 16;
91
92struct UringTransport {
98 ring: io_uring::IoUring,
99 tcp_fd: RawFd,
104 recv_pool: Vec<u8>,
110 pool_ptr: *mut u8,
111 multishot_active: bool,
112 connected: bool,
113 ack_buf: Vec<u8>,
114 ack_offset: usize,
115 ack_in_flight: bool,
116 pending_ack: Option<Ack>,
126 acked_since_recv: bool,
131}
132
133impl UringTransport {
134 fn new(tcp_stream: &TcpStream) -> io::Result<Self> {
135 use io_uring::{IoUring, opcode, types};
136 use std::os::unix::io::AsRawFd;
137
138 let tcp_fd = tcp_stream.as_raw_fd();
139
140 let mut ring: IoUring = IoUring::builder()
141 .setup_single_issuer()
142 .build(64)
143 .map_err(|e| io::Error::other(format!("io_uring init failed: {e}")))?;
144
145 ring.submitter()
146 .register_files(&[tcp_fd])
147 .map_err(|e| io::Error::other(format!("io_uring register_files failed: {e}")))?;
148
149 arm_tcp_quickack(tcp_fd);
150
151 {
153 let mut cpuset: libc::cpu_set_t = unsafe { std::mem::zeroed() };
154 unsafe { libc::CPU_SET(0, &mut cpuset) };
155 let _ = ring.submitter().register_iowq_aff(&cpuset);
156 }
157
158 let mut recv_pool: Vec<u8> = vec![0u8; NUM_RECV_BUFFERS as usize * RECV_BUF_SIZE];
159 let pool_ptr = recv_pool.as_mut_ptr();
160
161 {
163 let sqe = opcode::ProvideBuffers::new(
164 pool_ptr,
165 RECV_BUF_SIZE as i32,
166 NUM_RECV_BUFFERS,
167 RECV_BUF_GROUP_ID,
168 0,
169 )
170 .build()
171 .user_data(TOKEN_PROVIDE);
172 unsafe { ring.submission().push(&sqe).expect("SQ full") };
173 ring.submit_and_wait(1)?;
174 let cqe = ring
175 .completion()
176 .next()
177 .ok_or_else(|| io::Error::other("no cqe after ProvideBuffers"))?;
178 if cqe.result() < 0 {
179 return Err(io::Error::other(format!(
180 "ProvideBuffers failed: {}",
181 cqe.result()
182 )));
183 }
184 }
185
186 let sqe = opcode::RecvMulti::new(types::Fd(tcp_fd), RECV_BUF_GROUP_ID)
188 .build()
189 .user_data(TOKEN_RECV);
190 unsafe { ring.submission().push(&sqe).expect("SQ full") };
191
192 Ok(UringTransport {
193 ring,
194 tcp_fd,
195 recv_pool,
196 pool_ptr,
197 multishot_active: true,
198 connected: true,
199 ack_buf: Vec::with_capacity(64),
200 ack_offset: 0,
201 ack_in_flight: false,
202 pending_ack: None,
203 acked_since_recv: false,
204 })
205 }
206
207 fn push_ack_send(&mut self, ack: &Ack) {
213 use io_uring::{opcode, types};
214
215 debug_assert!(!self.ack_in_flight);
216 self.ack_buf.clear();
217 encode_ack(ack, &mut self.ack_buf);
218 let sqe = opcode::Send::new(
219 types::Fixed(0),
220 self.ack_buf.as_ptr(),
221 self.ack_buf.len() as u32,
222 )
223 .build()
224 .user_data(TOKEN_SEND);
225 unsafe { self.ring.submission().push(&sqe).expect("SQ full") };
229 self.ack_in_flight = true;
230 self.ack_offset = 0;
231 self.acked_since_recv = true;
232 }
233
234 fn settle_unprocessed(&mut self, rest: &[(u64, i32, u32)]) {
244 for &(token, _result, flags) in rest {
245 match token {
246 TOKEN_RECV if (flags & IORING_CQE_F_MORE) == 0 => self.multishot_active = false,
247 TOKEN_SEND => self.ack_in_flight = false,
248 _ => {}
249 }
250 }
251 }
252
253 fn drain(&mut self) -> bool {
266 loop {
272 match self.ring.submit() {
273 Ok(_) => break,
274 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
275 Err(_) => return false,
276 }
277 }
278 let mut backoff = DrainBackoff::new();
279 while self.multishot_active || self.ack_in_flight {
280 for cqe in self.ring.completion() {
281 match cqe.user_data() {
282 TOKEN_RECV if (cqe.flags() & IORING_CQE_F_MORE) == 0 => {
288 self.multishot_active = false;
289 }
290 TOKEN_SEND => self.ack_in_flight = false,
294 _ => {}
298 }
299 }
300 if (self.multishot_active || self.ack_in_flight) && !backoff.wait() {
301 return false;
302 }
303 }
304 true
305 }
306}
307
308impl Drop for UringTransport {
309 fn drop(&mut self) {
310 wake_pending_ops(self.tcp_fd);
311 if !self.drain() {
312 std::mem::forget(std::mem::take(&mut self.recv_pool));
318 std::mem::forget(std::mem::take(&mut self.ack_buf));
319 tracing::warn!(
320 multishot_active = self.multishot_active,
321 ack_in_flight = self.ack_in_flight,
322 "io_uring teardown drain did not complete; leaking receiver buffers"
323 );
324 }
325 }
326}
327
328impl ReceiverTransport for UringTransport {
329 fn poll_recv(&mut self, recv_buf: &mut Vec<u8>) -> io::Result<bool> {
330 use io_uring::{opcode, types};
331
332 if !self.connected {
333 return Err(io::Error::other("not connected"));
334 }
335
336 let pending = self.ring.submission().len();
339 if pending > 0 {
340 self.ring.submit()?;
341 }
342
343 let mut cqes: [(u64, i32, u32); 16] = [(0, 0, 0); 16];
345 let mut cqe_count = 0;
346 {
347 let mut cq = self.ring.completion();
348 while cqe_count < cqes.len() {
349 match cq.next() {
350 Some(cqe) => {
351 cqes[cqe_count] = (cqe.user_data(), cqe.result(), cqe.flags());
352 cqe_count += 1;
353 }
354 None => break,
355 }
356 }
357 }
358
359 let mut any_recv = false;
360 let mut submit_chained_ack = false;
364
365 for (idx, &(token, result, flags)) in cqes[..cqe_count].iter().enumerate() {
366 match token {
371 TOKEN_RECV => {
372 if (flags & IORING_CQE_F_MORE) == 0 {
373 self.multishot_active = false;
374 }
375 if result < 0 {
376 if result == -libc::ENOBUFS {
377 tracing::debug!("recv multishot ENOBUFS — pool exhausted");
378 continue;
379 }
380 self.connected = false;
381 self.settle_unprocessed(&cqes[idx + 1..cqe_count]);
382 return Err(io::Error::other(format!(
383 "primary disconnected (recv returned {result})"
384 )));
385 }
386 if result == 0 {
387 self.connected = false;
388 self.settle_unprocessed(&cqes[idx + 1..cqe_count]);
389 return Err(io::Error::other("primary disconnected (recv returned 0)"));
390 }
391 if (flags & IORING_CQE_F_BUFFER) == 0 {
392 self.connected = false;
393 self.settle_unprocessed(&cqes[idx + 1..cqe_count]);
394 return Err(io::Error::other("recv cqe missing F_BUFFER flag"));
395 }
396
397 let n = result as usize;
398 let buf_id = (flags >> IORING_CQE_BUFFER_SHIFT) as usize;
399 let buf_ptr = unsafe { self.pool_ptr.add(buf_id * RECV_BUF_SIZE) };
402 let slice = unsafe { std::slice::from_raw_parts(buf_ptr, n) };
403 recv_buf.extend_from_slice(slice);
404 any_recv = true;
405
406 let provide_sqe = opcode::ProvideBuffers::new(
408 buf_ptr,
409 RECV_BUF_SIZE as i32,
410 1,
411 RECV_BUF_GROUP_ID,
412 buf_id as u16,
413 )
414 .build()
415 .user_data(TOKEN_PROVIDE);
416 unsafe { self.ring.submission().push(&provide_sqe).expect("SQ full") };
417 }
418
419 TOKEN_PROVIDE if result < 0 => {
420 tracing::debug!("ProvideBuffers re-provision failed: {result}");
421 }
422
423 TOKEN_SEND => {
424 if result < 0 {
425 self.connected = false;
426 self.ack_in_flight = false;
429 self.settle_unprocessed(&cqes[idx + 1..cqe_count]);
430 return Err(io::Error::other(format!(
431 "ack send error (returned {result})"
432 )));
433 }
434 let sent = result as usize;
435 self.ack_offset += sent;
436 if self.ack_offset >= self.ack_buf.len() {
437 self.ack_buf.clear();
438 self.ack_offset = 0;
439 self.ack_in_flight = false;
440 if let Some(ack) = self.pending_ack.take() {
447 self.push_ack_send(&ack);
448 submit_chained_ack = true;
449 }
450 } else {
451 let sqe = opcode::Send::new(
453 types::Fixed(0),
454 self.ack_buf[self.ack_offset..].as_ptr(),
455 (self.ack_buf.len() - self.ack_offset) as u32,
456 )
457 .build()
458 .user_data(TOKEN_SEND);
459 unsafe { self.ring.submission().push(&sqe).expect("SQ full") };
460 }
461 }
462
463 _ => {}
464 }
465 }
466
467 if !self.multishot_active {
469 let sqe = opcode::RecvMulti::new(types::Fd(self.tcp_fd), RECV_BUF_GROUP_ID)
470 .build()
471 .user_data(TOKEN_RECV);
472 unsafe { self.ring.submission().push(&sqe).expect("SQ full") };
473 self.multishot_active = true;
474 }
475
476 if submit_chained_ack {
479 self.ring.submit()?;
480 }
481
482 if any_recv {
483 if self.acked_since_recv {
490 self.acked_since_recv = false;
491 } else {
492 arm_tcp_quickack(self.tcp_fd);
493 }
494 }
495
496 Ok(any_recv)
497 }
498
499 fn send_ack(&mut self, ack: &Ack) -> io::Result<bool> {
500 if self.ack_in_flight {
501 self.pending_ack = Some(*ack);
505 return Ok(true);
506 }
507 self.push_ack_send(ack);
508 Ok(true)
509 }
510
511 fn ack_in_flight(&self) -> bool {
512 self.ack_in_flight || self.pending_ack.is_some()
517 }
518
519 fn is_connected(&mut self) -> bool {
520 self.connected
521 }
522}
523
524struct TcpFrameSource<'a> {
532 reader: &'a mut TcpStream,
533}
534
535impl ControlFrameSource for TcpFrameSource<'_> {
536 fn next_frame(
537 &mut self,
538 max_size: usize,
539 ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
540 Ok(read_frame(self.reader, max_size)?)
541 }
542}
543
544pub type ReceiverResult<A, W> = Result<Option<(A, W)>, Box<dyn std::error::Error>>;
546
547#[allow(clippy::too_many_arguments)]
548pub fn run_receiver<A>(
549 primary_addr: SocketAddr,
550 store: &NodeStore,
553 signing_key: &ed25519_dalek::SigningKey,
554 shutdown: &AtomicBool,
555 control: &super::ReplicaControlPlane,
559 snapshot_interval_ms: u64,
560 cores: crate::layout::PipelineCores,
561 staging_mode: melin_journal::StagingMode,
564 group_commit_delay: std::time::Duration,
565 pipeline_depth: usize,
566 fence_state: std::sync::Arc<melin_transport_core::fence::FenceState>,
567 sizing: &A::Sizing,
570) -> ReceiverResult<A, BufferedWriter<A::Event>>
571where
572 A: Application + Send + 'static,
573 A::Event: Send + Sync + 'static,
574 A::Report: Send + 'static,
575 A::QueryResponse: Send + 'static,
576{
577 let super::ReplicaControlPlane {
578 promote,
579 tip_ready,
580 journal_tip,
581 primary_link_up,
582 primary_observed: _,
584 lineage_ack_policy,
585 pipeline_healthy,
586 } = control;
587 let (mut app, mut journal_writer, mut last_sequence, mut chain_hash) =
591 recover_replica_state::<A, BufferedWriter<A::Event>>(
592 store,
593 &fence_state,
594 lineage_ack_policy,
595 sizing,
596 )?;
597 journal_tip.advance(melin_transport_core::WireSeq::new(last_sequence));
603 tip_ready.store(true, Ordering::Release);
604
605 let mut backoff = std::time::Duration::from_secs(1);
606
607 let mut divergence_resyncs: u32 = 0;
610
611 let mut send_buf = Vec::with_capacity(64);
612 let mut pipeline: Option<ReplicaPipelineHandles<A, BufferedWriter<A::Event>>> = None;
613
614 loop {
616 primary_link_up.store(false, Ordering::Release);
619
620 if let Some(p) = pipeline.as_ref() {
621 match p
631 .settled_resume_point(|| shutdown.load(Ordering::Relaxed) || promote.is_requested())
632 {
633 ResumePoint::Settled(fsync_state) => {
634 last_sequence = fsync_state.journal_seq.get();
635 chain_hash = fsync_state.chain_hash;
636 }
637 ResumePoint::Interrupted => {}
639 ResumePoint::JournalFailed => {
642 let after = handle_session_exit(
643 journal_failed_while_disconnected(),
644 &mut pipeline,
645 &mut divergence_resyncs,
646 &mut backoff,
647 last_sequence,
648 store,
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 store,
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 store.journal(),
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 store,
892 group_commit_delay,
893 Arc::clone(&fence_state),
894 lineage_ack_policy,
895 Arc::clone(pipeline_healthy),
896 sizing,
897 )?);
898 }
899
900 let result = {
902 let p = pipeline.as_mut().expect("pipeline must exist by here");
903 let input_producer = &mut p.input_producer;
904 let journal_cursor = p.journal_cursor.as_ref();
905 let stream_marks = &p.stream_marks;
906 let journal_failed = &p.journal_failed;
907 std::thread::scope(|s| {
908 let receiver = || {
909 melin_app::affinity::pin_thread("replica-receiver", cores.reader.core);
910 let mut transport = match UringTransport::new(&tcp_writer) {
911 Ok(t) => t,
912 Err(e) => {
913 tracing::error!(error = %e, "UringTransport init failed");
914 return super::receiver_transport::StreamingResult {
920 exit: SessionExit::Disconnected,
921 heard_from_primary: false,
922 };
923 }
924 };
925 streaming_loop::<UringTransport, A::Event>(
926 &mut transport,
927 input_producer,
928 journal_cursor,
929 shutdown,
930 control,
931 pipeline_depth,
932 cores.reader.wait,
933 session_start,
934 Vec::with_capacity(MAX_DATA_FRAME + 4),
935 None,
936 stream_marks,
937 journal_failed,
938 )
939 };
940 let handle = melin_app::affinity::with_child_context(
943 "replica-receiver",
944 cores.reader.core,
945 || {
946 std::thread::Builder::new()
947 .name("replica-receiver".into())
948 .spawn_scoped(s, receiver)
949 },
950 )
951 .expect("spawn replica-receiver thread");
952 handle.join().expect("replica-receiver thread panicked")
953 })
954 };
955
956 primary_link_up.store(false, Ordering::Release);
957 let after = handle_session_exit(
958 result,
959 &mut pipeline,
960 &mut divergence_resyncs,
961 &mut backoff,
962 last_sequence,
963 store,
964 &fence_state,
965 lineage_ack_policy,
966 shutdown,
967 promote,
968 || {},
972 sizing,
973 );
974 if let Some(r) = after.adopt(
975 &mut app,
976 &mut journal_writer,
977 &mut last_sequence,
978 &mut chain_hash,
979 ) {
980 return r;
981 }
982 }
983}
984
985#[cfg(test)]
990mod tests {
991 use super::*;
992 use crate::uring_teardown::DRAIN_TIMEOUT;
993 use std::io::Read;
994 use std::net::TcpListener;
995 use std::time::{Duration, Instant};
996
997 use melin_transport_core::replication::protocol::{ReplicaMessage, decode_replica_message};
998 use melin_wire_protocol::framing::split_frame_limited;
999
1000 #[cfg(not(feature = "no-persist"))]
1005 fn store(journal: &std::path::Path) -> NodeStore {
1006 NodeStore::resolve(journal.to_path_buf(), None).expect("store")
1007 }
1008
1009 fn socket_pair() -> (TcpStream, TcpStream) {
1011 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1012 let addr = listener.local_addr().unwrap();
1013 let client = TcpStream::connect(addr).unwrap();
1014 let (server, _) = listener.accept().unwrap();
1015 (client, server)
1016 }
1017
1018 fn ack(seq: u64) -> Ack {
1019 Ack {
1020 acked_sequence: seq,
1021 in_memory_sequence: seq,
1022 }
1023 }
1024
1025 fn flush_acks(transport: &mut UringTransport) {
1027 let mut recv_buf = Vec::new();
1028 let deadline = Instant::now() + Duration::from_secs(5);
1029 while transport.ack_in_flight() {
1030 transport.poll_recv(&mut recv_buf).unwrap();
1031 assert!(Instant::now() < deadline, "ack send never completed");
1032 std::hint::spin_loop();
1033 }
1034 }
1035
1036 fn read_acks(peer: &mut TcpStream, expect: usize) -> Vec<Ack> {
1038 peer.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
1039 let mut buf = Vec::new();
1040 let mut chunk = [0u8; 256];
1041 let mut acks = Vec::new();
1042 while acks.len() < expect {
1043 let n = peer.read(&mut chunk).expect("peer read");
1044 assert!(n > 0, "peer closed before all acks arrived");
1045 buf.extend_from_slice(&chunk[..n]);
1046 while let Some(frame) = split_frame_limited(&buf, MAX_CONTROL_FRAME).unwrap() {
1048 match decode_replica_message(frame.payload).expect("decodable frame") {
1049 ReplicaMessage::Ack(a) => acks.push(a),
1050 other => panic!("expected Ack frame, got {other:?}"),
1051 }
1052 let consumed = frame.consumed();
1053 buf.drain(..consumed);
1054 }
1055 }
1056 assert!(buf.is_empty(), "trailing bytes after expected acks");
1057 acks
1058 }
1059
1060 #[test]
1061 fn coalesces_to_latest_ack_while_send_in_flight() {
1062 let (client, mut peer) = socket_pair();
1063 let mut transport = UringTransport::new(&client).unwrap();
1064
1065 assert!(transport.send_ack(&ack(1)).unwrap());
1070 assert!(transport.ack_in_flight());
1071 assert!(transport.send_ack(&ack(2)).unwrap());
1072 assert!(transport.send_ack(&ack(3)).unwrap());
1073
1074 flush_acks(&mut transport);
1075
1076 let acks = read_acks(&mut peer, 2);
1078 let seqs: Vec<u64> = acks.iter().map(|a| a.acked_sequence).collect();
1079 assert_eq!(seqs, vec![1, 3]);
1080 }
1081
1082 #[test]
1083 fn sequential_acks_all_reach_the_wire_in_order() {
1084 let (client, mut peer) = socket_pair();
1085 let mut transport = UringTransport::new(&client).unwrap();
1086
1087 for seq in 1..=3 {
1088 assert!(transport.send_ack(&ack(seq)).unwrap());
1089 flush_acks(&mut transport);
1090 }
1091
1092 let acks = read_acks(&mut peer, 3);
1093 let seqs: Vec<u64> = acks.iter().map(|a| a.acked_sequence).collect();
1094 assert_eq!(seqs, vec![1, 2, 3]);
1095 }
1096
1097 #[test]
1105 fn drop_quiesces_the_multishot_recv() {
1106 let (client, _peer) = socket_pair();
1107 let mut transport = UringTransport::new(&client).unwrap();
1108 let mut recv_buf = Vec::new();
1111 transport.poll_recv(&mut recv_buf).unwrap();
1112 assert!(transport.multishot_active);
1113
1114 let started = Instant::now();
1115 drop(transport);
1116 let elapsed = started.elapsed();
1117 assert!(
1118 elapsed < DRAIN_TIMEOUT / 4,
1119 "teardown took {elapsed:?}, close to the {DRAIN_TIMEOUT:?} deadline"
1120 );
1121 }
1122
1123 #[test]
1130 fn abandoned_cqes_still_settle_kernel_ownership() {
1131 let (client, _peer) = socket_pair();
1132 let mut transport = UringTransport::new(&client).unwrap();
1133 let mut recv_buf = Vec::new();
1134 transport.poll_recv(&mut recv_buf).unwrap();
1135
1136 transport.ack_in_flight = true;
1139 transport.settle_unprocessed(&[(TOKEN_SEND, 13, 0)]);
1140 assert!(
1141 !transport.ack_in_flight,
1142 "an abandoned SEND completion must release `ack_buf`"
1143 );
1144
1145 transport.settle_unprocessed(&[(TOKEN_RECV, 128, IORING_CQE_F_MORE)]);
1148 assert!(
1149 transport.multishot_active,
1150 "F_MORE must leave the multishot armed"
1151 );
1152 transport.settle_unprocessed(&[(TOKEN_RECV, 0, 0)]);
1154 assert!(!transport.multishot_active);
1155
1156 transport.multishot_active = true;
1160 }
1161
1162 #[test]
1166 fn drop_quiesces_a_pending_ack_send() {
1167 let (client, _peer) = socket_pair();
1168 let mut transport = UringTransport::new(&client).unwrap();
1169 assert!(transport.send_ack(&ack(7)).unwrap());
1170 assert!(transport.ack_in_flight());
1171
1172 let started = Instant::now();
1173 drop(transport);
1174 let elapsed = started.elapsed();
1175 assert!(
1176 elapsed < DRAIN_TIMEOUT / 4,
1177 "teardown took {elapsed:?}, close to the {DRAIN_TIMEOUT:?} deadline"
1178 );
1179 }
1180
1181 #[cfg(not(feature = "no-persist"))]
1186 mod scripted {
1187 use super::*;
1188 use melin_app::{AppEvent, Application, ApplyCtx, CodecError, QueryCtx, RejectReason};
1189
1190 pub(super) use melin_transport_core::pipeline::{InputSlot, RequestId};
1193
1194 #[derive(Debug, Clone, Copy, PartialEq)]
1195 pub(super) struct EvtAdd(pub(super) u64);
1196
1197 impl AppEvent for EvtAdd {
1198 const MAX_ENCODED_SIZE: usize = 8;
1199
1200 fn encoded_size(&self) -> usize {
1201 8
1202 }
1203 fn encode(&self, buf: &mut [u8]) -> usize {
1204 buf[..8].copy_from_slice(&self.0.to_le_bytes());
1205 8
1206 }
1207 fn decode(buf: &[u8]) -> Result<Self, CodecError> {
1208 if buf.len() < 8 {
1209 return Err(CodecError::Truncated);
1210 }
1211 Ok(EvtAdd(u64::from_le_bytes(buf[..8].try_into().expect("8"))))
1212 }
1213 fn is_query(&self) -> bool {
1214 false
1215 }
1216 }
1217
1218 #[derive(Debug, Clone, Copy)]
1219 pub(super) struct Rpt;
1220
1221 #[derive(Default)]
1222 pub(super) struct App;
1223
1224 impl Application for App {
1225 type Event = EvtAdd;
1226 type Report = Rpt;
1227 type QueryResponse = Rpt;
1228 type Sizing = ();
1229 const APP_VERSION: u16 = 1;
1230
1231 fn apply(&mut self, _e: EvtAdd, _ctx: &ApplyCtx, _out: &mut Vec<Rpt>) {}
1232 fn query(&self, _e: EvtAdd, _ctx: &QueryCtx) -> Option<Rpt> {
1233 None
1234 }
1235 fn tick(&mut self, _now_ns: u64, _out: &mut Vec<Rpt>) {}
1236 fn build_reject(_e: &EvtAdd, _r: RejectReason) -> Rpt {
1237 Rpt
1238 }
1239 fn snapshot<W: std::io::Write>(&self, _w: &mut W) -> std::io::Result<()> {
1240 Ok(())
1241 }
1242 fn restore<R: std::io::Read>(_r: &mut R) -> std::io::Result<Self> {
1243 Ok(App)
1244 }
1245 }
1246
1247 pub(super) const REPLICA_KEY: [u8; 32] = [0xFC; 32];
1250
1251 pub(super) fn replica_auth() -> melin_app::auth::AuthorizedKeys {
1254 let key = ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY);
1255 let pub_b64 = base64::Engine::encode(
1256 &base64::engine::general_purpose::STANDARD,
1257 key.verifying_key().to_bytes(),
1258 );
1259 melin_app::auth::AuthorizedKeys::parse::<melin_app::auth::NoRoles>(&format!(
1260 "replication {pub_b64} test-replica\n"
1261 ))
1262 .expect("parse keys")
1263 }
1264
1265 pub(super) fn read_replica_msg(stream: &mut TcpStream) -> ReplicaMessage {
1267 let mut len_buf = [0u8; 4];
1268 stream.read_exact(&mut len_buf).expect("frame length");
1269 let len = u32::from_le_bytes(len_buf) as usize;
1270 let mut payload = vec![0u8; len];
1271 stream.read_exact(&mut payload).expect("frame payload");
1272 decode_replica_message(&payload).expect("decodable replica frame")
1273 }
1274
1275 pub(super) fn wait_for_ack(stream: &mut TcpStream, seq: u64) {
1277 loop {
1278 if let ReplicaMessage::Ack(a) = read_replica_msg(stream)
1279 && a.acked_sequence >= seq
1280 {
1281 return;
1282 }
1283 }
1284 }
1285
1286 pub(super) fn accept_within(listener: &std::net::TcpListener, secs: u64) -> TcpStream {
1289 let deadline = Instant::now() + Duration::from_secs(secs);
1290 loop {
1291 match listener.accept() {
1292 Ok((s, _)) => {
1293 s.set_read_timeout(Some(Duration::from_secs(10)))
1294 .expect("read timeout");
1295 return s;
1296 }
1297 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
1298 assert!(Instant::now() < deadline, "replica never connected");
1299 std::thread::sleep(Duration::from_millis(10));
1300 }
1301 Err(e) => panic!("accept failed: {e}"),
1302 }
1303 }
1304 }
1305 }
1306
1307 #[cfg(all(feature = "hash-chain", not(feature = "no-persist")))]
1313 mod divergence_resync {
1314 use super::super::super::auth::authenticate_replica;
1315 use super::scripted::*;
1316 use super::*;
1317 use melin_journal::{
1318 AckPolicyByte, AckPolicyChange, BufferedWriter, JournalEvent, JournalWrite,
1319 };
1320 use melin_transport_core::cursors::WireSeq;
1321 use melin_transport_core::replication::catchup::{lineage_origin, snapshot_transfer_with};
1322 use melin_transport_core::replication::protocol::{
1323 PrimaryMessage, decode_primary_message, encode_hash_mismatch, encode_rotate,
1324 encode_stream_start,
1325 };
1326 use std::sync::Arc;
1327 use std::sync::atomic::AtomicBool;
1328
1329 const DIVERGENT_POLICY: AckPolicyByte = AckPolicyByte(1);
1332
1333 const PRIMARY_POLICY: AckPolicyByte = AckPolicyByte(0);
1336
1337 const PRIMARY_LINEAGE_POLICY: melin_transport_core::lineage::LineagePolicy =
1340 melin_transport_core::lineage::LineagePolicy {
1341 change: AckPolicyChange {
1342 policy: PRIMARY_POLICY,
1343 unwitnessed: true,
1344 },
1345 wire_seq: WireSeq::new(3),
1346 previous: Some(AckPolicyByte(2)),
1347 };
1348
1349 const SHADOW_INTERVAL_MS: u64 = 50;
1351
1352 #[test]
1361 fn mid_stream_divergence_resyncs_in_process() {
1362 let dir = tempfile::tempdir().expect("tempdir");
1363
1364 let primary_journal = dir.path().join("primary.journal");
1368 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1369 let mut chain_at_4 = [0u8; 32];
1370 for v in 1..=5u64 {
1371 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1372 if v == 2 {
1373 w.rotate_segment().expect("rotate");
1374 }
1375 if v == 4 {
1376 chain_at_4 = w.chain_hash().expect("chain");
1377 }
1378 }
1379 drop(w);
1380 melin_transport_core::snapshot::save::<App>(
1384 &App,
1385 WireSeq::new(4),
1386 chain_at_4,
1387 melin_transport_core::lineage::Lineage {
1388 epoch: 0,
1389 ack_policy: Some(PRIMARY_LINEAGE_POLICY),
1390 },
1391 Some(0),
1392 store(&primary_journal).snapshot(),
1393 )
1394 .expect("save snapshot");
1395 let origin = lineage_origin(&primary_journal).expect("lineage");
1396 let (lineage_start, lineage_anchor, lineage_genesis) = (
1397 origin.starting_sequence,
1398 origin.anchor_hash,
1399 origin.genesis_entries,
1400 );
1401
1402 let authorized_keys = replica_auth();
1404
1405 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1406 listener.set_nonblocking(true).expect("nonblocking");
1407 let addr = listener.local_addr().expect("addr");
1408
1409 let replica_journal = dir.path().join("replica.journal");
1411 let replica_snapshot_check = store(&replica_journal).snapshot().to_path_buf();
1412 let shutdown = Arc::new(AtomicBool::new(false));
1413 let control = crate::replication::ReplicaControlPlane::new();
1414 let cores = crate::layout::PipelineCores::unpinned();
1415 let replica = {
1416 let journal = replica_journal.clone();
1417 let shutdown = Arc::clone(&shutdown);
1418 let control = control.clone();
1419 std::thread::spawn(move || -> Result<bool, String> {
1420 run_receiver::<App>(
1421 addr,
1422 &store(&journal),
1423 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1424 &shutdown,
1425 &control,
1426 SHADOW_INTERVAL_MS,
1430 cores,
1431 melin_journal::StagingMode::ZeroFill,
1432 Duration::ZERO,
1433 64,
1434 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1435 &(),
1436 )
1437 .map(|state| state.is_none())
1439 .map_err(|e| e.to_string())
1440 })
1441 };
1442
1443 let mut s1 = accept_within(&listener, 30);
1446 let mut s1r = s1.try_clone().expect("clone");
1447 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1448 match read_replica_msg(&mut s1) {
1449 ReplicaMessage::Handshake(h) => {
1450 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
1451 }
1452 other => panic!("expected Handshake, got {other:?}"),
1453 }
1454 let mut buf = Vec::new();
1455 encode_stream_start(
1456 0,
1457 lineage_start,
1458 lineage_anchor,
1459 lineage_genesis,
1460 0,
1461 &mut buf,
1462 );
1463 s1.write_all(&buf).expect("StreamStart 1");
1464 buf.clear();
1465 melin_transport_core::replication_wire::encode_input_batch(
1469 &[InputSlot::<EvtAdd> {
1470 connection_id: 0,
1471 request_id: RequestId(0),
1472 key_hash: 0,
1473 sequence: 1,
1474 timestamp_ns: 1,
1475 event: JournalEvent::AckPolicyChange(AckPolicyChange {
1476 policy: DIVERGENT_POLICY,
1477 unwitnessed: false,
1478 }),
1479 publish_ts: Default::default(),
1481 recv_ts: Default::default(),
1482 }],
1483 &mut buf,
1484 )
1485 .expect("encode InputBatch");
1486 s1.write_all(&buf).expect("InputBatch");
1487 wait_for_ack(&mut s1, 1);
1488
1489 buf.clear();
1492 encode_rotate(1, &[0xEE; 32], &mut buf);
1493 s1.write_all(&buf).expect("Rotate");
1494
1495 let mut s2 = accept_within(&listener, 30);
1498 let mut s2r = s2.try_clone().expect("clone");
1499 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1500 match read_replica_msg(&mut s2) {
1501 ReplicaMessage::Handshake(h) => assert_eq!(
1502 h.last_sequence, 1,
1503 "post-divergence handshake must carry the recovered journal position"
1504 ),
1505 other => panic!("expected Handshake, got {other:?}"),
1506 }
1507 assert!(
1513 !control.pipeline_healthy.load(Ordering::Acquire),
1514 "the divergence must have latched the health mirror false"
1515 );
1516 assert_eq!(
1520 control.lineage_ack_policy.load(),
1521 Some(melin_transport_core::lineage::LineagePolicy {
1522 change: AckPolicyChange {
1523 policy: DIVERGENT_POLICY,
1524 unwitnessed: false,
1525 },
1526 wire_seq: WireSeq::new(1),
1527 previous: None,
1528 }),
1529 "recovery must store the forked journal's policy"
1530 );
1531 buf.clear();
1532 encode_hash_mismatch(&mut buf);
1533 s2.write_all(&buf).expect("HashMismatch");
1534 let transfer_shutdown = AtomicBool::new(false);
1535 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1536 s2.write_all(b)?;
1537 s2.flush()
1538 };
1539 let end = snapshot_transfer_with::<EvtAdd>(
1542 &store(&primary_journal),
1543 &mut publish,
1544 &transfer_shutdown,
1545 )
1546 .expect("snapshot transfer");
1547 assert_eq!(
1548 end,
1549 melin_transport_core::replication::catchup::CatchUpResult::Ok(5)
1550 );
1551 let mut s2_acks = s2.try_clone().expect("clone");
1552 wait_for_ack(&mut s2_acks, 5);
1553
1554 assert!(
1557 control.pipeline_healthy.load(Ordering::Acquire),
1558 "the resync rebuild must reset the health mirror true"
1559 );
1560 assert_eq!(
1564 control.lineage_ack_policy.load(),
1565 Some(PRIMARY_LINEAGE_POLICY),
1566 "the resync must adopt the primary snapshot's policy and drop the divergent tail's"
1567 );
1568 let deadline = std::time::Instant::now() + Duration::from_secs(30);
1572 let shadow_lineage = loop {
1573 if let Ok((_, header)) =
1575 melin_transport_core::snapshot::load::<App>(&replica_snapshot_check)
1576 && header.sequence == 5
1577 {
1578 break header.lineage;
1579 }
1580 assert!(
1581 std::time::Instant::now() < deadline,
1582 "the rebuilt shadow never snapshotted entry 5"
1583 );
1584 std::thread::sleep(Duration::from_millis(10));
1585 };
1586 assert_eq!(
1587 shadow_lineage.ack_policy,
1588 Some(PRIMARY_LINEAGE_POLICY),
1589 "the replica's shadow must seed its lineage from the adopted policy"
1590 );
1591
1592 shutdown.store(true, Ordering::Relaxed);
1595 let result = replica.join().expect("replica thread panicked");
1596 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
1597
1598 assert!(
1601 dir.path().join("replica.journal.divergent.0").exists(),
1602 "divergent lineage must be archived"
1603 );
1604 assert!(replica_journal.exists(), "re-seeded live journal");
1605 }
1606
1607 #[test]
1626 fn resync_transfer_failure_rebuilds_clean_not_over_stale_writer() {
1627 let dir = tempfile::tempdir().expect("tempdir");
1628
1629 let primary_journal = dir.path().join("primary.journal");
1634 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1635 let mut chain_at_2 = [0u8; 32];
1636 for v in 1..=3u64 {
1637 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1638 if v == 2 {
1639 chain_at_2 = w.chain_hash().expect("chain");
1640 }
1641 }
1642 drop(w);
1643 melin_transport_core::snapshot::save::<App>(
1644 &App,
1645 WireSeq::new(2),
1646 chain_at_2,
1647 melin_transport_core::lineage::Lineage {
1648 epoch: 0,
1649 ack_policy: Some(PRIMARY_LINEAGE_POLICY),
1650 },
1651 Some(0),
1652 store(&primary_journal).snapshot(),
1653 )
1654 .expect("save snapshot");
1655 let origin = lineage_origin(&primary_journal).expect("lineage");
1656 let (lineage_start, lineage_anchor, lineage_genesis) = (
1657 origin.starting_sequence,
1658 origin.anchor_hash,
1659 origin.genesis_entries,
1660 );
1661
1662 let authorized_keys = replica_auth();
1663
1664 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1665 listener.set_nonblocking(true).expect("nonblocking");
1666 let addr = listener.local_addr().expect("addr");
1667
1668 let replica_journal = dir.path().join("replica.journal");
1669 let shutdown = Arc::new(AtomicBool::new(false));
1670 let control = crate::replication::ReplicaControlPlane::new();
1671 let cores = crate::layout::PipelineCores::unpinned();
1672 let replica = {
1673 let journal = replica_journal.clone();
1674 let shutdown = Arc::clone(&shutdown);
1675 let control = control.clone();
1676 std::thread::spawn(move || -> Result<bool, String> {
1677 run_receiver::<App>(
1678 addr,
1679 &store(&journal),
1680 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1681 &shutdown,
1682 &control,
1683 3_600_000,
1684 cores,
1685 melin_journal::StagingMode::ZeroFill,
1686 Duration::ZERO,
1687 64,
1688 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1689 &(),
1690 )
1691 .map(|state| state.is_none())
1692 .map_err(|e| e.to_string())
1693 })
1694 };
1695
1696 let stream_one = |s: &mut TcpStream, buf: &mut Vec<u8>| {
1699 buf.clear();
1700 encode_stream_start(0, lineage_start, lineage_anchor, lineage_genesis, 0, buf);
1701 s.write_all(buf).expect("StreamStart");
1702 buf.clear();
1703 melin_transport_core::replication_wire::encode_input_batch(
1704 &[InputSlot::<EvtAdd> {
1705 connection_id: 0,
1706 request_id: RequestId(0),
1707 key_hash: 0,
1708 sequence: 1,
1709 timestamp_ns: 1,
1710 event: JournalEvent::App(EvtAdd(1)),
1711 publish_ts: Default::default(),
1712 recv_ts: Default::default(),
1713 }],
1714 buf,
1715 )
1716 .expect("encode InputBatch");
1717 s.write_all(buf).expect("InputBatch");
1718 };
1719
1720 let mut buf = Vec::new();
1725 let mut s1 = accept_within(&listener, 30);
1726 let mut s1r = s1.try_clone().expect("clone");
1727 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1728 match read_replica_msg(&mut s1) {
1729 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0, "fresh"),
1730 other => panic!("expected Handshake, got {other:?}"),
1731 }
1732 stream_one(&mut s1, &mut buf);
1733 wait_for_ack(&mut s1, 1);
1734 buf.clear();
1735 encode_rotate(1, &[0xEE; 32], &mut buf);
1736 s1.write_all(&buf).expect("Rotate");
1737
1738 let mut s2 = accept_within(&listener, 30);
1745 let mut s2r = s2.try_clone().expect("clone");
1746 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1747 match read_replica_msg(&mut s2) {
1748 ReplicaMessage::Handshake(h) => assert_eq!(
1749 h.last_sequence, 1,
1750 "reconnect carries the recovered forked position"
1751 ),
1752 other => panic!("expected Handshake, got {other:?}"),
1753 }
1754 buf.clear();
1755 encode_hash_mismatch(&mut buf);
1756 s2.write_all(&buf).expect("HashMismatch");
1757 s2.flush().expect("flush");
1758 {
1759 let transfer_shutdown = AtomicBool::new(false);
1760 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1764 if matches!(
1765 decode_primary_message(&b[4..]),
1766 Ok(PrimaryMessage::SegmentSeedBegin { .. })
1767 ) {
1768 return Err(std::io::Error::other("scripted drop before the seed"));
1769 }
1770 s2.write_all(b)?;
1771 s2.flush()
1772 };
1773 snapshot_transfer_with::<EvtAdd>(
1774 &store(&primary_journal),
1775 &mut publish,
1776 &transfer_shutdown,
1777 )
1778 .expect_err("the scripted primary drops before the seed");
1779 }
1780 drop(s2);
1784 drop(s2r);
1785
1786 let mut s3 = accept_within(&listener, 30);
1791 let mut s3r = s3.try_clone().expect("clone");
1792 authenticate_replica(&mut s3r, &authorized_keys).expect("auth 3");
1793 match read_replica_msg(&mut s3) {
1794 ReplicaMessage::Handshake(h) => assert_eq!(
1795 h.last_sequence, 0,
1796 "after a failed transfer the retry handshakes as fresh"
1797 ),
1798 other => panic!("expected Handshake, got {other:?}"),
1799 }
1800 assert_eq!(
1804 control.lineage_ack_policy.load(),
1805 None,
1806 "a failed transfer must not adopt the snapshot's policy"
1807 );
1808 stream_one(&mut s3, &mut buf);
1809 wait_for_ack(&mut s3, 1);
1810
1811 shutdown.store(true, Ordering::Relaxed);
1812 let result = replica.join().expect("replica thread panicked");
1813 assert_eq!(result, Ok(true), "receiver exits cleanly via shutdown");
1814
1815 assert!(
1820 dir.path().join("replica.journal.divergent.0").exists(),
1821 "forked lineage archived, never deleted"
1822 );
1823 assert!(
1824 replica_journal.exists(),
1825 "fresh live journal recreated after the failed transfer"
1826 );
1827 }
1828
1829 #[test]
1836 fn second_mid_stream_divergence_exits_hard() {
1837 let dir = tempfile::tempdir().expect("tempdir");
1838
1839 let primary_journal = dir.path().join("primary.journal");
1840 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1841 let mut chain_at_4 = [0u8; 32];
1842 for v in 1..=5u64 {
1843 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1844 if v == 2 {
1845 w.rotate_segment().expect("rotate");
1846 }
1847 if v == 4 {
1848 chain_at_4 = w.chain_hash().expect("chain");
1849 }
1850 }
1851 drop(w);
1852 melin_transport_core::snapshot::save::<App>(
1853 &App,
1854 WireSeq::new(4),
1855 chain_at_4,
1856 melin_transport_core::lineage::Lineage::GENESIS,
1857 Some(0),
1858 store(&primary_journal).snapshot(),
1859 )
1860 .expect("save snapshot");
1861 let origin = lineage_origin(&primary_journal).expect("lineage");
1862 let (lineage_start, lineage_anchor, lineage_genesis) = (
1863 origin.starting_sequence,
1864 origin.anchor_hash,
1865 origin.genesis_entries,
1866 );
1867
1868 let authorized_keys = replica_auth();
1869
1870 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1871 listener.set_nonblocking(true).expect("nonblocking");
1872 let addr = listener.local_addr().expect("addr");
1873
1874 let replica_journal = dir.path().join("replica.journal");
1875 let shutdown = Arc::new(AtomicBool::new(false));
1876 let control = crate::replication::ReplicaControlPlane::new();
1877 let cores = crate::layout::PipelineCores::unpinned();
1878 let replica = {
1879 let journal = replica_journal.clone();
1880 let shutdown = Arc::clone(&shutdown);
1881 let control = control.clone();
1882 std::thread::spawn(move || -> Result<bool, String> {
1883 run_receiver::<App>(
1884 addr,
1885 &store(&journal),
1886 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1887 &shutdown,
1888 &control,
1889 3_600_000,
1890 cores,
1891 melin_journal::StagingMode::ZeroFill,
1892 Duration::ZERO,
1893 64,
1894 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1895 &(),
1896 )
1897 .map(|state| state.is_none())
1898 .map_err(|e| e.to_string())
1899 })
1900 };
1901
1902 let mut s1 = accept_within(&listener, 30);
1904 let mut s1r = s1.try_clone().expect("clone");
1905 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1906 match read_replica_msg(&mut s1) {
1907 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0),
1908 other => panic!("expected Handshake, got {other:?}"),
1909 }
1910 let mut buf = Vec::new();
1911 encode_stream_start(
1912 0,
1913 lineage_start,
1914 lineage_anchor,
1915 lineage_genesis,
1916 0,
1917 &mut buf,
1918 );
1919 s1.write_all(&buf).expect("StreamStart 1");
1920 buf.clear();
1921 melin_transport_core::replication_wire::encode_input_batch(
1922 &[InputSlot::<EvtAdd> {
1923 connection_id: 0,
1924 request_id: RequestId(0),
1925 key_hash: 0,
1926 sequence: 1,
1927 timestamp_ns: 1,
1928 event: JournalEvent::App(EvtAdd(1)),
1929 publish_ts: Default::default(),
1930 recv_ts: Default::default(),
1931 }],
1932 &mut buf,
1933 )
1934 .expect("encode InputBatch");
1935 s1.write_all(&buf).expect("InputBatch");
1936 wait_for_ack(&mut s1, 1);
1937 buf.clear();
1938 encode_rotate(1, &[0xEE; 32], &mut buf);
1939 s1.write_all(&buf).expect("Rotate 1");
1940
1941 let mut s2 = accept_within(&listener, 30);
1944 let mut s2r = s2.try_clone().expect("clone");
1945 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1946 match read_replica_msg(&mut s2) {
1947 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 1),
1948 other => panic!("expected Handshake, got {other:?}"),
1949 }
1950 buf.clear();
1951 encode_hash_mismatch(&mut buf);
1952 s2.write_all(&buf).expect("HashMismatch");
1953 let transfer_shutdown = AtomicBool::new(false);
1954 {
1955 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1956 s2.write_all(b)?;
1957 s2.flush()
1958 };
1959 snapshot_transfer_with::<EvtAdd>(
1960 &store(&primary_journal),
1961 &mut publish,
1962 &transfer_shutdown,
1963 )
1964 .expect("snapshot transfer");
1965 }
1966 buf.clear();
1967 melin_transport_core::replication_wire::encode_input_batch(
1968 &[InputSlot::<EvtAdd> {
1969 connection_id: 0,
1970 request_id: RequestId(0),
1971 key_hash: 0,
1972 sequence: 6,
1973 timestamp_ns: 6,
1974 event: JournalEvent::App(EvtAdd(6)),
1975 publish_ts: Default::default(),
1976 recv_ts: Default::default(),
1977 }],
1978 &mut buf,
1979 )
1980 .expect("encode InputBatch");
1981 s2.write_all(&buf).expect("InputBatch 6");
1982 let mut s2_acks = s2.try_clone().expect("clone");
1983 wait_for_ack(&mut s2_acks, 6);
1984 buf.clear();
1985 encode_rotate(6, &[0xEE; 32], &mut buf);
1986 s2.write_all(&buf).expect("Rotate 2");
1987
1988 let result = replica.join().expect("replica thread panicked");
1991 let err = result.expect_err("second divergence must exit hard");
1992 assert!(
1993 err.contains("divergence recurred"),
1994 "error must name the recurrence: {err}"
1995 );
1996 }
1997
1998 type PromoteHandles =
2007 crate::replication::ReplicaPipelineHandles<App, BufferedWriter<EvtAdd>>;
2008
2009 #[test]
2010 fn promotion_hands_back_local_state_when_present() {
2011 let dir = tempfile::tempdir().unwrap();
2012 let mut pipeline: Option<PromoteHandles> = None;
2013 let mut app = Some(App);
2014 let mut journal_writer =
2015 Some(BufferedWriter::<EvtAdd>::create(&dir.path().join("p.journal")).unwrap());
2016
2017 let result = crate::replication::take_pipeline_for_promotion(
2018 &mut pipeline,
2019 &mut app,
2020 &mut journal_writer,
2021 );
2022
2023 assert!(matches!(result, Ok(Some(_))), "warm state must be promoted");
2024 assert!(
2025 app.is_none() && journal_writer.is_none(),
2026 "state must be moved into the result"
2027 );
2028 }
2029
2030 #[test]
2031 fn promotion_errs_when_no_local_state() {
2032 let mut pipeline: Option<PromoteHandles> = None;
2033 let mut app: Option<App> = None;
2034 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
2035
2036 let result = crate::replication::take_pipeline_for_promotion(
2037 &mut pipeline,
2038 &mut app,
2039 &mut journal_writer,
2040 );
2041
2042 let err = match result {
2044 Err(e) => e,
2045 Ok(_) => panic!("a promote with nothing to promote must error"),
2046 };
2047 assert!(
2048 err.to_string().contains("no local state available"),
2049 "{err}"
2050 );
2051 }
2052
2053 #[test]
2054 fn promotion_errs_on_partial_state() {
2055 let mut pipeline: Option<PromoteHandles> = None;
2057 let mut app = Some(App);
2058 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
2059
2060 let result = crate::replication::take_pipeline_for_promotion(
2061 &mut pipeline,
2062 &mut app,
2063 &mut journal_writer,
2064 );
2065
2066 assert!(result.is_err(), "partial state is not a valid promotion");
2067 }
2068 }
2069
2070 #[cfg(not(feature = "no-persist"))]
2076 mod backoff_shutdown {
2077 use super::super::super::auth::authenticate_replica;
2078 use super::scripted::*;
2079 use super::*;
2080 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
2081 use melin_transport_core::replication::catchup::lineage_origin;
2082 use melin_transport_core::replication::protocol::encode_stream_start;
2083 use std::sync::Arc;
2084 use std::sync::atomic::AtomicBool;
2085
2086 fn live_thread_names() -> Vec<String> {
2089 std::fs::read_dir("/proc/self/task")
2090 .expect("read /proc/self/task")
2091 .filter_map(|entry| {
2092 let entry = entry.ok()?;
2095 let name = std::fs::read_to_string(entry.path().join("comm")).ok()?;
2096 Some(name.trim().to_string())
2097 })
2098 .collect()
2099 }
2100
2101 const ISOLATED: &str = "MELIN_TEST_BACKOFF_SHUTDOWN_ISOLATED";
2103
2104 #[test]
2117 fn shutdown_during_reconnect_backoff_joins_pipeline_threads() {
2118 if std::env::var_os(ISOLATED).is_some() {
2119 return backoff_shutdown_body();
2120 }
2121 let module = module_path!();
2124 let path = module.split_once("::").map_or(module, |(_, rest)| rest);
2125 let test = format!("{path}::shutdown_during_reconnect_backoff_joins_pipeline_threads");
2126 let output = std::process::Command::new(std::env::current_exe().expect("current_exe"))
2127 .args(["--exact", &test, "--test-threads=1", "--nocapture"])
2128 .env(ISOLATED, "1")
2129 .output()
2130 .expect("run the test in a process of its own");
2131 let stdout = String::from_utf8_lossy(&output.stdout);
2132 let stderr = String::from_utf8_lossy(&output.stderr);
2133 assert!(
2136 output.status.success() && stdout.contains("1 passed"),
2137 "the isolated run failed ({}):\n{stdout}\n{stderr}",
2138 output.status
2139 );
2140 }
2141
2142 fn backoff_shutdown_body() {
2143 let dir = tempfile::tempdir().expect("tempdir");
2144
2145 let primary_journal = dir.path().join("primary.journal");
2148 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
2149 w.append(&JournalEvent::App(EvtAdd(1))).expect("append");
2150 drop(w);
2151 let origin = lineage_origin(&primary_journal).expect("lineage");
2152 let (lineage_start, lineage_anchor, lineage_genesis) = (
2153 origin.starting_sequence,
2154 origin.anchor_hash,
2155 origin.genesis_entries,
2156 );
2157
2158 let authorized_keys = replica_auth();
2159 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2160 listener.set_nonblocking(true).expect("nonblocking");
2161 let addr = listener.local_addr().expect("addr");
2162
2163 let shutdown = Arc::new(AtomicBool::new(false));
2164 let control = crate::replication::ReplicaControlPlane::new();
2165 let replica = {
2166 let journal = dir.path().join("replica.journal");
2167 let shutdown = Arc::clone(&shutdown);
2168 let control = control.clone();
2169 std::thread::spawn(move || -> Result<bool, String> {
2170 run_receiver::<App>(
2171 addr,
2172 &store(&journal),
2173 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
2174 &shutdown,
2175 &control,
2176 3_600_000,
2177 crate::layout::PipelineCores::unpinned(),
2178 melin_journal::StagingMode::ZeroFill,
2179 Duration::ZERO,
2180 64,
2181 Arc::new(melin_transport_core::fence::FenceState::new(0)),
2182 &(),
2183 )
2184 .map(|state| state.is_none())
2186 .map_err(|e| e.to_string())
2187 })
2188 };
2189
2190 let mut s1 = accept_within(&listener, 30);
2194 let mut s1r = s1.try_clone().expect("clone");
2195 authenticate_replica(&mut s1r, &authorized_keys).expect("auth");
2196 match read_replica_msg(&mut s1) {
2197 ReplicaMessage::Handshake(h) => {
2198 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
2199 }
2200 other => panic!("expected Handshake, got {other:?}"),
2201 }
2202 let mut buf = Vec::new();
2203 encode_stream_start(
2204 0,
2205 lineage_start,
2206 lineage_anchor,
2207 lineage_genesis,
2208 0,
2209 &mut buf,
2210 );
2211 s1.write_all(&buf).expect("StreamStart");
2212 buf.clear();
2213 melin_transport_core::replication_wire::encode_input_batch(
2214 &[InputSlot::<EvtAdd> {
2215 connection_id: 0,
2216 request_id: RequestId(0),
2217 key_hash: 0,
2218 sequence: 1,
2219 timestamp_ns: 1,
2220 event: JournalEvent::App(EvtAdd(1)),
2221 publish_ts: Default::default(),
2222 recv_ts: Default::default(),
2223 }],
2224 &mut buf,
2225 )
2226 .expect("encode InputBatch");
2227 s1.write_all(&buf).expect("InputBatch");
2228 wait_for_ack(&mut s1, 1);
2229
2230 drop(s1);
2234 drop(s1r);
2235 drop(listener);
2236 std::thread::sleep(Duration::from_millis(1500));
2241
2242 shutdown.store(true, Ordering::Relaxed);
2243 let deadline = Instant::now() + Duration::from_secs(10);
2244 while !replica.is_finished() {
2245 assert!(
2246 Instant::now() < deadline,
2247 "receiver did not exit after shutdown"
2248 );
2249 std::thread::sleep(Duration::from_millis(20));
2250 }
2251 let result = replica.join().expect("replica thread panicked");
2252 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
2253
2254 let deadline = Instant::now() + Duration::from_secs(10);
2259 loop {
2260 let names = live_thread_names();
2261 if !names
2262 .iter()
2263 .any(|n| n == "journal-seq" || n == "matching" || n == "drain")
2264 {
2265 break;
2266 }
2267 assert!(
2268 Instant::now() < deadline,
2269 "pipeline threads still alive after shutdown: {names:?}"
2270 );
2271 std::thread::sleep(Duration::from_millis(50));
2272 }
2273 }
2274 }
2275
2276 #[cfg(not(feature = "no-persist"))]
2282 mod reconnect_resume {
2283 use super::super::super::auth::authenticate_replica;
2284 use super::scripted::*;
2285 use super::*;
2286 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
2287 use melin_transport_core::replication::catchup::lineage_origin;
2288 use melin_transport_core::replication::protocol::encode_stream_start;
2289 use std::sync::Arc;
2290 use std::sync::atomic::AtomicBool;
2291
2292 fn primary_journal(
2295 dir: &std::path::Path,
2296 ) -> (std::path::PathBuf, melin_journal::FileHeaderInfo) {
2297 let path = dir.join("primary.journal");
2298 let mut w = BufferedWriter::<EvtAdd>::create(&path).expect("create");
2299 w.append(&JournalEvent::App(EvtAdd(1))).expect("append");
2300 drop(w);
2301 let lineage = lineage_origin(&path).expect("lineage");
2302 (path, lineage)
2303 }
2304
2305 fn spawn_replica(
2308 dir: &std::path::Path,
2309 addr: std::net::SocketAddr,
2310 group_commit_delay: Duration,
2311 shutdown: &Arc<AtomicBool>,
2312 ) -> std::thread::JoinHandle<Result<bool, String>> {
2313 let journal = dir.join("replica.journal");
2314 let shutdown = Arc::clone(shutdown);
2315 std::thread::spawn(move || -> Result<bool, String> {
2316 run_receiver::<App>(
2317 addr,
2318 &store(&journal),
2319 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
2320 &shutdown,
2321 &crate::replication::ReplicaControlPlane::new(),
2322 3_600_000,
2323 crate::layout::PipelineCores::unpinned(),
2324 melin_journal::StagingMode::ZeroFill,
2325 group_commit_delay,
2326 64,
2327 Arc::new(melin_transport_core::fence::FenceState::new(0)),
2328 &(),
2329 )
2330 .map(|state| state.is_none())
2332 .map_err(|e| e.to_string())
2333 })
2334 }
2335
2336 fn next_handshake(
2339 listener: &std::net::TcpListener,
2340 ) -> (TcpStream, TcpStream, crate::replication::Handshake) {
2341 let mut s = accept_within(listener, 30);
2342 let mut sr = s.try_clone().expect("clone");
2343 authenticate_replica(&mut sr, &replica_auth()).expect("auth");
2344 match read_replica_msg(&mut s) {
2345 ReplicaMessage::Handshake(h) => (s, sr, h),
2346 other => panic!("expected Handshake, got {other:?}"),
2347 }
2348 }
2349
2350 fn stream_start(
2352 stream: &mut TcpStream,
2353 start: u64,
2354 lineage: melin_journal::FileHeaderInfo,
2355 ) {
2356 let mut buf = Vec::new();
2357 encode_stream_start(
2358 start,
2359 lineage.starting_sequence,
2360 lineage.anchor_hash,
2361 lineage.genesis_entries,
2362 0,
2363 &mut buf,
2364 );
2365 stream.write_all(&buf).expect("StreamStart");
2366 }
2367
2368 fn entry(sequence: u64) -> InputSlot<EvtAdd> {
2370 InputSlot::<EvtAdd> {
2371 connection_id: 0,
2372 request_id: RequestId(0),
2373 key_hash: 0,
2374 sequence,
2375 timestamp_ns: sequence,
2376 event: JournalEvent::App(EvtAdd(sequence)),
2377 publish_ts: Default::default(),
2378 recv_ts: Default::default(),
2379 }
2380 }
2381
2382 fn send_entries(stream: &mut TcpStream, sequences: &[u64]) {
2383 let slots: Vec<_> = sequences.iter().map(|&s| entry(s)).collect();
2384 let mut buf = Vec::new();
2385 melin_transport_core::replication_wire::encode_input_batch(&slots, &mut buf)
2386 .expect("encode InputBatch");
2387 stream.write_all(&buf).expect("InputBatch");
2388 }
2389
2390 fn stop(replica: std::thread::JoinHandle<Result<bool, String>>, shutdown: &AtomicBool) {
2391 shutdown.store(true, Ordering::Relaxed);
2392 let deadline = Instant::now() + Duration::from_secs(10);
2393 while !replica.is_finished() {
2394 assert!(
2395 Instant::now() < deadline,
2396 "receiver did not exit after shutdown"
2397 );
2398 std::thread::sleep(Duration::from_millis(20));
2399 }
2400 let result = replica.join().expect("replica thread panicked");
2401 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
2402 }
2403
2404 #[test]
2410 fn a_reconnect_claims_entries_received_before_the_drop() {
2411 let dir = tempfile::tempdir().expect("tempdir");
2412 let (_, lineage) = primary_journal(dir.path());
2413 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2414 listener.set_nonblocking(true).expect("nonblocking");
2415 let shutdown = Arc::new(AtomicBool::new(false));
2416 let delay = Duration::from_millis(2_500);
2420 let replica = spawn_replica(
2421 dir.path(),
2422 listener.local_addr().expect("addr"),
2423 delay,
2424 &shutdown,
2425 );
2426
2427 let (mut s1, _s1r, h1) = next_handshake(&listener);
2428 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2429 stream_start(&mut s1, 0, lineage);
2430 send_entries(&mut s1, &[1]);
2433 wait_for_ack(&mut s1, 1);
2434 send_entries(&mut s1, &[2, 3, 5]);
2437
2438 let (mut s2, _s2r, h2) = next_handshake(&listener);
2439 assert_eq!(
2440 h2.last_sequence, 3,
2441 "the reconnect must claim the entries received before the drop"
2442 );
2443 stream_start(&mut s2, h2.last_sequence, lineage);
2446 stop(replica, &shutdown);
2447 }
2448
2449 #[test]
2453 fn a_failed_resync_transfer_closes_the_socket_before_backing_off() {
2454 use melin_transport_core::replication::protocol::encode_need_snapshot;
2455
2456 let dir = tempfile::tempdir().expect("tempdir");
2457 let (_, lineage) = primary_journal(dir.path());
2458 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2459 listener.set_nonblocking(true).expect("nonblocking");
2460 let shutdown = Arc::new(AtomicBool::new(false));
2461 let replica = spawn_replica(
2462 dir.path(),
2463 listener.local_addr().expect("addr"),
2464 Duration::ZERO,
2465 &shutdown,
2466 );
2467
2468 let (mut s1, _s1r, _) = next_handshake(&listener);
2469 let mut buf = Vec::new();
2472 encode_need_snapshot(&mut buf);
2473 encode_need_snapshot(&mut buf);
2474 s1.write_all(&buf).expect("NeedSnapshot x2");
2475
2476 s1.set_read_timeout(Some(Duration::from_millis(700)))
2479 .expect("read timeout");
2480 let mut byte = [0u8; 1];
2481 match s1.read(&mut byte) {
2482 Ok(0) => {}
2483 Err(e) if e.kind() == std::io::ErrorKind::ConnectionReset => {}
2484 other => panic!("socket still open during the backoff: {other:?}"),
2485 }
2486
2487 let (mut s2, _s2r, h2) = next_handshake(&listener);
2489 assert_eq!(h2.last_sequence, 0, "retry handshakes as fresh");
2490 stream_start(&mut s2, 0, lineage);
2491 stop(replica, &shutdown);
2492 }
2493
2494 #[cfg(feature = "hash-chain")]
2508 #[test]
2509 fn a_journal_that_dies_while_disconnected_is_torn_down_before_dialing() {
2510 use melin_transport_core::replication::protocol::encode_chain_check;
2511
2512 let dir = tempfile::tempdir().expect("tempdir");
2513 let (_, lineage) = primary_journal(dir.path());
2514 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2515 listener.set_nonblocking(true).expect("nonblocking");
2516 let shutdown = Arc::new(AtomicBool::new(false));
2517 let delay = Duration::from_millis(2_500);
2520 let replica = spawn_replica(
2521 dir.path(),
2522 listener.local_addr().expect("addr"),
2523 delay,
2524 &shutdown,
2525 );
2526
2527 let (mut s1, _s1r, h1) = next_handshake(&listener);
2528 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2529 stream_start(&mut s1, 0, lineage);
2530 send_entries(&mut s1, &[1]);
2531 wait_for_ack(&mut s1, 1);
2532 send_entries(&mut s1, &[2]);
2536 let mut buf = Vec::new();
2537 encode_chain_check(2, &[0xEE; 32], &mut buf);
2538 s1.write_all(&buf).expect("ChainCheck");
2539 send_entries(&mut s1, &[3]);
2540 send_entries(&mut s1, &[5]);
2541
2542 let (mut s2, _s2r, h2) = next_handshake(&listener);
2543 assert_eq!(
2544 h2.last_sequence, 1,
2545 "the first dial after the journal died claims what is on disk"
2546 );
2547 stream_start(&mut s2, h2.last_sequence, lineage);
2552 send_entries(&mut s2, &[2]);
2553 wait_for_ack(&mut s2, 2);
2554 stop(replica, &shutdown);
2555 }
2556
2557 #[test]
2562 fn a_reconnect_after_a_quiet_session_claims_the_recovered_journal() {
2563 let dir = tempfile::tempdir().expect("tempdir");
2564 let (primary, lineage) = primary_journal(dir.path());
2565 std::fs::copy(&primary, dir.path().join("replica.journal")).expect("copy");
2567 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2568 listener.set_nonblocking(true).expect("nonblocking");
2569 let shutdown = Arc::new(AtomicBool::new(false));
2570 let replica = spawn_replica(
2571 dir.path(),
2572 listener.local_addr().expect("addr"),
2573 Duration::ZERO,
2574 &shutdown,
2575 );
2576
2577 let (mut s1, s1r, h1) = next_handshake(&listener);
2578 assert_eq!(h1.last_sequence, 1, "recovered replica handshake");
2579 stream_start(&mut s1, 1, lineage);
2580 drop(s1);
2582 drop(s1r);
2583
2584 let (mut s2, _s2r, h2) = next_handshake(&listener);
2585 assert_eq!(
2586 (h2.last_sequence, h2.chain_hash),
2587 (h1.last_sequence, h1.chain_hash),
2588 "the reconnect must claim the journal the pipeline was built over"
2589 );
2590 stream_start(&mut s2, h2.last_sequence, lineage);
2592 stop(replica, &shutdown);
2593 }
2594
2595 #[test]
2602 fn a_damaged_entry_is_refused_and_fetched_again() {
2603 let dir = tempfile::tempdir().expect("tempdir");
2604 let (_, lineage) = primary_journal(dir.path());
2605 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2606 listener.set_nonblocking(true).expect("nonblocking");
2607 let shutdown = Arc::new(AtomicBool::new(false));
2608 let replica = spawn_replica(
2609 dir.path(),
2610 listener.local_addr().expect("addr"),
2611 Duration::ZERO,
2612 &shutdown,
2613 );
2614
2615 let (mut s1, _s1r, h1) = next_handshake(&listener);
2616 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2617 stream_start(&mut s1, 0, lineage);
2618 send_entries(&mut s1, &[1]);
2619 wait_for_ack(&mut s1, 1);
2620
2621 let mut buf = Vec::new();
2624 melin_transport_core::replication_wire::encode_input_batch(&[entry(2)], &mut buf)
2625 .expect("encode InputBatch");
2626 let payload_at = buf.len() - melin_journal::codec::CRC_SIZE - 8;
2627 buf[payload_at] ^= 0x01;
2628 s1.write_all(&buf).expect("damaged InputBatch");
2629
2630 loop {
2633 let mut len_buf = [0u8; 4];
2634 if std::io::Read::read_exact(&mut s1, &mut len_buf).is_err() {
2635 break;
2636 }
2637 let mut payload = vec![0u8; u32::from_le_bytes(len_buf) as usize];
2638 if std::io::Read::read_exact(&mut s1, &mut payload).is_err() {
2639 break;
2640 }
2641 if let Ok(ReplicaMessage::Ack(a)) = decode_replica_message(&payload) {
2642 assert!(
2643 a.in_memory_sequence < 2 && a.acked_sequence < 2,
2644 "the damaged entry was acknowledged: {a:?}"
2645 );
2646 }
2647 }
2648
2649 let (mut s2, _s2r, h2) = next_handshake(&listener);
2650 assert_eq!(
2651 h2.last_sequence, 1,
2652 "the reconnect must claim only the entries that passed the check"
2653 );
2654 stream_start(&mut s2, 1, lineage);
2655 send_entries(&mut s2, &[2]);
2656 wait_for_ack(&mut s2, 2);
2657 stop(replica, &shutdown);
2658
2659 let mut reader =
2660 melin_journal::JournalReader::<EvtAdd>::open(&dir.path().join("replica.journal"))
2661 .expect("open replica journal");
2662 let mut journaled = Vec::new();
2663 while let Some(e) = reader.next_entry().expect("read entry") {
2664 if let JournalEvent::App(EvtAdd(v)) = e.event {
2665 journaled.push(v);
2666 }
2667 }
2668 assert_eq!(journaled, vec![1, 2], "only the intact entry is journaled");
2669 }
2670
2671 fn with_genesis(
2674 lineage: melin_journal::FileHeaderInfo,
2675 genesis_entries: Option<u64>,
2676 ) -> melin_journal::FileHeaderInfo {
2677 melin_journal::FileHeaderInfo {
2678 genesis_entries,
2679 ..lineage
2680 }
2681 }
2682
2683 fn replica_genesis(dir: &std::path::Path) -> Option<u64> {
2684 melin_journal::segment::read_header_info(&dir.join("replica.journal"))
2685 .expect("replica journal header")
2686 .genesis_entries
2687 }
2688
2689 #[test]
2694 fn a_replica_learns_the_genesis_length_and_keeps_it_across_restarts() {
2695 let dir = tempfile::tempdir().expect("tempdir");
2696 let (_, base) = primary_journal(dir.path());
2697 let lineage = with_genesis(base, Some(3));
2698 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2699 listener.set_nonblocking(true).expect("nonblocking");
2700 let addr = listener.local_addr().expect("addr");
2701
2702 let shutdown = Arc::new(AtomicBool::new(false));
2703 let replica = spawn_replica(dir.path(), addr, Duration::ZERO, &shutdown);
2704 let (mut s1, _s1r, h1) = next_handshake(&listener);
2705 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2706 stream_start(&mut s1, 0, lineage);
2707 send_entries(&mut s1, &[1, 2]);
2708 wait_for_ack(&mut s1, 2);
2709 assert_eq!(replica_genesis(dir.path()), Some(3), "learned at creation");
2710 stop(replica, &shutdown);
2711
2712 let shutdown = Arc::new(AtomicBool::new(false));
2713 let replica = spawn_replica(dir.path(), addr, Duration::ZERO, &shutdown);
2714 let (mut s2, _s2r, h2) = next_handshake(&listener);
2715 assert_eq!(h2.last_sequence, 2, "restarted replica handshake");
2716 stream_start(&mut s2, 2, lineage);
2717 send_entries(&mut s2, &[3]);
2718 wait_for_ack(&mut s2, 3);
2719 stop(replica, &shutdown);
2720 assert_eq!(
2721 replica_genesis(dir.path()),
2722 Some(3),
2723 "kept across a restart"
2724 );
2725 }
2726
2727 #[test]
2734 fn an_empty_old_format_replica_journal_learns_the_genesis_length() {
2735 let dir = tempfile::tempdir().expect("tempdir");
2736 let (_, base) = primary_journal(dir.path());
2737 assert_eq!(base.starting_sequence, 1);
2738 drop(
2739 melin_journal::BufferedWriter::<EvtAdd>::create_continuing(
2740 &dir.path().join("replica.journal"),
2741 1,
2742 base.anchor_hash,
2743 None,
2744 )
2745 .expect("old-format empty replica journal"),
2746 );
2747 assert_eq!(replica_genesis(dir.path()), None);
2748 let lineage = with_genesis(base, Some(3));
2749 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2750 listener.set_nonblocking(true).expect("nonblocking");
2751 let shutdown = Arc::new(AtomicBool::new(false));
2752 let replica = spawn_replica(
2753 dir.path(),
2754 listener.local_addr().expect("addr"),
2755 Duration::ZERO,
2756 &shutdown,
2757 );
2758
2759 let (mut s1, _s1r, h1) = next_handshake(&listener);
2760 assert_eq!(h1.last_sequence, 0, "empty replica handshake");
2761 stream_start(&mut s1, 0, lineage);
2762 send_entries(&mut s1, &[1, 2]);
2763 wait_for_ack(&mut s1, 2);
2764 stop(replica, &shutdown);
2765 assert_eq!(replica_genesis(dir.path()), Some(3));
2766 }
2767
2768 #[test]
2772 fn a_primary_advertising_another_genesis_length_is_refused() {
2773 let dir = tempfile::tempdir().expect("tempdir");
2774 let (primary, lineage) = primary_journal(dir.path());
2775 assert_eq!(lineage.genesis_entries, Some(0));
2776 std::fs::copy(&primary, dir.path().join("replica.journal")).expect("copy");
2779 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2780 listener.set_nonblocking(true).expect("nonblocking");
2781 let shutdown = Arc::new(AtomicBool::new(false));
2782 let replica = spawn_replica(
2783 dir.path(),
2784 listener.local_addr().expect("addr"),
2785 Duration::ZERO,
2786 &shutdown,
2787 );
2788
2789 let (mut s1, _s1r, h1) = next_handshake(&listener);
2790 assert_eq!(h1.last_sequence, 1);
2791 stream_start(&mut s1, 1, with_genesis(lineage, Some(4)));
2792 let result = replica.join().expect("replica thread panicked");
2793 let err = result.expect_err("the replica must refuse the lineage");
2794 assert!(err.contains("refusing to follow the primary"), "{err}");
2795 assert_eq!(replica_genesis(dir.path()), Some(0), "never overwritten");
2796 }
2797
2798 #[test]
2805 fn a_replica_promoted_mid_genesis_is_refused() {
2806 let dir = tempfile::tempdir().expect("tempdir");
2807 let (_, base) = primary_journal(dir.path());
2808 let lineage = with_genesis(base, Some(3));
2809 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2810 listener.set_nonblocking(true).expect("nonblocking");
2811 let addr = listener.local_addr().expect("addr");
2812 let shutdown = Arc::new(AtomicBool::new(false));
2813 let control = crate::replication::ReplicaControlPlane::new();
2814 let replica = {
2815 let journal = dir.path().join("replica.journal");
2816 let shutdown = Arc::clone(&shutdown);
2817 let control = control.clone();
2818 std::thread::spawn(move || -> Result<Option<String>, String> {
2819 run_receiver::<App>(
2820 addr,
2821 &store(&journal),
2822 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
2823 &shutdown,
2824 &control,
2825 3_600_000,
2826 crate::layout::PipelineCores::unpinned(),
2827 melin_journal::StagingMode::ZeroFill,
2828 Duration::ZERO,
2829 64,
2830 Arc::new(melin_transport_core::fence::FenceState::new(0)),
2831 &(),
2832 )
2833 .map(|promoted| {
2836 promoted.map(
2837 |(_, writer)| match crate::server::check_promotable(&writer) {
2838 Ok(()) => "promotable".to_string(),
2839 Err(e) => e.to_string(),
2840 },
2841 )
2842 })
2843 .map_err(|e| e.to_string())
2844 })
2845 };
2846
2847 let (mut s1, s1r, h1) = next_handshake(&listener);
2848 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2849 stream_start(&mut s1, 0, lineage);
2850 send_entries(&mut s1, &[1, 2]);
2851 wait_for_ack(&mut s1, 2);
2852 drop(s1);
2854 drop(s1r);
2855 assert!(
2856 control
2857 .promote
2858 .request(crate::promotion::PromotionRequest::MANUAL),
2859 "promotion filed"
2860 );
2861
2862 let verdict = replica
2863 .join()
2864 .expect("replica thread panicked")
2865 .expect("the receiver hands its state over")
2866 .expect("promoted, not shut down");
2867 assert!(verdict.contains("refusing promotion"), "{verdict}");
2868 assert!(verdict.contains("holds 2 entries"), "{verdict}");
2869 }
2870 }
2871
2872 #[cfg(all(feature = "hash-chain", not(feature = "no-persist")))]
2879 mod rotation_adoption {
2880 use super::super::super::auth::authenticate_replica;
2881 use super::scripted::*;
2882 use super::*;
2883 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
2884 use melin_transport_core::replication::catchup::lineage_origin;
2885 use melin_transport_core::replication::protocol::{encode_rotate, encode_stream_start};
2886 use std::sync::Arc;
2887 use std::sync::atomic::AtomicBool;
2888
2889 fn payloads(path: &std::path::Path) -> Vec<u64> {
2891 let mut reader = melin_journal::JournalReader::<EvtAdd>::open(path).expect("open");
2892 let mut out = Vec::new();
2893 while let Some(entry) = reader.next_entry().expect("read entry") {
2894 if let JournalEvent::App(EvtAdd(v)) = entry.event {
2895 out.push(v);
2896 }
2897 }
2898 out
2899 }
2900
2901 fn boundary_adoption_rotates_and_streams_on(mode: melin_journal::StagingMode) {
2912 let _prealloc_guard =
2917 melin_journal::test_utils::PreallocOverrideGuard::new(1024 * 1024);
2918 let dir = tempfile::tempdir().expect("tempdir");
2919
2920 let primary_journal = dir.path().join("primary.journal");
2926 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
2927 for v in 1..=2u64 {
2928 w.batch_append_with_ts(&JournalEvent::App(EvtAdd(v)), v, 0)
2929 .expect("append");
2930 }
2931 w.flush_batch_sync().expect("flush");
2932 let chain_at_2 = w.chain_hash().expect("chain");
2933 drop(w);
2934 let origin = lineage_origin(&primary_journal).expect("lineage");
2935 let (lineage_start, lineage_anchor, lineage_genesis) = (
2936 origin.starting_sequence,
2937 origin.anchor_hash,
2938 origin.genesis_entries,
2939 );
2940
2941 let authorized_keys = replica_auth();
2942 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2943 listener.set_nonblocking(true).expect("nonblocking");
2944 let addr = listener.local_addr().expect("addr");
2945
2946 let replica_journal = dir.path().join("replica.journal");
2947 let shutdown = Arc::new(AtomicBool::new(false));
2948 let control = crate::replication::ReplicaControlPlane::new();
2949 let replica = {
2950 let journal = replica_journal.clone();
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 &store(&journal),
2957 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
2958 &shutdown,
2959 &control,
2960 3_600_000,
2961 crate::layout::PipelineCores::unpinned(),
2962 mode,
2963 Duration::ZERO,
2964 64,
2965 Arc::new(melin_transport_core::fence::FenceState::new(0)),
2966 &(),
2967 )
2968 .map(|state| state.is_none())
2970 .map_err(|e| e.to_string())
2971 })
2972 };
2973
2974 let mut s1 = accept_within(&listener, 30);
2975 let mut s1r = s1.try_clone().expect("clone");
2976 authenticate_replica(&mut s1r, &authorized_keys).expect("auth");
2977 match read_replica_msg(&mut s1) {
2978 ReplicaMessage::Handshake(h) => {
2979 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
2980 }
2981 other => panic!("expected Handshake, got {other:?}"),
2982 }
2983 let mut buf = Vec::new();
2984 encode_stream_start(
2985 0,
2986 lineage_start,
2987 lineage_anchor,
2988 lineage_genesis,
2989 0,
2990 &mut buf,
2991 );
2992 s1.write_all(&buf).expect("StreamStart");
2993
2994 let send_events = |s: &mut TcpStream, range: std::ops::RangeInclusive<u64>| {
2995 let slots: Vec<InputSlot<EvtAdd>> = range
2996 .map(|v| InputSlot {
2997 connection_id: 0,
2998 request_id: RequestId(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}