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 ) -> (
2341 TcpStream,
2342 TcpStream,
2343 melin_transport_core::replication::protocol::Handshake,
2344 ) {
2345 let mut s = accept_within(listener, 30);
2346 let mut sr = s.try_clone().expect("clone");
2347 authenticate_replica(&mut sr, &replica_auth()).expect("auth");
2348 match read_replica_msg(&mut s) {
2349 ReplicaMessage::Handshake(h) => (s, sr, h),
2350 other => panic!("expected Handshake, got {other:?}"),
2351 }
2352 }
2353
2354 fn stream_start(
2356 stream: &mut TcpStream,
2357 start: u64,
2358 lineage: melin_journal::FileHeaderInfo,
2359 ) {
2360 let mut buf = Vec::new();
2361 encode_stream_start(
2362 start,
2363 lineage.starting_sequence,
2364 lineage.anchor_hash,
2365 lineage.genesis_entries,
2366 0,
2367 &mut buf,
2368 );
2369 stream.write_all(&buf).expect("StreamStart");
2370 }
2371
2372 fn entry(sequence: u64) -> InputSlot<EvtAdd> {
2374 InputSlot::<EvtAdd> {
2375 connection_id: 0,
2376 request_id: RequestId(0),
2377 key_hash: 0,
2378 sequence,
2379 timestamp_ns: sequence,
2380 event: JournalEvent::App(EvtAdd(sequence)),
2381 publish_ts: Default::default(),
2382 recv_ts: Default::default(),
2383 }
2384 }
2385
2386 fn send_entries(stream: &mut TcpStream, sequences: &[u64]) {
2387 let slots: Vec<_> = sequences.iter().map(|&s| entry(s)).collect();
2388 let mut buf = Vec::new();
2389 melin_transport_core::replication_wire::encode_input_batch(&slots, &mut buf)
2390 .expect("encode InputBatch");
2391 stream.write_all(&buf).expect("InputBatch");
2392 }
2393
2394 fn stop(replica: std::thread::JoinHandle<Result<bool, String>>, shutdown: &AtomicBool) {
2395 shutdown.store(true, Ordering::Relaxed);
2396 let deadline = Instant::now() + Duration::from_secs(10);
2397 while !replica.is_finished() {
2398 assert!(
2399 Instant::now() < deadline,
2400 "receiver did not exit after shutdown"
2401 );
2402 std::thread::sleep(Duration::from_millis(20));
2403 }
2404 let result = replica.join().expect("replica thread panicked");
2405 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
2406 }
2407
2408 #[test]
2414 fn a_reconnect_claims_entries_received_before_the_drop() {
2415 let dir = tempfile::tempdir().expect("tempdir");
2416 let (_, lineage) = primary_journal(dir.path());
2417 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2418 listener.set_nonblocking(true).expect("nonblocking");
2419 let shutdown = Arc::new(AtomicBool::new(false));
2420 let delay = Duration::from_millis(2_500);
2424 let replica = spawn_replica(
2425 dir.path(),
2426 listener.local_addr().expect("addr"),
2427 delay,
2428 &shutdown,
2429 );
2430
2431 let (mut s1, _s1r, h1) = next_handshake(&listener);
2432 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2433 stream_start(&mut s1, 0, lineage);
2434 send_entries(&mut s1, &[1]);
2437 wait_for_ack(&mut s1, 1);
2438 send_entries(&mut s1, &[2, 3, 5]);
2441
2442 let (mut s2, _s2r, h2) = next_handshake(&listener);
2443 assert_eq!(
2444 h2.last_sequence, 3,
2445 "the reconnect must claim the entries received before the drop"
2446 );
2447 stream_start(&mut s2, h2.last_sequence, lineage);
2450 stop(replica, &shutdown);
2451 }
2452
2453 #[test]
2457 fn a_failed_resync_transfer_closes_the_socket_before_backing_off() {
2458 use melin_transport_core::replication::protocol::encode_need_snapshot;
2459
2460 let dir = tempfile::tempdir().expect("tempdir");
2461 let (_, lineage) = primary_journal(dir.path());
2462 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2463 listener.set_nonblocking(true).expect("nonblocking");
2464 let shutdown = Arc::new(AtomicBool::new(false));
2465 let replica = spawn_replica(
2466 dir.path(),
2467 listener.local_addr().expect("addr"),
2468 Duration::ZERO,
2469 &shutdown,
2470 );
2471
2472 let (mut s1, _s1r, _) = next_handshake(&listener);
2473 let mut buf = Vec::new();
2476 encode_need_snapshot(&mut buf);
2477 encode_need_snapshot(&mut buf);
2478 s1.write_all(&buf).expect("NeedSnapshot x2");
2479
2480 s1.set_read_timeout(Some(Duration::from_millis(700)))
2483 .expect("read timeout");
2484 let mut byte = [0u8; 1];
2485 match s1.read(&mut byte) {
2486 Ok(0) => {}
2487 Err(e) if e.kind() == std::io::ErrorKind::ConnectionReset => {}
2488 other => panic!("socket still open during the backoff: {other:?}"),
2489 }
2490
2491 let (mut s2, _s2r, h2) = next_handshake(&listener);
2493 assert_eq!(h2.last_sequence, 0, "retry handshakes as fresh");
2494 stream_start(&mut s2, 0, lineage);
2495 stop(replica, &shutdown);
2496 }
2497
2498 #[cfg(feature = "hash-chain")]
2512 #[test]
2513 fn a_journal_that_dies_while_disconnected_is_torn_down_before_dialing() {
2514 use melin_transport_core::replication::protocol::encode_chain_check;
2515
2516 let dir = tempfile::tempdir().expect("tempdir");
2517 let (_, lineage) = primary_journal(dir.path());
2518 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2519 listener.set_nonblocking(true).expect("nonblocking");
2520 let shutdown = Arc::new(AtomicBool::new(false));
2521 let delay = Duration::from_millis(2_500);
2524 let replica = spawn_replica(
2525 dir.path(),
2526 listener.local_addr().expect("addr"),
2527 delay,
2528 &shutdown,
2529 );
2530
2531 let (mut s1, _s1r, h1) = next_handshake(&listener);
2532 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2533 stream_start(&mut s1, 0, lineage);
2534 send_entries(&mut s1, &[1]);
2535 wait_for_ack(&mut s1, 1);
2536 send_entries(&mut s1, &[2]);
2540 let mut buf = Vec::new();
2541 encode_chain_check(2, &[0xEE; 32], &mut buf);
2542 s1.write_all(&buf).expect("ChainCheck");
2543 send_entries(&mut s1, &[3]);
2544 send_entries(&mut s1, &[5]);
2545
2546 let (mut s2, _s2r, h2) = next_handshake(&listener);
2547 assert_eq!(
2548 h2.last_sequence, 1,
2549 "the first dial after the journal died claims what is on disk"
2550 );
2551 stream_start(&mut s2, h2.last_sequence, lineage);
2556 send_entries(&mut s2, &[2]);
2557 wait_for_ack(&mut s2, 2);
2558 stop(replica, &shutdown);
2559 }
2560
2561 #[test]
2566 fn a_reconnect_after_a_quiet_session_claims_the_recovered_journal() {
2567 let dir = tempfile::tempdir().expect("tempdir");
2568 let (primary, lineage) = primary_journal(dir.path());
2569 std::fs::copy(&primary, dir.path().join("replica.journal")).expect("copy");
2571 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2572 listener.set_nonblocking(true).expect("nonblocking");
2573 let shutdown = Arc::new(AtomicBool::new(false));
2574 let replica = spawn_replica(
2575 dir.path(),
2576 listener.local_addr().expect("addr"),
2577 Duration::ZERO,
2578 &shutdown,
2579 );
2580
2581 let (mut s1, s1r, h1) = next_handshake(&listener);
2582 assert_eq!(h1.last_sequence, 1, "recovered replica handshake");
2583 stream_start(&mut s1, 1, lineage);
2584 drop(s1);
2586 drop(s1r);
2587
2588 let (mut s2, _s2r, h2) = next_handshake(&listener);
2589 assert_eq!(
2590 (h2.last_sequence, h2.chain_hash),
2591 (h1.last_sequence, h1.chain_hash),
2592 "the reconnect must claim the journal the pipeline was built over"
2593 );
2594 stream_start(&mut s2, h2.last_sequence, lineage);
2596 stop(replica, &shutdown);
2597 }
2598
2599 #[test]
2606 fn a_damaged_entry_is_refused_and_fetched_again() {
2607 let dir = tempfile::tempdir().expect("tempdir");
2608 let (_, lineage) = primary_journal(dir.path());
2609 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2610 listener.set_nonblocking(true).expect("nonblocking");
2611 let shutdown = Arc::new(AtomicBool::new(false));
2612 let replica = spawn_replica(
2613 dir.path(),
2614 listener.local_addr().expect("addr"),
2615 Duration::ZERO,
2616 &shutdown,
2617 );
2618
2619 let (mut s1, _s1r, h1) = next_handshake(&listener);
2620 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2621 stream_start(&mut s1, 0, lineage);
2622 send_entries(&mut s1, &[1]);
2623 wait_for_ack(&mut s1, 1);
2624
2625 let mut buf = Vec::new();
2628 melin_transport_core::replication_wire::encode_input_batch(&[entry(2)], &mut buf)
2629 .expect("encode InputBatch");
2630 let payload_at = buf.len() - melin_journal::codec::CRC_SIZE - 8;
2631 buf[payload_at] ^= 0x01;
2632 s1.write_all(&buf).expect("damaged InputBatch");
2633
2634 loop {
2637 let mut len_buf = [0u8; 4];
2638 if std::io::Read::read_exact(&mut s1, &mut len_buf).is_err() {
2639 break;
2640 }
2641 let mut payload = vec![0u8; u32::from_le_bytes(len_buf) as usize];
2642 if std::io::Read::read_exact(&mut s1, &mut payload).is_err() {
2643 break;
2644 }
2645 if let Ok(ReplicaMessage::Ack(a)) = decode_replica_message(&payload) {
2646 assert!(
2647 a.in_memory_sequence < 2 && a.acked_sequence < 2,
2648 "the damaged entry was acknowledged: {a:?}"
2649 );
2650 }
2651 }
2652
2653 let (mut s2, _s2r, h2) = next_handshake(&listener);
2654 assert_eq!(
2655 h2.last_sequence, 1,
2656 "the reconnect must claim only the entries that passed the check"
2657 );
2658 stream_start(&mut s2, 1, lineage);
2659 send_entries(&mut s2, &[2]);
2660 wait_for_ack(&mut s2, 2);
2661 stop(replica, &shutdown);
2662
2663 let mut reader =
2664 melin_journal::JournalReader::<EvtAdd>::open(&dir.path().join("replica.journal"))
2665 .expect("open replica journal");
2666 let mut journaled = Vec::new();
2667 while let Some(e) = reader.next_entry().expect("read entry") {
2668 if let JournalEvent::App(EvtAdd(v)) = e.event {
2669 journaled.push(v);
2670 }
2671 }
2672 assert_eq!(journaled, vec![1, 2], "only the intact entry is journaled");
2673 }
2674
2675 fn with_genesis(
2678 lineage: melin_journal::FileHeaderInfo,
2679 genesis_entries: Option<u64>,
2680 ) -> melin_journal::FileHeaderInfo {
2681 melin_journal::FileHeaderInfo {
2682 genesis_entries,
2683 ..lineage
2684 }
2685 }
2686
2687 fn replica_genesis(dir: &std::path::Path) -> Option<u64> {
2688 melin_journal::segment::read_header_info(&dir.join("replica.journal"))
2689 .expect("replica journal header")
2690 .genesis_entries
2691 }
2692
2693 #[test]
2698 fn a_replica_learns_the_genesis_length_and_keeps_it_across_restarts() {
2699 let dir = tempfile::tempdir().expect("tempdir");
2700 let (_, base) = primary_journal(dir.path());
2701 let lineage = with_genesis(base, Some(3));
2702 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2703 listener.set_nonblocking(true).expect("nonblocking");
2704 let addr = listener.local_addr().expect("addr");
2705
2706 let shutdown = Arc::new(AtomicBool::new(false));
2707 let replica = spawn_replica(dir.path(), addr, Duration::ZERO, &shutdown);
2708 let (mut s1, _s1r, h1) = next_handshake(&listener);
2709 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2710 stream_start(&mut s1, 0, lineage);
2711 send_entries(&mut s1, &[1, 2]);
2712 wait_for_ack(&mut s1, 2);
2713 assert_eq!(replica_genesis(dir.path()), Some(3), "learned at creation");
2714 stop(replica, &shutdown);
2715
2716 let shutdown = Arc::new(AtomicBool::new(false));
2717 let replica = spawn_replica(dir.path(), addr, Duration::ZERO, &shutdown);
2718 let (mut s2, _s2r, h2) = next_handshake(&listener);
2719 assert_eq!(h2.last_sequence, 2, "restarted replica handshake");
2720 stream_start(&mut s2, 2, lineage);
2721 send_entries(&mut s2, &[3]);
2722 wait_for_ack(&mut s2, 3);
2723 stop(replica, &shutdown);
2724 assert_eq!(
2725 replica_genesis(dir.path()),
2726 Some(3),
2727 "kept across a restart"
2728 );
2729 }
2730
2731 #[test]
2738 fn an_empty_old_format_replica_journal_learns_the_genesis_length() {
2739 let dir = tempfile::tempdir().expect("tempdir");
2740 let (_, base) = primary_journal(dir.path());
2741 assert_eq!(base.starting_sequence, 1);
2742 drop(
2743 melin_journal::BufferedWriter::<EvtAdd>::create_continuing(
2744 &dir.path().join("replica.journal"),
2745 1,
2746 base.anchor_hash,
2747 None,
2748 )
2749 .expect("old-format empty replica journal"),
2750 );
2751 assert_eq!(replica_genesis(dir.path()), None);
2752 let lineage = with_genesis(base, Some(3));
2753 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2754 listener.set_nonblocking(true).expect("nonblocking");
2755 let shutdown = Arc::new(AtomicBool::new(false));
2756 let replica = spawn_replica(
2757 dir.path(),
2758 listener.local_addr().expect("addr"),
2759 Duration::ZERO,
2760 &shutdown,
2761 );
2762
2763 let (mut s1, _s1r, h1) = next_handshake(&listener);
2764 assert_eq!(h1.last_sequence, 0, "empty replica handshake");
2765 stream_start(&mut s1, 0, lineage);
2766 send_entries(&mut s1, &[1, 2]);
2767 wait_for_ack(&mut s1, 2);
2768 stop(replica, &shutdown);
2769 assert_eq!(replica_genesis(dir.path()), Some(3));
2770 }
2771
2772 #[test]
2776 fn a_primary_advertising_another_genesis_length_is_refused() {
2777 let dir = tempfile::tempdir().expect("tempdir");
2778 let (primary, lineage) = primary_journal(dir.path());
2779 assert_eq!(lineage.genesis_entries, Some(0));
2780 std::fs::copy(&primary, dir.path().join("replica.journal")).expect("copy");
2783 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2784 listener.set_nonblocking(true).expect("nonblocking");
2785 let shutdown = Arc::new(AtomicBool::new(false));
2786 let replica = spawn_replica(
2787 dir.path(),
2788 listener.local_addr().expect("addr"),
2789 Duration::ZERO,
2790 &shutdown,
2791 );
2792
2793 let (mut s1, _s1r, h1) = next_handshake(&listener);
2794 assert_eq!(h1.last_sequence, 1);
2795 stream_start(&mut s1, 1, with_genesis(lineage, Some(4)));
2796 let result = replica.join().expect("replica thread panicked");
2797 let err = result.expect_err("the replica must refuse the lineage");
2798 assert!(err.contains("refusing to follow the primary"), "{err}");
2799 assert_eq!(replica_genesis(dir.path()), Some(0), "never overwritten");
2800 }
2801
2802 #[test]
2809 fn a_replica_promoted_mid_genesis_is_refused() {
2810 let dir = tempfile::tempdir().expect("tempdir");
2811 let (_, base) = primary_journal(dir.path());
2812 let lineage = with_genesis(base, Some(3));
2813 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2814 listener.set_nonblocking(true).expect("nonblocking");
2815 let addr = listener.local_addr().expect("addr");
2816 let shutdown = Arc::new(AtomicBool::new(false));
2817 let control = crate::replication::ReplicaControlPlane::new();
2818 let replica = {
2819 let journal = dir.path().join("replica.journal");
2820 let shutdown = Arc::clone(&shutdown);
2821 let control = control.clone();
2822 std::thread::spawn(move || -> Result<Option<String>, String> {
2823 run_receiver::<App>(
2824 addr,
2825 &store(&journal),
2826 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
2827 &shutdown,
2828 &control,
2829 3_600_000,
2830 crate::layout::PipelineCores::unpinned(),
2831 melin_journal::StagingMode::ZeroFill,
2832 Duration::ZERO,
2833 64,
2834 Arc::new(melin_transport_core::fence::FenceState::new(0)),
2835 &(),
2836 )
2837 .map(|promoted| {
2840 promoted.map(
2841 |(_, writer)| match crate::server::check_promotable(&writer) {
2842 Ok(()) => "promotable".to_string(),
2843 Err(e) => e.to_string(),
2844 },
2845 )
2846 })
2847 .map_err(|e| e.to_string())
2848 })
2849 };
2850
2851 let (mut s1, s1r, h1) = next_handshake(&listener);
2852 assert_eq!(h1.last_sequence, 0, "fresh replica handshake");
2853 stream_start(&mut s1, 0, lineage);
2854 send_entries(&mut s1, &[1, 2]);
2855 wait_for_ack(&mut s1, 2);
2856 drop(s1);
2858 drop(s1r);
2859 assert!(
2860 control
2861 .promote
2862 .request(crate::promotion::PromotionRequest::MANUAL),
2863 "promotion filed"
2864 );
2865
2866 let verdict = replica
2867 .join()
2868 .expect("replica thread panicked")
2869 .expect("the receiver hands its state over")
2870 .expect("promoted, not shut down");
2871 assert!(verdict.contains("refusing promotion"), "{verdict}");
2872 assert!(verdict.contains("holds 2 entries"), "{verdict}");
2873 }
2874 }
2875
2876 #[cfg(all(feature = "hash-chain", not(feature = "no-persist")))]
2883 mod rotation_adoption {
2884 use super::super::super::auth::authenticate_replica;
2885 use super::scripted::*;
2886 use super::*;
2887 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
2888 use melin_transport_core::replication::catchup::lineage_origin;
2889 use melin_transport_core::replication::protocol::{encode_rotate, encode_stream_start};
2890 use std::sync::Arc;
2891 use std::sync::atomic::AtomicBool;
2892
2893 fn payloads(path: &std::path::Path) -> Vec<u64> {
2895 let mut reader = melin_journal::JournalReader::<EvtAdd>::open(path).expect("open");
2896 let mut out = Vec::new();
2897 while let Some(entry) = reader.next_entry().expect("read entry") {
2898 if let JournalEvent::App(EvtAdd(v)) = entry.event {
2899 out.push(v);
2900 }
2901 }
2902 out
2903 }
2904
2905 fn boundary_adoption_rotates_and_streams_on(mode: melin_journal::StagingMode) {
2916 let _prealloc_guard =
2921 melin_journal::test_utils::PreallocOverrideGuard::new(1024 * 1024);
2922 let dir = tempfile::tempdir().expect("tempdir");
2923
2924 let primary_journal = dir.path().join("primary.journal");
2930 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
2931 for v in 1..=2u64 {
2932 w.batch_append_with_ts(&JournalEvent::App(EvtAdd(v)), v, 0)
2933 .expect("append");
2934 }
2935 w.flush_batch_sync().expect("flush");
2936 let chain_at_2 = w.chain_hash().expect("chain");
2937 drop(w);
2938 let origin = lineage_origin(&primary_journal).expect("lineage");
2939 let (lineage_start, lineage_anchor, lineage_genesis) = (
2940 origin.starting_sequence,
2941 origin.anchor_hash,
2942 origin.genesis_entries,
2943 );
2944
2945 let authorized_keys = replica_auth();
2946 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2947 listener.set_nonblocking(true).expect("nonblocking");
2948 let addr = listener.local_addr().expect("addr");
2949
2950 let replica_journal = dir.path().join("replica.journal");
2951 let shutdown = Arc::new(AtomicBool::new(false));
2952 let control = crate::replication::ReplicaControlPlane::new();
2953 let replica = {
2954 let journal = replica_journal.clone();
2955 let shutdown = Arc::clone(&shutdown);
2956 let control = control.clone();
2957 std::thread::spawn(move || -> Result<bool, String> {
2958 run_receiver::<App>(
2959 addr,
2960 &store(&journal),
2961 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
2962 &shutdown,
2963 &control,
2964 3_600_000,
2965 crate::layout::PipelineCores::unpinned(),
2966 mode,
2967 Duration::ZERO,
2968 64,
2969 Arc::new(melin_transport_core::fence::FenceState::new(0)),
2970 &(),
2971 )
2972 .map(|state| state.is_none())
2974 .map_err(|e| e.to_string())
2975 })
2976 };
2977
2978 let mut s1 = accept_within(&listener, 30);
2979 let mut s1r = s1.try_clone().expect("clone");
2980 authenticate_replica(&mut s1r, &authorized_keys).expect("auth");
2981 match read_replica_msg(&mut s1) {
2982 ReplicaMessage::Handshake(h) => {
2983 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
2984 }
2985 other => panic!("expected Handshake, got {other:?}"),
2986 }
2987 let mut buf = Vec::new();
2988 encode_stream_start(
2989 0,
2990 lineage_start,
2991 lineage_anchor,
2992 lineage_genesis,
2993 0,
2994 &mut buf,
2995 );
2996 s1.write_all(&buf).expect("StreamStart");
2997
2998 let send_events = |s: &mut TcpStream, range: std::ops::RangeInclusive<u64>| {
2999 let slots: Vec<InputSlot<EvtAdd>> = range
3000 .map(|v| InputSlot {
3001 connection_id: 0,
3002 request_id: RequestId(0),
3003 key_hash: 0,
3004 sequence: v,
3005 timestamp_ns: v,
3006 event: JournalEvent::App(EvtAdd(v)),
3007 publish_ts: Default::default(),
3009 recv_ts: Default::default(),
3010 })
3011 .collect();
3012 let mut buf = Vec::new();
3013 melin_transport_core::replication_wire::encode_input_batch(&slots, &mut buf)
3014 .expect("encode InputBatch");
3015 s.write_all(&buf).expect("InputBatch");
3016 };
3017
3018 send_events(&mut s1, 1..=2);
3019 wait_for_ack(&mut s1, 2);
3020
3021 let staging = {
3028 let mut s = replica_journal.clone().into_os_string();
3032 s.push(".next-staging");
3033 std::path::PathBuf::from(s)
3034 };
3035 let staged_len: u64 = (1 + 8) * 1024 * 1024;
3036 let deadline = Instant::now() + Duration::from_secs(10);
3037 loop {
3038 if std::fs::metadata(&staging).is_ok_and(|m| m.len() == staged_len) {
3039 break;
3040 }
3041 assert!(
3042 Instant::now() < deadline,
3043 "preparer produced no full-size staged segment in {mode:?} mode"
3044 );
3045 std::thread::sleep(Duration::from_millis(10));
3046 }
3047 std::thread::sleep(Duration::from_millis(300));
3055
3056 buf.clear();
3059 encode_rotate(2, &chain_at_2, &mut buf);
3060 s1.write_all(&buf).expect("Rotate");
3061
3062 send_events(&mut s1, 3..=4);
3066 wait_for_ack(&mut s1, 4);
3067
3068 shutdown.store(true, Ordering::Relaxed);
3069 let result = replica.join().expect("replica thread panicked");
3070 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
3071
3072 let archived = dir.path().join("replica.journal.000001");
3076 assert!(archived.exists(), "sealed segment must be archived");
3077 assert_eq!(payloads(&archived), vec![1, 2]);
3078 assert_eq!(payloads(&replica_journal), vec![3, 4]);
3079 }
3080
3081 #[test]
3082 fn boundary_adoption_zero_fill() {
3083 boundary_adoption_rotates_and_streams_on(melin_journal::StagingMode::ZeroFill);
3084 }
3085
3086 #[test]
3087 fn boundary_adoption_allocate() {
3088 boundary_adoption_rotates_and_streams_on(melin_journal::StagingMode::Allocate);
3089 }
3090 }
3091}