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 app, 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 app, &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 app,
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 app = Some(A::default());
831 journal_writer = Some(writer);
832 }
833
834 if pipeline.is_none() {
836 let cur_app = app.take().expect("application initialized");
837 let cur_writer = journal_writer.take().expect("journal_writer initialized");
838 pipeline = Some(build_replica_pipeline_with_threads::<A>(
839 cur_app,
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 app: ex,
920 journal_writer: wr,
921 last_sequence: seq,
922 chain_hash: hash,
923 } => {
924 app = 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, QueryCtx, 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>) {}
1178 fn query(&self, _e: EvtAdd, _ctx: &QueryCtx) -> Option<Rpt> {
1179 None
1180 }
1181 fn tick(&mut self, _now_ns: u64, _out: &mut Vec<Rpt>) {}
1182 fn build_reject(_e: &EvtAdd, _r: RejectReason) -> Rpt {
1183 Rpt
1184 }
1185 fn snapshot<W: std::io::Write>(&self, _w: &mut W) -> std::io::Result<()> {
1186 Ok(())
1187 }
1188 fn restore<R: std::io::Read>(_r: &mut R) -> std::io::Result<Self> {
1189 Ok(App)
1190 }
1191 }
1192
1193 pub(super) const REPLICA_KEY: [u8; 32] = [0xFC; 32];
1196
1197 pub(super) fn replica_auth() -> melin_app::auth::AuthorizedKeys {
1200 let key = ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY);
1201 let pub_b64 = base64::Engine::encode(
1202 &base64::engine::general_purpose::STANDARD,
1203 key.verifying_key().to_bytes(),
1204 );
1205 melin_app::auth::AuthorizedKeys::parse(&format!("replication {pub_b64} test-replica\n"))
1206 .expect("parse keys")
1207 }
1208
1209 pub(super) fn read_replica_msg(stream: &mut TcpStream) -> ReplicaMessage {
1211 let mut len_buf = [0u8; 4];
1212 stream.read_exact(&mut len_buf).expect("frame length");
1213 let len = u32::from_le_bytes(len_buf) as usize;
1214 let mut payload = vec![0u8; len];
1215 stream.read_exact(&mut payload).expect("frame payload");
1216 decode_replica_message(&payload).expect("decodable replica frame")
1217 }
1218
1219 pub(super) fn wait_for_ack(stream: &mut TcpStream, seq: u64) {
1221 loop {
1222 if let ReplicaMessage::Ack(a) = read_replica_msg(stream)
1223 && a.acked_sequence >= seq
1224 {
1225 return;
1226 }
1227 }
1228 }
1229
1230 pub(super) fn accept_within(listener: &std::net::TcpListener, secs: u64) -> TcpStream {
1233 let deadline = Instant::now() + Duration::from_secs(secs);
1234 loop {
1235 match listener.accept() {
1236 Ok((s, _)) => {
1237 s.set_read_timeout(Some(Duration::from_secs(10)))
1238 .expect("read timeout");
1239 return s;
1240 }
1241 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
1242 assert!(Instant::now() < deadline, "replica never connected");
1243 std::thread::sleep(Duration::from_millis(10));
1244 }
1245 Err(e) => panic!("accept failed: {e}"),
1246 }
1247 }
1248 }
1249 }
1250
1251 #[cfg(all(feature = "hash-chain", not(feature = "no-persist")))]
1257 mod divergence_resync {
1258 use super::super::super::auth::authenticate_replica;
1259 use super::scripted::*;
1260 use super::*;
1261 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
1262 use melin_transport_core::cursors::WireSeq;
1263 use melin_transport_core::replication::catchup::{lineage_origin, snapshot_transfer_with};
1264 use melin_transport_core::replication::protocol::{
1265 encode_hash_mismatch, encode_rotate, encode_stream_start,
1266 };
1267 use std::sync::Arc;
1268 use std::sync::atomic::AtomicBool;
1269
1270 #[test]
1279 fn mid_stream_divergence_resyncs_in_process() {
1280 let dir = tempfile::tempdir().expect("tempdir");
1281
1282 let primary_journal = dir.path().join("primary.journal");
1286 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1287 let mut chain_at_4 = [0u8; 32];
1288 for v in 1..=5u64 {
1289 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1290 if v == 2 {
1291 w.rotate_segment().expect("rotate");
1292 }
1293 if v == 4 {
1294 chain_at_4 = w.chain_hash().expect("chain");
1295 }
1296 }
1297 drop(w);
1298 melin_transport_core::snapshot::save::<App>(
1299 &App,
1300 WireSeq::new(4),
1301 chain_at_4,
1302 0,
1303 &primary_journal.with_extension("snapshot"),
1304 )
1305 .expect("save snapshot");
1306 let (lineage_start, lineage_anchor) =
1307 lineage_origin(&primary_journal).expect("lineage");
1308
1309 let authorized_keys = replica_auth();
1311
1312 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1313 listener.set_nonblocking(true).expect("nonblocking");
1314 let addr = listener.local_addr().expect("addr");
1315
1316 let replica_journal = dir.path().join("replica.journal");
1318 let replica_snapshot = dir.path().join("replica.snapshot");
1319 let shutdown = Arc::new(AtomicBool::new(false));
1320 let control = crate::replication::ReplicaControlPlane::new();
1321 let cores = crate::layout::PipelineCores::unpinned();
1322 let replica = {
1323 let journal = replica_journal.clone();
1324 let shutdown = Arc::clone(&shutdown);
1325 let control = control.clone();
1326 std::thread::spawn(move || -> Result<bool, String> {
1327 run_receiver::<App>(
1328 addr,
1329 &journal,
1330 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1331 &shutdown,
1332 &control,
1333 3_600_000,
1334 replica_snapshot,
1335 cores,
1336 melin_journal::StagingMode::ZeroFill,
1337 Duration::ZERO,
1338 64,
1339 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1340 &(),
1341 )
1342 .map(|state| state.is_none())
1344 .map_err(|e| e.to_string())
1345 })
1346 };
1347
1348 let mut s1 = accept_within(&listener, 30);
1351 let mut s1r = s1.try_clone().expect("clone");
1352 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1353 match read_replica_msg(&mut s1) {
1354 ReplicaMessage::Handshake(h) => {
1355 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
1356 }
1357 other => panic!("expected Handshake, got {other:?}"),
1358 }
1359 let mut buf = Vec::new();
1360 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
1361 s1.write_all(&buf).expect("StreamStart 1");
1362 buf.clear();
1363 melin_transport_core::replication_wire::encode_input_batch(
1364 &[InputSlot::<EvtAdd> {
1365 connection_id: 0,
1366 key_hash: 0,
1367 sequence: 1,
1368 timestamp_ns: 1,
1369 event: JournalEvent::App(EvtAdd(1)),
1370 publish_ts: Default::default(),
1372 recv_ts: Default::default(),
1373 }],
1374 &mut buf,
1375 );
1376 s1.write_all(&buf).expect("InputBatch");
1377 wait_for_ack(&mut s1, 1);
1378
1379 buf.clear();
1382 encode_rotate(1, &[0xEE; 32], &mut buf);
1383 s1.write_all(&buf).expect("Rotate");
1384
1385 let mut s2 = accept_within(&listener, 30);
1388 let mut s2r = s2.try_clone().expect("clone");
1389 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1390 match read_replica_msg(&mut s2) {
1391 ReplicaMessage::Handshake(h) => assert_eq!(
1392 h.last_sequence, 1,
1393 "post-divergence handshake must carry the recovered journal position"
1394 ),
1395 other => panic!("expected Handshake, got {other:?}"),
1396 }
1397 assert!(
1403 !control.pipeline_healthy.load(Ordering::Acquire),
1404 "the divergence must have latched the health mirror false"
1405 );
1406 buf.clear();
1407 encode_hash_mismatch(&mut buf);
1408 s2.write_all(&buf).expect("HashMismatch");
1409 let transfer_shutdown = AtomicBool::new(false);
1410 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1411 s2.write_all(b)?;
1412 s2.flush()
1413 };
1414 let end = snapshot_transfer_with::<EvtAdd>(
1417 &primary_journal,
1418 &mut publish,
1419 &transfer_shutdown,
1420 1,
1421 )
1422 .expect("snapshot transfer");
1423 assert_eq!(
1424 end,
1425 melin_transport_core::replication::catchup::CatchUpResult::Ok(5)
1426 );
1427 let mut s2_acks = s2.try_clone().expect("clone");
1428 wait_for_ack(&mut s2_acks, 5);
1429
1430 assert!(
1433 control.pipeline_healthy.load(Ordering::Acquire),
1434 "the resync rebuild must reset the health mirror true"
1435 );
1436
1437 shutdown.store(true, Ordering::Relaxed);
1440 let result = replica.join().expect("replica thread panicked");
1441 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
1442
1443 assert!(
1446 dir.path().join("replica.journal.divergent.0").exists(),
1447 "divergent lineage must be archived"
1448 );
1449 assert!(replica_journal.exists(), "re-seeded live journal");
1450 }
1451
1452 #[test]
1466 fn resync_transfer_failure_rebuilds_clean_not_over_stale_writer() {
1467 let dir = tempfile::tempdir().expect("tempdir");
1468
1469 let primary_journal = dir.path().join("primary.journal");
1473 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1474 for v in 1..=2u64 {
1475 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1476 }
1477 drop(w);
1478 let (lineage_start, lineage_anchor) =
1479 lineage_origin(&primary_journal).expect("lineage");
1480
1481 let authorized_keys = replica_auth();
1482
1483 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1484 listener.set_nonblocking(true).expect("nonblocking");
1485 let addr = listener.local_addr().expect("addr");
1486
1487 let replica_journal = dir.path().join("replica.journal");
1488 let replica_snapshot = dir.path().join("replica.snapshot");
1489 let shutdown = Arc::new(AtomicBool::new(false));
1490 let control = crate::replication::ReplicaControlPlane::new();
1491 let cores = crate::layout::PipelineCores::unpinned();
1492 let replica = {
1493 let journal = replica_journal.clone();
1494 let shutdown = Arc::clone(&shutdown);
1495 let control = control.clone();
1496 std::thread::spawn(move || -> Result<bool, String> {
1497 run_receiver::<App>(
1498 addr,
1499 &journal,
1500 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1501 &shutdown,
1502 &control,
1503 3_600_000,
1504 replica_snapshot,
1505 cores,
1506 melin_journal::StagingMode::ZeroFill,
1507 Duration::ZERO,
1508 64,
1509 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1510 &(),
1511 )
1512 .map(|state| state.is_none())
1513 .map_err(|e| e.to_string())
1514 })
1515 };
1516
1517 let stream_one = |s: &mut TcpStream, buf: &mut Vec<u8>| {
1520 buf.clear();
1521 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, buf);
1522 s.write_all(buf).expect("StreamStart");
1523 buf.clear();
1524 melin_transport_core::replication_wire::encode_input_batch(
1525 &[InputSlot::<EvtAdd> {
1526 connection_id: 0,
1527 key_hash: 0,
1528 sequence: 1,
1529 timestamp_ns: 1,
1530 event: JournalEvent::App(EvtAdd(1)),
1531 publish_ts: Default::default(),
1532 recv_ts: Default::default(),
1533 }],
1534 buf,
1535 );
1536 s.write_all(buf).expect("InputBatch");
1537 };
1538
1539 let mut buf = Vec::new();
1544 let mut s1 = accept_within(&listener, 30);
1545 let mut s1r = s1.try_clone().expect("clone");
1546 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1547 match read_replica_msg(&mut s1) {
1548 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0, "fresh"),
1549 other => panic!("expected Handshake, got {other:?}"),
1550 }
1551 stream_one(&mut s1, &mut buf);
1552 wait_for_ack(&mut s1, 1);
1553 buf.clear();
1554 encode_rotate(1, &[0xEE; 32], &mut buf);
1555 s1.write_all(&buf).expect("Rotate");
1556
1557 let mut s2 = accept_within(&listener, 30);
1562 let mut s2r = s2.try_clone().expect("clone");
1563 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1564 match read_replica_msg(&mut s2) {
1565 ReplicaMessage::Handshake(h) => assert_eq!(
1566 h.last_sequence, 1,
1567 "reconnect carries the recovered forked position"
1568 ),
1569 other => panic!("expected Handshake, got {other:?}"),
1570 }
1571 buf.clear();
1572 encode_hash_mismatch(&mut buf);
1573 s2.write_all(&buf).expect("HashMismatch");
1574 s2.flush().expect("flush");
1575 drop(s2);
1578 drop(s2r);
1579
1580 let mut s3 = accept_within(&listener, 30);
1585 let mut s3r = s3.try_clone().expect("clone");
1586 authenticate_replica(&mut s3r, &authorized_keys).expect("auth 3");
1587 match read_replica_msg(&mut s3) {
1588 ReplicaMessage::Handshake(h) => assert_eq!(
1589 h.last_sequence, 0,
1590 "after a failed transfer the retry handshakes as fresh"
1591 ),
1592 other => panic!("expected Handshake, got {other:?}"),
1593 }
1594 stream_one(&mut s3, &mut buf);
1595 wait_for_ack(&mut s3, 1);
1596
1597 shutdown.store(true, Ordering::Relaxed);
1598 let result = replica.join().expect("replica thread panicked");
1599 assert_eq!(result, Ok(true), "receiver exits cleanly via shutdown");
1600
1601 assert!(
1606 dir.path().join("replica.journal.divergent.0").exists(),
1607 "forked lineage archived, never deleted"
1608 );
1609 assert!(
1610 replica_journal.exists(),
1611 "fresh live journal recreated after the failed transfer"
1612 );
1613 }
1614
1615 #[test]
1622 fn second_mid_stream_divergence_exits_hard() {
1623 let dir = tempfile::tempdir().expect("tempdir");
1624
1625 let primary_journal = dir.path().join("primary.journal");
1626 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1627 let mut chain_at_4 = [0u8; 32];
1628 for v in 1..=5u64 {
1629 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1630 if v == 2 {
1631 w.rotate_segment().expect("rotate");
1632 }
1633 if v == 4 {
1634 chain_at_4 = w.chain_hash().expect("chain");
1635 }
1636 }
1637 drop(w);
1638 melin_transport_core::snapshot::save::<App>(
1639 &App,
1640 WireSeq::new(4),
1641 chain_at_4,
1642 0,
1643 &primary_journal.with_extension("snapshot"),
1644 )
1645 .expect("save snapshot");
1646 let (lineage_start, lineage_anchor) =
1647 lineage_origin(&primary_journal).expect("lineage");
1648
1649 let authorized_keys = replica_auth();
1650
1651 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1652 listener.set_nonblocking(true).expect("nonblocking");
1653 let addr = listener.local_addr().expect("addr");
1654
1655 let replica_journal = dir.path().join("replica.journal");
1656 let replica_snapshot = dir.path().join("replica.snapshot");
1657 let shutdown = Arc::new(AtomicBool::new(false));
1658 let control = crate::replication::ReplicaControlPlane::new();
1659 let cores = crate::layout::PipelineCores::unpinned();
1660 let replica = {
1661 let journal = replica_journal.clone();
1662 let shutdown = Arc::clone(&shutdown);
1663 let control = control.clone();
1664 std::thread::spawn(move || -> Result<bool, String> {
1665 run_receiver::<App>(
1666 addr,
1667 &journal,
1668 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1669 &shutdown,
1670 &control,
1671 3_600_000,
1672 replica_snapshot,
1673 cores,
1674 melin_journal::StagingMode::ZeroFill,
1675 Duration::ZERO,
1676 64,
1677 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1678 &(),
1679 )
1680 .map(|state| state.is_none())
1681 .map_err(|e| e.to_string())
1682 })
1683 };
1684
1685 let mut s1 = accept_within(&listener, 30);
1687 let mut s1r = s1.try_clone().expect("clone");
1688 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1689 match read_replica_msg(&mut s1) {
1690 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0),
1691 other => panic!("expected Handshake, got {other:?}"),
1692 }
1693 let mut buf = Vec::new();
1694 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
1695 s1.write_all(&buf).expect("StreamStart 1");
1696 buf.clear();
1697 melin_transport_core::replication_wire::encode_input_batch(
1698 &[InputSlot::<EvtAdd> {
1699 connection_id: 0,
1700 key_hash: 0,
1701 sequence: 1,
1702 timestamp_ns: 1,
1703 event: JournalEvent::App(EvtAdd(1)),
1704 publish_ts: Default::default(),
1705 recv_ts: Default::default(),
1706 }],
1707 &mut buf,
1708 );
1709 s1.write_all(&buf).expect("InputBatch");
1710 wait_for_ack(&mut s1, 1);
1711 buf.clear();
1712 encode_rotate(1, &[0xEE; 32], &mut buf);
1713 s1.write_all(&buf).expect("Rotate 1");
1714
1715 let mut s2 = accept_within(&listener, 30);
1718 let mut s2r = s2.try_clone().expect("clone");
1719 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1720 match read_replica_msg(&mut s2) {
1721 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 1),
1722 other => panic!("expected Handshake, got {other:?}"),
1723 }
1724 buf.clear();
1725 encode_hash_mismatch(&mut buf);
1726 s2.write_all(&buf).expect("HashMismatch");
1727 let transfer_shutdown = AtomicBool::new(false);
1728 {
1729 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1730 s2.write_all(b)?;
1731 s2.flush()
1732 };
1733 snapshot_transfer_with::<EvtAdd>(
1734 &primary_journal,
1735 &mut publish,
1736 &transfer_shutdown,
1737 1,
1738 )
1739 .expect("snapshot transfer");
1740 }
1741 buf.clear();
1742 melin_transport_core::replication_wire::encode_input_batch(
1743 &[InputSlot::<EvtAdd> {
1744 connection_id: 0,
1745 key_hash: 0,
1746 sequence: 6,
1747 timestamp_ns: 6,
1748 event: JournalEvent::App(EvtAdd(6)),
1749 publish_ts: Default::default(),
1750 recv_ts: Default::default(),
1751 }],
1752 &mut buf,
1753 );
1754 s2.write_all(&buf).expect("InputBatch 6");
1755 let mut s2_acks = s2.try_clone().expect("clone");
1756 wait_for_ack(&mut s2_acks, 6);
1757 buf.clear();
1758 encode_rotate(6, &[0xEE; 32], &mut buf);
1759 s2.write_all(&buf).expect("Rotate 2");
1760
1761 let result = replica.join().expect("replica thread panicked");
1764 let err = result.expect_err("second divergence must exit hard");
1765 assert!(
1766 err.contains("divergence recurred"),
1767 "error must name the recurrence: {err}"
1768 );
1769 }
1770
1771 type PromoteHandles =
1780 crate::replication::ReplicaPipelineHandles<App, BufferedWriter<EvtAdd>>;
1781
1782 #[test]
1783 fn promotion_hands_back_local_state_when_present() {
1784 let dir = tempfile::tempdir().unwrap();
1785 let mut pipeline: Option<PromoteHandles> = None;
1786 let mut app = Some(App);
1787 let mut journal_writer =
1788 Some(BufferedWriter::<EvtAdd>::create(&dir.path().join("p.journal")).unwrap());
1789
1790 let result = crate::replication::take_pipeline_for_promotion(
1791 &mut pipeline,
1792 &mut app,
1793 &mut journal_writer,
1794 );
1795
1796 assert!(matches!(result, Ok(Some(_))), "warm state must be promoted");
1797 assert!(
1798 app.is_none() && journal_writer.is_none(),
1799 "state must be moved into the result"
1800 );
1801 }
1802
1803 #[test]
1804 fn promotion_errs_when_no_local_state() {
1805 let mut pipeline: Option<PromoteHandles> = None;
1806 let mut app: Option<App> = None;
1807 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
1808
1809 let result = crate::replication::take_pipeline_for_promotion(
1810 &mut pipeline,
1811 &mut app,
1812 &mut journal_writer,
1813 );
1814
1815 let err = match result {
1817 Err(e) => e,
1818 Ok(_) => panic!("a promote with nothing to promote must error"),
1819 };
1820 assert!(
1821 err.to_string().contains("no local state available"),
1822 "{err}"
1823 );
1824 }
1825
1826 #[test]
1827 fn promotion_errs_on_partial_state() {
1828 let mut pipeline: Option<PromoteHandles> = None;
1830 let mut app = Some(App);
1831 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
1832
1833 let result = crate::replication::take_pipeline_for_promotion(
1834 &mut pipeline,
1835 &mut app,
1836 &mut journal_writer,
1837 );
1838
1839 assert!(result.is_err(), "partial state is not a valid promotion");
1840 }
1841 }
1842
1843 #[cfg(not(feature = "no-persist"))]
1849 mod backoff_shutdown {
1850 use super::super::super::auth::authenticate_replica;
1851 use super::scripted::*;
1852 use super::*;
1853 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
1854 use melin_transport_core::replication::catchup::lineage_origin;
1855 use melin_transport_core::replication::protocol::encode_stream_start;
1856 use std::sync::Arc;
1857 use std::sync::atomic::AtomicBool;
1858
1859 fn live_thread_names() -> Vec<String> {
1862 std::fs::read_dir("/proc/self/task")
1863 .expect("read /proc/self/task")
1864 .filter_map(|entry| {
1865 let entry = entry.ok()?;
1868 let name = std::fs::read_to_string(entry.path().join("comm")).ok()?;
1869 Some(name.trim().to_string())
1870 })
1871 .collect()
1872 }
1873
1874 #[test]
1880 fn shutdown_during_reconnect_backoff_joins_pipeline_threads() {
1881 let dir = tempfile::tempdir().expect("tempdir");
1882
1883 let primary_journal = dir.path().join("primary.journal");
1886 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1887 w.append(&JournalEvent::App(EvtAdd(1))).expect("append");
1888 drop(w);
1889 let (lineage_start, lineage_anchor) =
1890 lineage_origin(&primary_journal).expect("lineage");
1891
1892 let authorized_keys = replica_auth();
1893 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1894 listener.set_nonblocking(true).expect("nonblocking");
1895 let addr = listener.local_addr().expect("addr");
1896
1897 let shutdown = Arc::new(AtomicBool::new(false));
1898 let control = crate::replication::ReplicaControlPlane::new();
1899 let replica = {
1900 let journal = dir.path().join("replica.journal");
1901 let snapshot = dir.path().join("replica.snapshot");
1902 let shutdown = Arc::clone(&shutdown);
1903 let control = control.clone();
1904 std::thread::spawn(move || -> Result<bool, String> {
1905 run_receiver::<App>(
1906 addr,
1907 &journal,
1908 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1909 &shutdown,
1910 &control,
1911 3_600_000,
1912 snapshot,
1913 crate::layout::PipelineCores::unpinned(),
1914 melin_journal::StagingMode::ZeroFill,
1915 Duration::ZERO,
1916 64,
1917 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1918 &(),
1919 )
1920 .map(|state| state.is_none())
1922 .map_err(|e| e.to_string())
1923 })
1924 };
1925
1926 let mut s1 = accept_within(&listener, 30);
1930 let mut s1r = s1.try_clone().expect("clone");
1931 authenticate_replica(&mut s1r, &authorized_keys).expect("auth");
1932 match read_replica_msg(&mut s1) {
1933 ReplicaMessage::Handshake(h) => {
1934 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
1935 }
1936 other => panic!("expected Handshake, got {other:?}"),
1937 }
1938 let mut buf = Vec::new();
1939 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
1940 s1.write_all(&buf).expect("StreamStart");
1941 buf.clear();
1942 melin_transport_core::replication_wire::encode_input_batch(
1943 &[InputSlot::<EvtAdd> {
1944 connection_id: 0,
1945 key_hash: 0,
1946 sequence: 1,
1947 timestamp_ns: 1,
1948 event: JournalEvent::App(EvtAdd(1)),
1949 publish_ts: Default::default(),
1950 recv_ts: Default::default(),
1951 }],
1952 &mut buf,
1953 );
1954 s1.write_all(&buf).expect("InputBatch");
1955 wait_for_ack(&mut s1, 1);
1956
1957 drop(s1);
1961 drop(s1r);
1962 drop(listener);
1963 std::thread::sleep(Duration::from_millis(1500));
1968
1969 shutdown.store(true, Ordering::Relaxed);
1970 let deadline = Instant::now() + Duration::from_secs(10);
1971 while !replica.is_finished() {
1972 assert!(
1973 Instant::now() < deadline,
1974 "receiver did not exit after shutdown"
1975 );
1976 std::thread::sleep(Duration::from_millis(20));
1977 }
1978 let result = replica.join().expect("replica thread panicked");
1979 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
1980
1981 let deadline = Instant::now() + Duration::from_secs(10);
1987 loop {
1988 let names = live_thread_names();
1989 if !names
1990 .iter()
1991 .any(|n| n == "journal-seq" || n == "matching" || n == "drain")
1992 {
1993 break;
1994 }
1995 assert!(
1996 Instant::now() < deadline,
1997 "pipeline threads still alive after shutdown: {names:?}"
1998 );
1999 std::thread::sleep(Duration::from_millis(50));
2000 }
2001 }
2002 }
2003
2004 #[cfg(all(feature = "hash-chain", not(feature = "no-persist")))]
2011 mod rotation_adoption {
2012 use super::super::super::auth::authenticate_replica;
2013 use super::scripted::*;
2014 use super::*;
2015 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
2016 use melin_transport_core::replication::catchup::lineage_origin;
2017 use melin_transport_core::replication::protocol::{encode_rotate, encode_stream_start};
2018 use std::sync::Arc;
2019 use std::sync::atomic::AtomicBool;
2020
2021 fn payloads(path: &std::path::Path) -> Vec<u64> {
2023 let mut reader = melin_journal::JournalReader::<EvtAdd>::open(path).expect("open");
2024 let mut out = Vec::new();
2025 while let Some(entry) = reader.next_entry().expect("read entry") {
2026 if let JournalEvent::App(EvtAdd(v)) = entry.event {
2027 out.push(v);
2028 }
2029 }
2030 out
2031 }
2032
2033 fn boundary_adoption_rotates_and_streams_on(mode: melin_journal::StagingMode) {
2044 let _prealloc_guard =
2049 melin_journal::test_utils::PreallocOverrideGuard::new(1024 * 1024);
2050 let dir = tempfile::tempdir().expect("tempdir");
2051
2052 let primary_journal = dir.path().join("primary.journal");
2058 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
2059 for v in 1..=2u64 {
2060 w.batch_append_with_ts(&JournalEvent::App(EvtAdd(v)), v, 0)
2061 .expect("append");
2062 }
2063 w.flush_batch_sync().expect("flush");
2064 let chain_at_2 = w.chain_hash().expect("chain");
2065 drop(w);
2066 let (lineage_start, lineage_anchor) =
2067 lineage_origin(&primary_journal).expect("lineage");
2068
2069 let authorized_keys = replica_auth();
2070 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2071 listener.set_nonblocking(true).expect("nonblocking");
2072 let addr = listener.local_addr().expect("addr");
2073
2074 let replica_journal = dir.path().join("replica.journal");
2075 let shutdown = Arc::new(AtomicBool::new(false));
2076 let control = crate::replication::ReplicaControlPlane::new();
2077 let replica = {
2078 let journal = replica_journal.clone();
2079 let snapshot = dir.path().join("replica.snapshot");
2080 let shutdown = Arc::clone(&shutdown);
2081 let control = control.clone();
2082 std::thread::spawn(move || -> Result<bool, String> {
2083 run_receiver::<App>(
2084 addr,
2085 &journal,
2086 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
2087 &shutdown,
2088 &control,
2089 3_600_000,
2090 snapshot,
2091 crate::layout::PipelineCores::unpinned(),
2092 mode,
2093 Duration::ZERO,
2094 64,
2095 Arc::new(melin_transport_core::fence::FenceState::new(0)),
2096 &(),
2097 )
2098 .map(|state| state.is_none())
2100 .map_err(|e| e.to_string())
2101 })
2102 };
2103
2104 let mut s1 = accept_within(&listener, 30);
2105 let mut s1r = s1.try_clone().expect("clone");
2106 authenticate_replica(&mut s1r, &authorized_keys).expect("auth");
2107 match read_replica_msg(&mut s1) {
2108 ReplicaMessage::Handshake(h) => {
2109 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
2110 }
2111 other => panic!("expected Handshake, got {other:?}"),
2112 }
2113 let mut buf = Vec::new();
2114 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
2115 s1.write_all(&buf).expect("StreamStart");
2116
2117 let send_events = |s: &mut TcpStream, range: std::ops::RangeInclusive<u64>| {
2118 let slots: Vec<InputSlot<EvtAdd>> = range
2119 .map(|v| InputSlot {
2120 connection_id: 0,
2121 key_hash: 0,
2122 sequence: v,
2123 timestamp_ns: v,
2124 event: JournalEvent::App(EvtAdd(v)),
2125 publish_ts: Default::default(),
2127 recv_ts: Default::default(),
2128 })
2129 .collect();
2130 let mut buf = Vec::new();
2131 melin_transport_core::replication_wire::encode_input_batch(&slots, &mut buf);
2132 s.write_all(&buf).expect("InputBatch");
2133 };
2134
2135 send_events(&mut s1, 1..=2);
2136 wait_for_ack(&mut s1, 2);
2137
2138 let staging = {
2145 let mut s = replica_journal.clone().into_os_string();
2149 s.push(".next-staging");
2150 std::path::PathBuf::from(s)
2151 };
2152 let staged_len: u64 = (1 + 8) * 1024 * 1024;
2153 let deadline = Instant::now() + Duration::from_secs(10);
2154 loop {
2155 if std::fs::metadata(&staging).is_ok_and(|m| m.len() == staged_len) {
2156 break;
2157 }
2158 assert!(
2159 Instant::now() < deadline,
2160 "preparer produced no full-size staged segment in {mode:?} mode"
2161 );
2162 std::thread::sleep(Duration::from_millis(10));
2163 }
2164 std::thread::sleep(Duration::from_millis(300));
2172
2173 buf.clear();
2176 encode_rotate(2, &chain_at_2, &mut buf);
2177 s1.write_all(&buf).expect("Rotate");
2178
2179 send_events(&mut s1, 3..=4);
2183 wait_for_ack(&mut s1, 4);
2184
2185 shutdown.store(true, Ordering::Relaxed);
2186 let result = replica.join().expect("replica thread panicked");
2187 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
2188
2189 let archived = dir.path().join("replica.journal.000001");
2193 assert!(archived.exists(), "sealed segment must be archived");
2194 assert_eq!(payloads(&archived), vec![1, 2]);
2195 assert_eq!(payloads(&replica_journal), vec![3, 4]);
2196 }
2197
2198 #[test]
2199 fn boundary_adoption_zero_fill() {
2200 boundary_adoption_rotates_and_streams_on(melin_journal::StagingMode::ZeroFill);
2201 }
2202
2203 #[test]
2204 fn boundary_adoption_allocate() {
2205 boundary_adoption_rotates_and_streams_on(melin_journal::StagingMode::Allocate);
2206 }
2207 }
2208}