1use std::io::{self, Write};
10use std::net::{SocketAddr, TcpStream};
11use std::os::unix::io::RawFd;
12use std::sync::Arc;
13use std::sync::atomic::{AtomicBool, Ordering};
14
15use tracing::{info, warn};
16
17use melin_app::Application;
18use melin_journal::BufferedWriter;
19
20use super::auth::authenticate_with_primary;
21use super::receiver_transport::{
22 ControlFrameSource, ReceiverTransport, SessionExit, streaming_loop,
23};
24use super::{
25 AfterSession, ReplicaPipelineHandles, ResyncDecision, build_replica_pipeline_with_threads,
26 handle_resync_verdict, handle_session_exit, recover_replica_state, sleep_then_double_backoff,
27 take_pipeline_for_promotion, teardown_replica_pipeline,
28};
29use crate::uring_teardown::{DrainBackoff, wake_pending_ops};
30use melin_transport_core::replication::protocol::{
31 Ack, Handshake, MAX_CONTROL_FRAME, MAX_DATA_FRAME, PrimaryMessage, decode_primary_message,
32 encode_ack, encode_handshake, read_frame,
33};
34
35#[inline]
45fn arm_tcp_quickack(fd: RawFd) {
46 let on: libc::c_int = 1;
47 let rc = unsafe {
51 libc::setsockopt(
52 fd,
53 libc::IPPROTO_TCP,
54 libc::TCP_QUICKACK,
55 &on as *const libc::c_int as *const libc::c_void,
56 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
57 )
58 };
59 if rc != 0 {
60 debug_assert!(
66 std::io::Error::last_os_error().raw_os_error() != Some(libc::EFAULT),
67 "TCP_QUICKACK setsockopt got EFAULT — bad option pointer"
68 );
69 }
70}
71
72const TOKEN_RECV: u64 = 0;
77const TOKEN_SEND: u64 = 1;
78const TOKEN_PROVIDE: u64 = 2;
79
80const NUM_RECV_BUFFERS: u16 = 16;
82const RECV_BUF_SIZE: usize = MAX_DATA_FRAME;
83const RECV_BUF_GROUP_ID: u16 = 0;
84
85const IORING_CQE_F_BUFFER: u32 = 1 << 0;
87const IORING_CQE_F_MORE: u32 = 1 << 1;
88const IORING_CQE_BUFFER_SHIFT: u32 = 16;
89
90struct UringTransport {
96 ring: io_uring::IoUring,
97 tcp_fd: RawFd,
102 recv_pool: Vec<u8>,
108 pool_ptr: *mut u8,
109 multishot_active: bool,
110 connected: bool,
111 ack_buf: Vec<u8>,
112 ack_offset: usize,
113 ack_in_flight: bool,
114 pending_ack: Option<Ack>,
124 acked_since_recv: bool,
129}
130
131impl UringTransport {
132 fn new(tcp_stream: &TcpStream) -> io::Result<Self> {
133 use io_uring::{IoUring, opcode, types};
134 use std::os::unix::io::AsRawFd;
135
136 let tcp_fd = tcp_stream.as_raw_fd();
137
138 let mut ring: IoUring = IoUring::builder()
139 .setup_single_issuer()
140 .build(64)
141 .map_err(|e| io::Error::other(format!("io_uring init failed: {e}")))?;
142
143 ring.submitter()
144 .register_files(&[tcp_fd])
145 .map_err(|e| io::Error::other(format!("io_uring register_files failed: {e}")))?;
146
147 arm_tcp_quickack(tcp_fd);
148
149 {
151 let mut cpuset: libc::cpu_set_t = unsafe { std::mem::zeroed() };
152 unsafe { libc::CPU_SET(0, &mut cpuset) };
153 let _ = ring.submitter().register_iowq_aff(&cpuset);
154 }
155
156 let mut recv_pool: Vec<u8> = vec![0u8; NUM_RECV_BUFFERS as usize * RECV_BUF_SIZE];
157 let pool_ptr = recv_pool.as_mut_ptr();
158
159 {
161 let sqe = opcode::ProvideBuffers::new(
162 pool_ptr,
163 RECV_BUF_SIZE as i32,
164 NUM_RECV_BUFFERS,
165 RECV_BUF_GROUP_ID,
166 0,
167 )
168 .build()
169 .user_data(TOKEN_PROVIDE);
170 unsafe { ring.submission().push(&sqe).expect("SQ full") };
171 ring.submit_and_wait(1)?;
172 let cqe = ring
173 .completion()
174 .next()
175 .ok_or_else(|| io::Error::other("no cqe after ProvideBuffers"))?;
176 if cqe.result() < 0 {
177 return Err(io::Error::other(format!(
178 "ProvideBuffers failed: {}",
179 cqe.result()
180 )));
181 }
182 }
183
184 let sqe = opcode::RecvMulti::new(types::Fd(tcp_fd), RECV_BUF_GROUP_ID)
186 .build()
187 .user_data(TOKEN_RECV);
188 unsafe { ring.submission().push(&sqe).expect("SQ full") };
189
190 Ok(UringTransport {
191 ring,
192 tcp_fd,
193 recv_pool,
194 pool_ptr,
195 multishot_active: true,
196 connected: true,
197 ack_buf: Vec::with_capacity(64),
198 ack_offset: 0,
199 ack_in_flight: false,
200 pending_ack: None,
201 acked_since_recv: false,
202 })
203 }
204
205 fn push_ack_send(&mut self, ack: &Ack) {
211 use io_uring::{opcode, types};
212
213 debug_assert!(!self.ack_in_flight);
214 self.ack_buf.clear();
215 encode_ack(ack, &mut self.ack_buf);
216 let sqe = opcode::Send::new(
217 types::Fixed(0),
218 self.ack_buf.as_ptr(),
219 self.ack_buf.len() as u32,
220 )
221 .build()
222 .user_data(TOKEN_SEND);
223 unsafe { self.ring.submission().push(&sqe).expect("SQ full") };
227 self.ack_in_flight = true;
228 self.ack_offset = 0;
229 self.acked_since_recv = true;
230 }
231
232 fn settle_unprocessed(&mut self, rest: &[(u64, i32, u32)]) {
242 for &(token, _result, flags) in rest {
243 match token {
244 TOKEN_RECV if (flags & IORING_CQE_F_MORE) == 0 => self.multishot_active = false,
245 TOKEN_SEND => self.ack_in_flight = false,
246 _ => {}
247 }
248 }
249 }
250
251 fn drain(&mut self) -> bool {
264 loop {
270 match self.ring.submit() {
271 Ok(_) => break,
272 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
273 Err(_) => return false,
274 }
275 }
276 let mut backoff = DrainBackoff::new();
277 while self.multishot_active || self.ack_in_flight {
278 for cqe in self.ring.completion() {
279 match cqe.user_data() {
280 TOKEN_RECV if (cqe.flags() & IORING_CQE_F_MORE) == 0 => {
286 self.multishot_active = false;
287 }
288 TOKEN_SEND => self.ack_in_flight = false,
292 _ => {}
296 }
297 }
298 if (self.multishot_active || self.ack_in_flight) && !backoff.wait() {
299 return false;
300 }
301 }
302 true
303 }
304}
305
306impl Drop for UringTransport {
307 fn drop(&mut self) {
308 wake_pending_ops(self.tcp_fd);
309 if !self.drain() {
310 std::mem::forget(std::mem::take(&mut self.recv_pool));
316 std::mem::forget(std::mem::take(&mut self.ack_buf));
317 tracing::warn!(
318 multishot_active = self.multishot_active,
319 ack_in_flight = self.ack_in_flight,
320 "io_uring teardown drain did not complete; leaking receiver buffers"
321 );
322 }
323 }
324}
325
326impl ReceiverTransport for UringTransport {
327 fn poll_recv(&mut self, recv_buf: &mut Vec<u8>) -> io::Result<bool> {
328 use io_uring::{opcode, types};
329
330 if !self.connected {
331 return Err(io::Error::other("not connected"));
332 }
333
334 let pending = self.ring.submission().len();
337 if pending > 0 {
338 self.ring.submit()?;
339 }
340
341 let mut cqes: [(u64, i32, u32); 16] = [(0, 0, 0); 16];
343 let mut cqe_count = 0;
344 {
345 let mut cq = self.ring.completion();
346 while cqe_count < cqes.len() {
347 match cq.next() {
348 Some(cqe) => {
349 cqes[cqe_count] = (cqe.user_data(), cqe.result(), cqe.flags());
350 cqe_count += 1;
351 }
352 None => break,
353 }
354 }
355 }
356
357 let mut any_recv = false;
358 let mut submit_chained_ack = false;
362
363 for (idx, &(token, result, flags)) in cqes[..cqe_count].iter().enumerate() {
364 match token {
369 TOKEN_RECV => {
370 if (flags & IORING_CQE_F_MORE) == 0 {
371 self.multishot_active = false;
372 }
373 if result < 0 {
374 if result == -libc::ENOBUFS {
375 tracing::debug!("recv multishot ENOBUFS — pool exhausted");
376 continue;
377 }
378 self.connected = false;
379 self.settle_unprocessed(&cqes[idx + 1..cqe_count]);
380 return Err(io::Error::other(format!(
381 "primary disconnected (recv returned {result})"
382 )));
383 }
384 if result == 0 {
385 self.connected = false;
386 self.settle_unprocessed(&cqes[idx + 1..cqe_count]);
387 return Err(io::Error::other("primary disconnected (recv returned 0)"));
388 }
389 if (flags & IORING_CQE_F_BUFFER) == 0 {
390 self.connected = false;
391 self.settle_unprocessed(&cqes[idx + 1..cqe_count]);
392 return Err(io::Error::other("recv cqe missing F_BUFFER flag"));
393 }
394
395 let n = result as usize;
396 let buf_id = (flags >> IORING_CQE_BUFFER_SHIFT) as usize;
397 let buf_ptr = unsafe { self.pool_ptr.add(buf_id * RECV_BUF_SIZE) };
400 let slice = unsafe { std::slice::from_raw_parts(buf_ptr, n) };
401 recv_buf.extend_from_slice(slice);
402 any_recv = true;
403
404 let provide_sqe = opcode::ProvideBuffers::new(
406 buf_ptr,
407 RECV_BUF_SIZE as i32,
408 1,
409 RECV_BUF_GROUP_ID,
410 buf_id as u16,
411 )
412 .build()
413 .user_data(TOKEN_PROVIDE);
414 unsafe { self.ring.submission().push(&provide_sqe).expect("SQ full") };
415 }
416
417 TOKEN_PROVIDE if result < 0 => {
418 tracing::debug!("ProvideBuffers re-provision failed: {result}");
419 }
420
421 TOKEN_SEND => {
422 if result < 0 {
423 self.connected = false;
424 self.ack_in_flight = false;
427 self.settle_unprocessed(&cqes[idx + 1..cqe_count]);
428 return Err(io::Error::other(format!(
429 "ack send error (returned {result})"
430 )));
431 }
432 let sent = result as usize;
433 self.ack_offset += sent;
434 if self.ack_offset >= self.ack_buf.len() {
435 self.ack_buf.clear();
436 self.ack_offset = 0;
437 self.ack_in_flight = false;
438 if let Some(ack) = self.pending_ack.take() {
445 self.push_ack_send(&ack);
446 submit_chained_ack = true;
447 }
448 } else {
449 let sqe = opcode::Send::new(
451 types::Fixed(0),
452 self.ack_buf[self.ack_offset..].as_ptr(),
453 (self.ack_buf.len() - self.ack_offset) as u32,
454 )
455 .build()
456 .user_data(TOKEN_SEND);
457 unsafe { self.ring.submission().push(&sqe).expect("SQ full") };
458 }
459 }
460
461 _ => {}
462 }
463 }
464
465 if !self.multishot_active {
467 let sqe = opcode::RecvMulti::new(types::Fd(self.tcp_fd), RECV_BUF_GROUP_ID)
468 .build()
469 .user_data(TOKEN_RECV);
470 unsafe { self.ring.submission().push(&sqe).expect("SQ full") };
471 self.multishot_active = true;
472 }
473
474 if submit_chained_ack {
477 self.ring.submit()?;
478 }
479
480 if any_recv {
481 if self.acked_since_recv {
488 self.acked_since_recv = false;
489 } else {
490 arm_tcp_quickack(self.tcp_fd);
491 }
492 }
493
494 Ok(any_recv)
495 }
496
497 fn send_ack(&mut self, ack: &Ack) -> io::Result<bool> {
498 if self.ack_in_flight {
499 self.pending_ack = Some(*ack);
503 return Ok(true);
504 }
505 self.push_ack_send(ack);
506 Ok(true)
507 }
508
509 fn ack_in_flight(&self) -> bool {
510 self.ack_in_flight || self.pending_ack.is_some()
515 }
516
517 fn is_connected(&mut self) -> bool {
518 self.connected
519 }
520}
521
522struct TcpFrameSource<'a> {
530 reader: &'a mut TcpStream,
531}
532
533impl ControlFrameSource for TcpFrameSource<'_> {
534 fn next_frame(
535 &mut self,
536 max_size: usize,
537 ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
538 Ok(read_frame(self.reader, max_size)?)
539 }
540}
541
542pub type ReceiverResult<A, W> = Result<Option<(A, W)>, Box<dyn std::error::Error>>;
544
545#[allow(clippy::too_many_arguments)]
546pub fn run_receiver<A>(
547 primary_addr: SocketAddr,
548 journal_path: &std::path::Path,
549 signing_key: &ed25519_dalek::SigningKey,
550 shutdown: &AtomicBool,
551 control: &super::ReplicaControlPlane,
555 snapshot_interval_ms: u64,
556 snapshot_path: std::path::PathBuf,
557 cores: crate::layout::PipelineCores,
558 staging_mode: melin_journal::StagingMode,
561 group_commit_delay: std::time::Duration,
562 pipeline_depth: usize,
563 fence_state: std::sync::Arc<melin_transport_core::fence::FenceState>,
564 sizing: &A::Sizing,
567) -> ReceiverResult<A, BufferedWriter<A::Event>>
568where
569 A: Application + Send + 'static,
570 A::Event: Send + Sync + 'static,
571 A::Report: Send + 'static,
572 A::QueryResponse: Send + 'static,
573{
574 let super::ReplicaControlPlane {
575 promote,
576 tip_ready,
577 journal_tip,
578 primary_link_up,
579 primary_ack_policy,
580 pipeline_healthy,
581 } = control;
582 let (mut exchange, mut journal_writer, mut last_sequence, mut chain_hash) =
586 recover_replica_state::<A, BufferedWriter<A::Event>>(
587 journal_path,
588 &snapshot_path,
589 &fence_state,
590 sizing,
591 )?;
592 journal_tip.advance(melin_transport_core::WireSeq::new(last_sequence));
598 tip_ready.store(true, Ordering::Release);
599
600 let mut backoff = std::time::Duration::from_secs(1);
601
602 let mut divergence_resyncs: u32 = 0;
605
606 let mut send_buf = Vec::with_capacity(64);
607 let mut pipeline: Option<ReplicaPipelineHandles<A, BufferedWriter<A::Event>>> = None;
608
609 loop {
611 primary_link_up.store(false, Ordering::Release);
614
615 if let Some(p) = pipeline.as_ref() {
616 if let Some(ref lock) = p.chain_hash_lock {
625 let fsync_state = lock.load();
626 last_sequence = fsync_state.journal_seq.get();
627 chain_hash = fsync_state.chain_hash;
628 } else {
629 last_sequence = p.last_seq.load().get();
630 }
631 }
632
633 if shutdown.load(Ordering::Relaxed) {
638 if let Some(p) = pipeline.take() {
639 let _ = teardown_replica_pipeline::<A, BufferedWriter<A::Event>>(p);
640 }
641 return Ok(None);
642 }
643 if promote.is_requested() {
644 info!("promotion triggered while disconnected");
645 return take_pipeline_for_promotion(&mut pipeline, &mut exchange, &mut journal_writer);
646 }
647
648 info!(primary = %primary_addr, "connecting to primary as replica");
650
651 let stream = match TcpStream::connect(primary_addr) {
652 Ok(s) => s,
653 Err(e) => {
654 warn!(
655 error = %e,
656 backoff_secs = backoff.as_secs(),
657 "failed to connect to primary — retrying"
658 );
659 sleep_then_double_backoff(&mut backoff, shutdown, promote);
660 continue;
661 }
662 };
663 if let Err(e) = stream.set_nodelay(true) {
664 warn!(error = %e, "failed to set TCP_NODELAY on replica receive socket");
665 }
666 {
667 use std::os::unix::io::AsRawFd;
668 let busy_poll: libc::c_int = 50;
669 let rc = unsafe {
670 libc::setsockopt(
671 stream.as_raw_fd(),
672 libc::SOL_SOCKET,
673 libc::SO_BUSY_POLL,
674 &busy_poll as *const libc::c_int as *const libc::c_void,
675 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
676 )
677 };
678 if rc != 0 {
679 let err = std::io::Error::last_os_error();
680 warn!(error = %err, "failed to set SO_BUSY_POLL on replica receive socket");
681 }
682 }
683 stream.set_read_timeout(Some(std::time::Duration::from_secs(5)))?;
684
685 let mut reader = stream.try_clone()?;
686 let mut tcp_writer = stream;
687
688 if let Err(e) = authenticate_with_primary(&mut reader, signing_key) {
691 if matches!(
698 e.kind(),
699 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
700 ) {
701 warn!(error = %e, "no auth challenge from primary (not serving yet?) — retrying");
702 } else {
703 warn!(error = %e, "authentication failed — retrying");
704 }
705 drop(reader);
710 drop(tcp_writer);
711 sleep_then_double_backoff(&mut backoff, shutdown, promote);
714 continue;
715 }
716 info!("authenticated with primary");
717 primary_link_up.store(true, Ordering::Release);
720
721 let handshake = Handshake {
726 last_sequence,
727 chain_hash,
728 epoch: fence_state.epoch(),
729 };
730 send_buf.clear();
731 encode_handshake(&handshake, &mut send_buf);
732 tcp_writer.write_all(&send_buf)?;
733 tcp_writer.flush()?;
734 send_buf.clear();
735
736 let response_frame = read_frame(&mut reader, MAX_CONTROL_FRAME)?;
743 let response = decode_primary_message(&response_frame)?;
744 let (stream_lineage, session_start) = match response {
745 PrimaryMessage::StreamStart {
746 start_sequence,
747 segment_start_sequence,
748 anchor_hash,
749 epoch,
750 ack_policy,
751 } => {
752 let our_epoch = fence_state.epoch();
760 if fence_state.refuses_primary(epoch) {
761 warn!(
762 primary_epoch = epoch,
763 our_epoch,
764 "primary is behind our fencing epoch — refusing to follow stale primary"
765 );
766 drop(reader);
769 drop(tcp_writer);
770 sleep_then_double_backoff(&mut backoff, shutdown, promote);
771 continue;
772 }
773 fence_state.observe_epoch(epoch);
777 primary_ack_policy.store(ack_policy, Ordering::Release);
778 info!(start_sequence, epoch, ack_policy, "streaming started");
779 ((segment_start_sequence, anchor_hash), last_sequence)
780 }
781 ref resync @ (PrimaryMessage::NeedSnapshot | PrimaryMessage::HashMismatch) => {
782 let divergent = matches!(resync, PrimaryMessage::HashMismatch);
783 let decision = handle_resync_verdict(
787 divergent,
788 &mut TcpFrameSource {
789 reader: &mut reader,
790 },
791 &mut pipeline,
792 &mut exchange,
793 &mut journal_writer,
794 journal_path,
795 &snapshot_path,
796 &fence_state,
797 control,
798 &mut last_sequence,
799 &mut chain_hash,
800 )
801 .map_err(|e| -> Box<dyn std::error::Error> { e })?;
802 match decision {
803 ResyncDecision::Ready {
804 segment_start_sequence,
805 anchor_hash,
806 resume_sequence,
807 } => ((segment_start_sequence, anchor_hash), resume_sequence),
808 ResyncDecision::Retry => {
809 sleep_then_double_backoff(&mut backoff, shutdown, promote);
810 continue;
811 }
812 }
813 }
814 _ => {
815 return Err(format!("unexpected response: {response:?}").into());
816 }
817 };
818
819 if pipeline.is_none() && journal_writer.is_none() {
827 let (lineage_start, lineage_anchor) = stream_lineage;
828 let writer =
829 BufferedWriter::create_continuing(journal_path, lineage_start, lineage_anchor)?;
830 exchange = Some(A::default());
831 journal_writer = Some(writer);
832 }
833
834 if pipeline.is_none() {
836 let cur_exchange = exchange.take().expect("exchange initialized");
837 let cur_writer = journal_writer.take().expect("journal_writer initialized");
838 pipeline = Some(build_replica_pipeline_with_threads::<A>(
839 cur_exchange,
840 cur_writer,
841 cores,
842 staging_mode,
843 snapshot_interval_ms,
844 snapshot_path.clone(),
845 group_commit_delay,
846 Arc::clone(&fence_state),
847 Arc::clone(pipeline_healthy),
848 sizing,
849 )?);
850 }
851
852 let result = {
854 let p = pipeline.as_mut().expect("pipeline must exist by here");
855 let input_producer = &mut p.input_producer;
856 let journal_cursor = p.journal_cursor.as_ref();
857 let stream_marks = &p.stream_marks;
858 let journal_failed = &p.journal_failed;
859 std::thread::scope(|s| {
860 let handle = std::thread::Builder::new()
861 .name("replica-receiver".into())
862 .spawn_scoped(s, || {
863 melin_app::affinity::pin_thread("replica-receiver", cores.reader.core);
864 let mut transport = match UringTransport::new(&tcp_writer) {
865 Ok(t) => t,
866 Err(e) => {
867 tracing::error!(error = %e, "UringTransport init failed");
868 return super::receiver_transport::StreamingResult {
874 exit: SessionExit::Disconnected,
875 heard_from_primary: false,
876 };
877 }
878 };
879 streaming_loop::<UringTransport, A::Event>(
880 &mut transport,
881 input_producer,
882 journal_cursor,
883 shutdown,
884 control,
885 pipeline_depth,
886 cores.reader.wait,
887 session_start,
888 Vec::with_capacity(MAX_DATA_FRAME + 4),
889 None,
890 stream_marks,
891 journal_failed,
892 )
893 })
894 .expect("spawn replica-receiver thread");
895 handle.join().expect("replica-receiver thread panicked")
896 })
897 };
898
899 primary_link_up.store(false, Ordering::Release);
900 match handle_session_exit(
901 result,
902 &mut pipeline,
903 &mut divergence_resyncs,
904 &mut backoff,
905 last_sequence,
906 journal_path,
907 &snapshot_path,
908 &fence_state,
909 shutdown,
910 promote,
911 || {},
915 sizing,
916 ) {
917 AfterSession::Return(r) => return r,
918 AfterSession::Resync {
919 exchange: ex,
920 journal_writer: wr,
921 last_sequence: seq,
922 chain_hash: hash,
923 } => {
924 exchange = ex;
925 journal_writer = wr;
926 last_sequence = seq;
927 chain_hash = hash;
928 continue;
929 }
930 AfterSession::Reconnect => {}
931 }
932 }
933}
934
935#[cfg(test)]
940mod tests {
941 use super::*;
942 use crate::uring_teardown::DRAIN_TIMEOUT;
943 use std::io::Read;
944 use std::net::TcpListener;
945 use std::time::{Duration, Instant};
946
947 use melin_transport_core::replication::protocol::{ReplicaMessage, decode_replica_message};
948
949 fn socket_pair() -> (TcpStream, TcpStream) {
951 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
952 let addr = listener.local_addr().unwrap();
953 let client = TcpStream::connect(addr).unwrap();
954 let (server, _) = listener.accept().unwrap();
955 (client, server)
956 }
957
958 fn ack(seq: u64) -> Ack {
959 Ack {
960 acked_sequence: seq,
961 in_memory_sequence: seq,
962 }
963 }
964
965 fn flush_acks(transport: &mut UringTransport) {
967 let mut recv_buf = Vec::new();
968 let deadline = Instant::now() + Duration::from_secs(5);
969 while transport.ack_in_flight() {
970 transport.poll_recv(&mut recv_buf).unwrap();
971 assert!(Instant::now() < deadline, "ack send never completed");
972 std::hint::spin_loop();
973 }
974 }
975
976 fn read_acks(peer: &mut TcpStream, expect: usize) -> Vec<Ack> {
978 peer.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
979 let mut buf = Vec::new();
980 let mut chunk = [0u8; 256];
981 let mut acks = Vec::new();
982 while acks.len() < expect {
983 let n = peer.read(&mut chunk).expect("peer read");
984 assert!(n > 0, "peer closed before all acks arrived");
985 buf.extend_from_slice(&chunk[..n]);
986 loop {
988 if buf.len() < 4 {
989 break;
990 }
991 let len = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
992 if buf.len() < 4 + len {
993 break;
994 }
995 match decode_replica_message(&buf[4..4 + len]).expect("decodable frame") {
996 ReplicaMessage::Ack(a) => acks.push(a),
997 other => panic!("expected Ack frame, got {other:?}"),
998 }
999 buf.drain(..4 + len);
1000 }
1001 }
1002 assert!(buf.is_empty(), "trailing bytes after expected acks");
1003 acks
1004 }
1005
1006 #[test]
1007 fn coalesces_to_latest_ack_while_send_in_flight() {
1008 let (client, mut peer) = socket_pair();
1009 let mut transport = UringTransport::new(&client).unwrap();
1010
1011 assert!(transport.send_ack(&ack(1)).unwrap());
1016 assert!(transport.ack_in_flight());
1017 assert!(transport.send_ack(&ack(2)).unwrap());
1018 assert!(transport.send_ack(&ack(3)).unwrap());
1019
1020 flush_acks(&mut transport);
1021
1022 let acks = read_acks(&mut peer, 2);
1024 let seqs: Vec<u64> = acks.iter().map(|a| a.acked_sequence).collect();
1025 assert_eq!(seqs, vec![1, 3]);
1026 }
1027
1028 #[test]
1029 fn sequential_acks_all_reach_the_wire_in_order() {
1030 let (client, mut peer) = socket_pair();
1031 let mut transport = UringTransport::new(&client).unwrap();
1032
1033 for seq in 1..=3 {
1034 assert!(transport.send_ack(&ack(seq)).unwrap());
1035 flush_acks(&mut transport);
1036 }
1037
1038 let acks = read_acks(&mut peer, 3);
1039 let seqs: Vec<u64> = acks.iter().map(|a| a.acked_sequence).collect();
1040 assert_eq!(seqs, vec![1, 2, 3]);
1041 }
1042
1043 #[test]
1051 fn drop_quiesces_the_multishot_recv() {
1052 let (client, _peer) = socket_pair();
1053 let mut transport = UringTransport::new(&client).unwrap();
1054 let mut recv_buf = Vec::new();
1057 transport.poll_recv(&mut recv_buf).unwrap();
1058 assert!(transport.multishot_active);
1059
1060 let started = Instant::now();
1061 drop(transport);
1062 let elapsed = started.elapsed();
1063 assert!(
1064 elapsed < DRAIN_TIMEOUT / 4,
1065 "teardown took {elapsed:?}, close to the {DRAIN_TIMEOUT:?} deadline"
1066 );
1067 }
1068
1069 #[test]
1076 fn abandoned_cqes_still_settle_kernel_ownership() {
1077 let (client, _peer) = socket_pair();
1078 let mut transport = UringTransport::new(&client).unwrap();
1079 let mut recv_buf = Vec::new();
1080 transport.poll_recv(&mut recv_buf).unwrap();
1081
1082 transport.ack_in_flight = true;
1085 transport.settle_unprocessed(&[(TOKEN_SEND, 13, 0)]);
1086 assert!(
1087 !transport.ack_in_flight,
1088 "an abandoned SEND completion must release `ack_buf`"
1089 );
1090
1091 transport.settle_unprocessed(&[(TOKEN_RECV, 128, IORING_CQE_F_MORE)]);
1094 assert!(
1095 transport.multishot_active,
1096 "F_MORE must leave the multishot armed"
1097 );
1098 transport.settle_unprocessed(&[(TOKEN_RECV, 0, 0)]);
1100 assert!(!transport.multishot_active);
1101
1102 transport.multishot_active = true;
1106 }
1107
1108 #[test]
1112 fn drop_quiesces_a_pending_ack_send() {
1113 let (client, _peer) = socket_pair();
1114 let mut transport = UringTransport::new(&client).unwrap();
1115 assert!(transport.send_ack(&ack(7)).unwrap());
1116 assert!(transport.ack_in_flight());
1117
1118 let started = Instant::now();
1119 drop(transport);
1120 let elapsed = started.elapsed();
1121 assert!(
1122 elapsed < DRAIN_TIMEOUT / 4,
1123 "teardown took {elapsed:?}, close to the {DRAIN_TIMEOUT:?} deadline"
1124 );
1125 }
1126
1127 #[cfg(not(feature = "no-persist"))]
1132 mod scripted {
1133 use super::*;
1134 use melin_app::{AppEvent, Application, ApplyCtx, CodecError, RejectReason};
1135
1136 pub(super) use melin_transport_core::pipeline::InputSlot;
1139
1140 #[derive(Debug, Clone, Copy, PartialEq)]
1141 pub(super) struct EvtAdd(pub(super) u64);
1142
1143 impl AppEvent for EvtAdd {
1144 const MAX_ENCODED_SIZE: usize = 8;
1145
1146 fn encoded_size(&self) -> usize {
1147 8
1148 }
1149 fn encode(&self, buf: &mut [u8]) -> usize {
1150 buf[..8].copy_from_slice(&self.0.to_le_bytes());
1151 8
1152 }
1153 fn decode(buf: &[u8]) -> Result<Self, CodecError> {
1154 if buf.len() < 8 {
1155 return Err(CodecError::Truncated);
1156 }
1157 Ok(EvtAdd(u64::from_le_bytes(buf[..8].try_into().expect("8"))))
1158 }
1159 fn is_query(&self) -> bool {
1160 false
1161 }
1162 }
1163
1164 #[derive(Debug, Clone, Copy)]
1165 pub(super) struct Rpt;
1166
1167 #[derive(Default)]
1168 pub(super) struct App;
1169
1170 impl Application for App {
1171 type Event = EvtAdd;
1172 type Report = Rpt;
1173 type QueryResponse = Rpt;
1174 type Sizing = ();
1175 const APP_VERSION: u16 = 1;
1176
1177 fn apply(&mut self, _e: EvtAdd, _ctx: &ApplyCtx, _out: &mut Vec<Rpt>) -> Option<Rpt> {
1178 None
1179 }
1180 fn tick(&mut self, _now_ns: u64, _out: &mut Vec<Rpt>) {}
1181 fn check_request_seq(&mut self, _key_hash: u64, _seq: u64) -> bool {
1182 true
1183 }
1184 fn build_reject(_e: &EvtAdd, _r: RejectReason) -> Rpt {
1185 Rpt
1186 }
1187 fn snapshot<W: std::io::Write>(&self, _w: &mut W) -> std::io::Result<()> {
1188 Ok(())
1189 }
1190 fn restore<R: std::io::Read>(_r: &mut R) -> std::io::Result<Self> {
1191 Ok(App)
1192 }
1193 }
1194
1195 pub(super) const REPLICA_KEY: [u8; 32] = [0xFC; 32];
1198
1199 pub(super) fn replica_auth() -> melin_app::auth::AuthorizedKeys {
1202 let key = ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY);
1203 let pub_b64 = base64::Engine::encode(
1204 &base64::engine::general_purpose::STANDARD,
1205 key.verifying_key().to_bytes(),
1206 );
1207 melin_app::auth::AuthorizedKeys::parse(&format!("replication {pub_b64} test-replica\n"))
1208 .expect("parse keys")
1209 }
1210
1211 pub(super) fn read_replica_msg(stream: &mut TcpStream) -> ReplicaMessage {
1213 let mut len_buf = [0u8; 4];
1214 stream.read_exact(&mut len_buf).expect("frame length");
1215 let len = u32::from_le_bytes(len_buf) as usize;
1216 let mut payload = vec![0u8; len];
1217 stream.read_exact(&mut payload).expect("frame payload");
1218 decode_replica_message(&payload).expect("decodable replica frame")
1219 }
1220
1221 pub(super) fn wait_for_ack(stream: &mut TcpStream, seq: u64) {
1223 loop {
1224 if let ReplicaMessage::Ack(a) = read_replica_msg(stream)
1225 && a.acked_sequence >= seq
1226 {
1227 return;
1228 }
1229 }
1230 }
1231
1232 pub(super) fn accept_within(listener: &std::net::TcpListener, secs: u64) -> TcpStream {
1235 let deadline = Instant::now() + Duration::from_secs(secs);
1236 loop {
1237 match listener.accept() {
1238 Ok((s, _)) => {
1239 s.set_read_timeout(Some(Duration::from_secs(10)))
1240 .expect("read timeout");
1241 return s;
1242 }
1243 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
1244 assert!(Instant::now() < deadline, "replica never connected");
1245 std::thread::sleep(Duration::from_millis(10));
1246 }
1247 Err(e) => panic!("accept failed: {e}"),
1248 }
1249 }
1250 }
1251 }
1252
1253 #[cfg(all(feature = "hash-chain", not(feature = "no-persist")))]
1259 mod divergence_resync {
1260 use super::super::super::auth::authenticate_replica;
1261 use super::scripted::*;
1262 use super::*;
1263 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
1264 use melin_transport_core::cursors::WireSeq;
1265 use melin_transport_core::replication::catchup::{lineage_origin, snapshot_transfer_with};
1266 use melin_transport_core::replication::protocol::{
1267 encode_hash_mismatch, encode_rotate, encode_stream_start,
1268 };
1269 use std::sync::Arc;
1270 use std::sync::atomic::AtomicBool;
1271
1272 #[test]
1281 fn mid_stream_divergence_resyncs_in_process() {
1282 let dir = tempfile::tempdir().expect("tempdir");
1283
1284 let primary_journal = dir.path().join("primary.journal");
1288 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1289 let mut chain_at_4 = [0u8; 32];
1290 for v in 1..=5u64 {
1291 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1292 if v == 2 {
1293 w.rotate_segment().expect("rotate");
1294 }
1295 if v == 4 {
1296 chain_at_4 = w.chain_hash().expect("chain");
1297 }
1298 }
1299 drop(w);
1300 melin_transport_core::snapshot::save::<App>(
1301 &App,
1302 WireSeq::new(4),
1303 chain_at_4,
1304 0,
1305 &primary_journal.with_extension("snapshot"),
1306 )
1307 .expect("save snapshot");
1308 let (lineage_start, lineage_anchor) =
1309 lineage_origin(&primary_journal).expect("lineage");
1310
1311 let authorized_keys = replica_auth();
1313
1314 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1315 listener.set_nonblocking(true).expect("nonblocking");
1316 let addr = listener.local_addr().expect("addr");
1317
1318 let replica_journal = dir.path().join("replica.journal");
1320 let replica_snapshot = dir.path().join("replica.snapshot");
1321 let shutdown = Arc::new(AtomicBool::new(false));
1322 let control = crate::replication::ReplicaControlPlane::new();
1323 let cores = crate::layout::PipelineCores::unpinned();
1324 let replica = {
1325 let journal = replica_journal.clone();
1326 let shutdown = Arc::clone(&shutdown);
1327 let control = control.clone();
1328 std::thread::spawn(move || -> Result<bool, String> {
1329 run_receiver::<App>(
1330 addr,
1331 &journal,
1332 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1333 &shutdown,
1334 &control,
1335 3_600_000,
1336 replica_snapshot,
1337 cores,
1338 melin_journal::StagingMode::ZeroFill,
1339 Duration::ZERO,
1340 64,
1341 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1342 &(),
1343 )
1344 .map(|state| state.is_none())
1346 .map_err(|e| e.to_string())
1347 })
1348 };
1349
1350 let mut s1 = accept_within(&listener, 30);
1353 let mut s1r = s1.try_clone().expect("clone");
1354 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1355 match read_replica_msg(&mut s1) {
1356 ReplicaMessage::Handshake(h) => {
1357 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
1358 }
1359 other => panic!("expected Handshake, got {other:?}"),
1360 }
1361 let mut buf = Vec::new();
1362 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
1363 s1.write_all(&buf).expect("StreamStart 1");
1364 buf.clear();
1365 melin_transport_core::replication_wire::encode_input_batch(
1366 &[InputSlot::<EvtAdd> {
1367 connection_id: 0,
1368 key_hash: 0,
1369 request_seq: 0,
1370 sequence: 1,
1371 timestamp_ns: 1,
1372 event: JournalEvent::App(EvtAdd(1)),
1373 publish_ts: Default::default(),
1375 recv_ts: Default::default(),
1376 }],
1377 &mut buf,
1378 );
1379 s1.write_all(&buf).expect("InputBatch");
1380 wait_for_ack(&mut s1, 1);
1381
1382 buf.clear();
1385 encode_rotate(1, &[0xEE; 32], &mut buf);
1386 s1.write_all(&buf).expect("Rotate");
1387
1388 let mut s2 = accept_within(&listener, 30);
1391 let mut s2r = s2.try_clone().expect("clone");
1392 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1393 match read_replica_msg(&mut s2) {
1394 ReplicaMessage::Handshake(h) => assert_eq!(
1395 h.last_sequence, 1,
1396 "post-divergence handshake must carry the recovered journal position"
1397 ),
1398 other => panic!("expected Handshake, got {other:?}"),
1399 }
1400 assert!(
1406 !control.pipeline_healthy.load(Ordering::Acquire),
1407 "the divergence must have latched the health mirror false"
1408 );
1409 buf.clear();
1410 encode_hash_mismatch(&mut buf);
1411 s2.write_all(&buf).expect("HashMismatch");
1412 let transfer_shutdown = AtomicBool::new(false);
1413 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1414 s2.write_all(b)?;
1415 s2.flush()
1416 };
1417 let end = snapshot_transfer_with::<EvtAdd>(
1420 &primary_journal,
1421 &mut publish,
1422 &transfer_shutdown,
1423 1,
1424 )
1425 .expect("snapshot transfer");
1426 assert_eq!(
1427 end,
1428 melin_transport_core::replication::catchup::CatchUpResult::Ok(5)
1429 );
1430 let mut s2_acks = s2.try_clone().expect("clone");
1431 wait_for_ack(&mut s2_acks, 5);
1432
1433 assert!(
1436 control.pipeline_healthy.load(Ordering::Acquire),
1437 "the resync rebuild must reset the health mirror true"
1438 );
1439
1440 shutdown.store(true, Ordering::Relaxed);
1443 let result = replica.join().expect("replica thread panicked");
1444 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
1445
1446 assert!(
1449 dir.path().join("replica.journal.divergent.0").exists(),
1450 "divergent lineage must be archived"
1451 );
1452 assert!(replica_journal.exists(), "re-seeded live journal");
1453 }
1454
1455 #[test]
1469 fn resync_transfer_failure_rebuilds_clean_not_over_stale_writer() {
1470 let dir = tempfile::tempdir().expect("tempdir");
1471
1472 let primary_journal = dir.path().join("primary.journal");
1476 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1477 for v in 1..=2u64 {
1478 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1479 }
1480 drop(w);
1481 let (lineage_start, lineage_anchor) =
1482 lineage_origin(&primary_journal).expect("lineage");
1483
1484 let authorized_keys = replica_auth();
1485
1486 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1487 listener.set_nonblocking(true).expect("nonblocking");
1488 let addr = listener.local_addr().expect("addr");
1489
1490 let replica_journal = dir.path().join("replica.journal");
1491 let replica_snapshot = dir.path().join("replica.snapshot");
1492 let shutdown = Arc::new(AtomicBool::new(false));
1493 let control = crate::replication::ReplicaControlPlane::new();
1494 let cores = crate::layout::PipelineCores::unpinned();
1495 let replica = {
1496 let journal = replica_journal.clone();
1497 let shutdown = Arc::clone(&shutdown);
1498 let control = control.clone();
1499 std::thread::spawn(move || -> Result<bool, String> {
1500 run_receiver::<App>(
1501 addr,
1502 &journal,
1503 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1504 &shutdown,
1505 &control,
1506 3_600_000,
1507 replica_snapshot,
1508 cores,
1509 melin_journal::StagingMode::ZeroFill,
1510 Duration::ZERO,
1511 64,
1512 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1513 &(),
1514 )
1515 .map(|state| state.is_none())
1516 .map_err(|e| e.to_string())
1517 })
1518 };
1519
1520 let stream_one = |s: &mut TcpStream, buf: &mut Vec<u8>| {
1523 buf.clear();
1524 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, buf);
1525 s.write_all(buf).expect("StreamStart");
1526 buf.clear();
1527 melin_transport_core::replication_wire::encode_input_batch(
1528 &[InputSlot::<EvtAdd> {
1529 connection_id: 0,
1530 key_hash: 0,
1531 request_seq: 0,
1532 sequence: 1,
1533 timestamp_ns: 1,
1534 event: JournalEvent::App(EvtAdd(1)),
1535 publish_ts: Default::default(),
1536 recv_ts: Default::default(),
1537 }],
1538 buf,
1539 );
1540 s.write_all(buf).expect("InputBatch");
1541 };
1542
1543 let mut buf = Vec::new();
1548 let mut s1 = accept_within(&listener, 30);
1549 let mut s1r = s1.try_clone().expect("clone");
1550 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1551 match read_replica_msg(&mut s1) {
1552 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0, "fresh"),
1553 other => panic!("expected Handshake, got {other:?}"),
1554 }
1555 stream_one(&mut s1, &mut buf);
1556 wait_for_ack(&mut s1, 1);
1557 buf.clear();
1558 encode_rotate(1, &[0xEE; 32], &mut buf);
1559 s1.write_all(&buf).expect("Rotate");
1560
1561 let mut s2 = accept_within(&listener, 30);
1566 let mut s2r = s2.try_clone().expect("clone");
1567 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1568 match read_replica_msg(&mut s2) {
1569 ReplicaMessage::Handshake(h) => assert_eq!(
1570 h.last_sequence, 1,
1571 "reconnect carries the recovered forked position"
1572 ),
1573 other => panic!("expected Handshake, got {other:?}"),
1574 }
1575 buf.clear();
1576 encode_hash_mismatch(&mut buf);
1577 s2.write_all(&buf).expect("HashMismatch");
1578 s2.flush().expect("flush");
1579 drop(s2);
1582 drop(s2r);
1583
1584 let mut s3 = accept_within(&listener, 30);
1589 let mut s3r = s3.try_clone().expect("clone");
1590 authenticate_replica(&mut s3r, &authorized_keys).expect("auth 3");
1591 match read_replica_msg(&mut s3) {
1592 ReplicaMessage::Handshake(h) => assert_eq!(
1593 h.last_sequence, 0,
1594 "after a failed transfer the retry handshakes as fresh"
1595 ),
1596 other => panic!("expected Handshake, got {other:?}"),
1597 }
1598 stream_one(&mut s3, &mut buf);
1599 wait_for_ack(&mut s3, 1);
1600
1601 shutdown.store(true, Ordering::Relaxed);
1602 let result = replica.join().expect("replica thread panicked");
1603 assert_eq!(result, Ok(true), "receiver exits cleanly via shutdown");
1604
1605 assert!(
1610 dir.path().join("replica.journal.divergent.0").exists(),
1611 "forked lineage archived, never deleted"
1612 );
1613 assert!(
1614 replica_journal.exists(),
1615 "fresh live journal recreated after the failed transfer"
1616 );
1617 }
1618
1619 #[test]
1626 fn second_mid_stream_divergence_exits_hard() {
1627 let dir = tempfile::tempdir().expect("tempdir");
1628
1629 let primary_journal = dir.path().join("primary.journal");
1630 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1631 let mut chain_at_4 = [0u8; 32];
1632 for v in 1..=5u64 {
1633 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1634 if v == 2 {
1635 w.rotate_segment().expect("rotate");
1636 }
1637 if v == 4 {
1638 chain_at_4 = w.chain_hash().expect("chain");
1639 }
1640 }
1641 drop(w);
1642 melin_transport_core::snapshot::save::<App>(
1643 &App,
1644 WireSeq::new(4),
1645 chain_at_4,
1646 0,
1647 &primary_journal.with_extension("snapshot"),
1648 )
1649 .expect("save snapshot");
1650 let (lineage_start, lineage_anchor) =
1651 lineage_origin(&primary_journal).expect("lineage");
1652
1653 let authorized_keys = replica_auth();
1654
1655 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1656 listener.set_nonblocking(true).expect("nonblocking");
1657 let addr = listener.local_addr().expect("addr");
1658
1659 let replica_journal = dir.path().join("replica.journal");
1660 let replica_snapshot = dir.path().join("replica.snapshot");
1661 let shutdown = Arc::new(AtomicBool::new(false));
1662 let control = crate::replication::ReplicaControlPlane::new();
1663 let cores = crate::layout::PipelineCores::unpinned();
1664 let replica = {
1665 let journal = replica_journal.clone();
1666 let shutdown = Arc::clone(&shutdown);
1667 let control = control.clone();
1668 std::thread::spawn(move || -> Result<bool, String> {
1669 run_receiver::<App>(
1670 addr,
1671 &journal,
1672 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1673 &shutdown,
1674 &control,
1675 3_600_000,
1676 replica_snapshot,
1677 cores,
1678 melin_journal::StagingMode::ZeroFill,
1679 Duration::ZERO,
1680 64,
1681 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1682 &(),
1683 )
1684 .map(|state| state.is_none())
1685 .map_err(|e| e.to_string())
1686 })
1687 };
1688
1689 let mut s1 = accept_within(&listener, 30);
1691 let mut s1r = s1.try_clone().expect("clone");
1692 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1693 match read_replica_msg(&mut s1) {
1694 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0),
1695 other => panic!("expected Handshake, got {other:?}"),
1696 }
1697 let mut buf = Vec::new();
1698 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
1699 s1.write_all(&buf).expect("StreamStart 1");
1700 buf.clear();
1701 melin_transport_core::replication_wire::encode_input_batch(
1702 &[InputSlot::<EvtAdd> {
1703 connection_id: 0,
1704 key_hash: 0,
1705 request_seq: 0,
1706 sequence: 1,
1707 timestamp_ns: 1,
1708 event: JournalEvent::App(EvtAdd(1)),
1709 publish_ts: Default::default(),
1710 recv_ts: Default::default(),
1711 }],
1712 &mut buf,
1713 );
1714 s1.write_all(&buf).expect("InputBatch");
1715 wait_for_ack(&mut s1, 1);
1716 buf.clear();
1717 encode_rotate(1, &[0xEE; 32], &mut buf);
1718 s1.write_all(&buf).expect("Rotate 1");
1719
1720 let mut s2 = accept_within(&listener, 30);
1723 let mut s2r = s2.try_clone().expect("clone");
1724 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1725 match read_replica_msg(&mut s2) {
1726 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 1),
1727 other => panic!("expected Handshake, got {other:?}"),
1728 }
1729 buf.clear();
1730 encode_hash_mismatch(&mut buf);
1731 s2.write_all(&buf).expect("HashMismatch");
1732 let transfer_shutdown = AtomicBool::new(false);
1733 {
1734 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1735 s2.write_all(b)?;
1736 s2.flush()
1737 };
1738 snapshot_transfer_with::<EvtAdd>(
1739 &primary_journal,
1740 &mut publish,
1741 &transfer_shutdown,
1742 1,
1743 )
1744 .expect("snapshot transfer");
1745 }
1746 buf.clear();
1747 melin_transport_core::replication_wire::encode_input_batch(
1748 &[InputSlot::<EvtAdd> {
1749 connection_id: 0,
1750 key_hash: 0,
1751 request_seq: 0,
1752 sequence: 6,
1753 timestamp_ns: 6,
1754 event: JournalEvent::App(EvtAdd(6)),
1755 publish_ts: Default::default(),
1756 recv_ts: Default::default(),
1757 }],
1758 &mut buf,
1759 );
1760 s2.write_all(&buf).expect("InputBatch 6");
1761 let mut s2_acks = s2.try_clone().expect("clone");
1762 wait_for_ack(&mut s2_acks, 6);
1763 buf.clear();
1764 encode_rotate(6, &[0xEE; 32], &mut buf);
1765 s2.write_all(&buf).expect("Rotate 2");
1766
1767 let result = replica.join().expect("replica thread panicked");
1770 let err = result.expect_err("second divergence must exit hard");
1771 assert!(
1772 err.contains("divergence recurred"),
1773 "error must name the recurrence: {err}"
1774 );
1775 }
1776
1777 type PromoteHandles =
1786 crate::replication::ReplicaPipelineHandles<App, BufferedWriter<EvtAdd>>;
1787
1788 #[test]
1789 fn promotion_hands_back_local_state_when_present() {
1790 let dir = tempfile::tempdir().unwrap();
1791 let mut pipeline: Option<PromoteHandles> = None;
1792 let mut exchange = Some(App);
1793 let mut journal_writer =
1794 Some(BufferedWriter::<EvtAdd>::create(&dir.path().join("p.journal")).unwrap());
1795
1796 let result = crate::replication::take_pipeline_for_promotion(
1797 &mut pipeline,
1798 &mut exchange,
1799 &mut journal_writer,
1800 );
1801
1802 assert!(matches!(result, Ok(Some(_))), "warm state must be promoted");
1803 assert!(
1804 exchange.is_none() && journal_writer.is_none(),
1805 "state must be moved into the result"
1806 );
1807 }
1808
1809 #[test]
1810 fn promotion_errs_when_no_local_state() {
1811 let mut pipeline: Option<PromoteHandles> = None;
1812 let mut exchange: Option<App> = None;
1813 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
1814
1815 let result = crate::replication::take_pipeline_for_promotion(
1816 &mut pipeline,
1817 &mut exchange,
1818 &mut journal_writer,
1819 );
1820
1821 let err = match result {
1823 Err(e) => e,
1824 Ok(_) => panic!("a promote with nothing to promote must error"),
1825 };
1826 assert!(
1827 err.to_string().contains("no local state available"),
1828 "{err}"
1829 );
1830 }
1831
1832 #[test]
1833 fn promotion_errs_on_partial_state() {
1834 let mut pipeline: Option<PromoteHandles> = None;
1836 let mut exchange = Some(App);
1837 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
1838
1839 let result = crate::replication::take_pipeline_for_promotion(
1840 &mut pipeline,
1841 &mut exchange,
1842 &mut journal_writer,
1843 );
1844
1845 assert!(result.is_err(), "partial state is not a valid promotion");
1846 }
1847 }
1848
1849 #[cfg(not(feature = "no-persist"))]
1855 mod backoff_shutdown {
1856 use super::super::super::auth::authenticate_replica;
1857 use super::scripted::*;
1858 use super::*;
1859 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
1860 use melin_transport_core::replication::catchup::lineage_origin;
1861 use melin_transport_core::replication::protocol::encode_stream_start;
1862 use std::sync::Arc;
1863 use std::sync::atomic::AtomicBool;
1864
1865 fn live_thread_names() -> Vec<String> {
1868 std::fs::read_dir("/proc/self/task")
1869 .expect("read /proc/self/task")
1870 .filter_map(|entry| {
1871 let entry = entry.ok()?;
1874 let name = std::fs::read_to_string(entry.path().join("comm")).ok()?;
1875 Some(name.trim().to_string())
1876 })
1877 .collect()
1878 }
1879
1880 #[test]
1886 fn shutdown_during_reconnect_backoff_joins_pipeline_threads() {
1887 let dir = tempfile::tempdir().expect("tempdir");
1888
1889 let primary_journal = dir.path().join("primary.journal");
1892 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1893 w.append(&JournalEvent::App(EvtAdd(1))).expect("append");
1894 drop(w);
1895 let (lineage_start, lineage_anchor) =
1896 lineage_origin(&primary_journal).expect("lineage");
1897
1898 let authorized_keys = replica_auth();
1899 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1900 listener.set_nonblocking(true).expect("nonblocking");
1901 let addr = listener.local_addr().expect("addr");
1902
1903 let shutdown = Arc::new(AtomicBool::new(false));
1904 let control = crate::replication::ReplicaControlPlane::new();
1905 let replica = {
1906 let journal = dir.path().join("replica.journal");
1907 let snapshot = dir.path().join("replica.snapshot");
1908 let shutdown = Arc::clone(&shutdown);
1909 let control = control.clone();
1910 std::thread::spawn(move || -> Result<bool, String> {
1911 run_receiver::<App>(
1912 addr,
1913 &journal,
1914 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1915 &shutdown,
1916 &control,
1917 3_600_000,
1918 snapshot,
1919 crate::layout::PipelineCores::unpinned(),
1920 melin_journal::StagingMode::ZeroFill,
1921 Duration::ZERO,
1922 64,
1923 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1924 &(),
1925 )
1926 .map(|state| state.is_none())
1928 .map_err(|e| e.to_string())
1929 })
1930 };
1931
1932 let mut s1 = accept_within(&listener, 30);
1936 let mut s1r = s1.try_clone().expect("clone");
1937 authenticate_replica(&mut s1r, &authorized_keys).expect("auth");
1938 match read_replica_msg(&mut s1) {
1939 ReplicaMessage::Handshake(h) => {
1940 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
1941 }
1942 other => panic!("expected Handshake, got {other:?}"),
1943 }
1944 let mut buf = Vec::new();
1945 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
1946 s1.write_all(&buf).expect("StreamStart");
1947 buf.clear();
1948 melin_transport_core::replication_wire::encode_input_batch(
1949 &[InputSlot::<EvtAdd> {
1950 connection_id: 0,
1951 key_hash: 0,
1952 request_seq: 0,
1953 sequence: 1,
1954 timestamp_ns: 1,
1955 event: JournalEvent::App(EvtAdd(1)),
1956 publish_ts: Default::default(),
1957 recv_ts: Default::default(),
1958 }],
1959 &mut buf,
1960 );
1961 s1.write_all(&buf).expect("InputBatch");
1962 wait_for_ack(&mut s1, 1);
1963
1964 drop(s1);
1968 drop(s1r);
1969 drop(listener);
1970 std::thread::sleep(Duration::from_millis(1500));
1975
1976 shutdown.store(true, Ordering::Relaxed);
1977 let deadline = Instant::now() + Duration::from_secs(10);
1978 while !replica.is_finished() {
1979 assert!(
1980 Instant::now() < deadline,
1981 "receiver did not exit after shutdown"
1982 );
1983 std::thread::sleep(Duration::from_millis(20));
1984 }
1985 let result = replica.join().expect("replica thread panicked");
1986 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
1987
1988 let deadline = Instant::now() + Duration::from_secs(10);
1994 loop {
1995 let names = live_thread_names();
1996 if !names
1997 .iter()
1998 .any(|n| n == "journal-seq" || n == "matching" || n == "drain")
1999 {
2000 break;
2001 }
2002 assert!(
2003 Instant::now() < deadline,
2004 "pipeline threads still alive after shutdown: {names:?}"
2005 );
2006 std::thread::sleep(Duration::from_millis(50));
2007 }
2008 }
2009 }
2010
2011 #[cfg(all(feature = "hash-chain", not(feature = "no-persist")))]
2018 mod rotation_adoption {
2019 use super::super::super::auth::authenticate_replica;
2020 use super::scripted::*;
2021 use super::*;
2022 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
2023 use melin_transport_core::replication::catchup::lineage_origin;
2024 use melin_transport_core::replication::protocol::{encode_rotate, encode_stream_start};
2025 use std::sync::Arc;
2026 use std::sync::atomic::AtomicBool;
2027
2028 fn payloads(path: &std::path::Path) -> Vec<u64> {
2030 let mut reader = melin_journal::JournalReader::<EvtAdd>::open(path).expect("open");
2031 let mut out = Vec::new();
2032 while let Some(entry) = reader.next_entry().expect("read entry") {
2033 if let JournalEvent::App(EvtAdd(v)) = entry.event {
2034 out.push(v);
2035 }
2036 }
2037 out
2038 }
2039
2040 fn boundary_adoption_rotates_and_streams_on(mode: melin_journal::StagingMode) {
2051 let _prealloc_guard =
2056 melin_journal::test_utils::PreallocOverrideGuard::new(1024 * 1024);
2057 let dir = tempfile::tempdir().expect("tempdir");
2058
2059 let primary_journal = dir.path().join("primary.journal");
2065 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
2066 for v in 1..=2u64 {
2067 w.batch_append_with_ts(&JournalEvent::App(EvtAdd(v)), v, 0, 0)
2068 .expect("append");
2069 }
2070 w.flush_batch_sync().expect("flush");
2071 let chain_at_2 = w.chain_hash().expect("chain");
2072 drop(w);
2073 let (lineage_start, lineage_anchor) =
2074 lineage_origin(&primary_journal).expect("lineage");
2075
2076 let authorized_keys = replica_auth();
2077 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2078 listener.set_nonblocking(true).expect("nonblocking");
2079 let addr = listener.local_addr().expect("addr");
2080
2081 let replica_journal = dir.path().join("replica.journal");
2082 let shutdown = Arc::new(AtomicBool::new(false));
2083 let control = crate::replication::ReplicaControlPlane::new();
2084 let replica = {
2085 let journal = replica_journal.clone();
2086 let snapshot = dir.path().join("replica.snapshot");
2087 let shutdown = Arc::clone(&shutdown);
2088 let control = control.clone();
2089 std::thread::spawn(move || -> Result<bool, String> {
2090 run_receiver::<App>(
2091 addr,
2092 &journal,
2093 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
2094 &shutdown,
2095 &control,
2096 3_600_000,
2097 snapshot,
2098 crate::layout::PipelineCores::unpinned(),
2099 mode,
2100 Duration::ZERO,
2101 64,
2102 Arc::new(melin_transport_core::fence::FenceState::new(0)),
2103 &(),
2104 )
2105 .map(|state| state.is_none())
2107 .map_err(|e| e.to_string())
2108 })
2109 };
2110
2111 let mut s1 = accept_within(&listener, 30);
2112 let mut s1r = s1.try_clone().expect("clone");
2113 authenticate_replica(&mut s1r, &authorized_keys).expect("auth");
2114 match read_replica_msg(&mut s1) {
2115 ReplicaMessage::Handshake(h) => {
2116 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
2117 }
2118 other => panic!("expected Handshake, got {other:?}"),
2119 }
2120 let mut buf = Vec::new();
2121 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
2122 s1.write_all(&buf).expect("StreamStart");
2123
2124 let send_events = |s: &mut TcpStream, range: std::ops::RangeInclusive<u64>| {
2125 let slots: Vec<InputSlot<EvtAdd>> = range
2126 .map(|v| InputSlot {
2127 connection_id: 0,
2128 key_hash: 0,
2129 request_seq: 0,
2130 sequence: v,
2131 timestamp_ns: v,
2132 event: JournalEvent::App(EvtAdd(v)),
2133 publish_ts: Default::default(),
2135 recv_ts: Default::default(),
2136 })
2137 .collect();
2138 let mut buf = Vec::new();
2139 melin_transport_core::replication_wire::encode_input_batch(&slots, &mut buf);
2140 s.write_all(&buf).expect("InputBatch");
2141 };
2142
2143 send_events(&mut s1, 1..=2);
2144 wait_for_ack(&mut s1, 2);
2145
2146 let staging = {
2153 let mut s = replica_journal.clone().into_os_string();
2157 s.push(".next-staging");
2158 std::path::PathBuf::from(s)
2159 };
2160 let staged_len: u64 = (1 + 8) * 1024 * 1024;
2161 let deadline = Instant::now() + Duration::from_secs(10);
2162 loop {
2163 if std::fs::metadata(&staging).is_ok_and(|m| m.len() == staged_len) {
2164 break;
2165 }
2166 assert!(
2167 Instant::now() < deadline,
2168 "preparer produced no full-size staged segment in {mode:?} mode"
2169 );
2170 std::thread::sleep(Duration::from_millis(10));
2171 }
2172 std::thread::sleep(Duration::from_millis(300));
2180
2181 buf.clear();
2184 encode_rotate(2, &chain_at_2, &mut buf);
2185 s1.write_all(&buf).expect("Rotate");
2186
2187 send_events(&mut s1, 3..=4);
2191 wait_for_ack(&mut s1, 4);
2192
2193 shutdown.store(true, Ordering::Relaxed);
2194 let result = replica.join().expect("replica thread panicked");
2195 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
2196
2197 let archived = dir.path().join("replica.journal.000001");
2201 assert!(archived.exists(), "sealed segment must be archived");
2202 assert_eq!(payloads(&archived), vec![1, 2]);
2203 assert_eq!(payloads(&replica_journal), vec![3, 4]);
2204 }
2205
2206 #[test]
2207 fn boundary_adoption_zero_fill() {
2208 boundary_adoption_rotates_and_streams_on(melin_journal::StagingMode::ZeroFill);
2209 }
2210
2211 #[test]
2212 fn boundary_adoption_allocate() {
2213 boundary_adoption_rotates_and_streams_on(melin_journal::StagingMode::Allocate);
2214 }
2215 }
2216}