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 factory: std::sync::Arc<dyn melin_app::app_factory::AppFactory<App = A>>,
564 fence_state: std::sync::Arc<melin_transport_core::fence::FenceState>,
565) -> ReceiverResult<A, BufferedWriter<A::Event>>
566where
567 A: Application + Send + 'static,
568 A::Event: Send + Sync + 'static,
569 A::Report: Send + 'static,
570 A::QueryResponse: Send + 'static,
571{
572 let super::ReplicaControlPlane {
573 promote,
574 tip_ready,
575 journal_tip,
576 primary_link_up,
577 primary_ack_policy,
578 pipeline_healthy,
579 } = control;
580 let (mut exchange, mut journal_writer, mut last_sequence, mut chain_hash) =
584 recover_replica_state::<A, BufferedWriter<A::Event>>(
585 journal_path,
586 &snapshot_path,
587 factory.as_ref(),
588 &fence_state,
589 )?;
590 journal_tip.advance(melin_transport_core::WireSeq::new(last_sequence));
596 tip_ready.store(true, Ordering::Release);
597
598 let mut backoff = std::time::Duration::from_secs(1);
599
600 let mut divergence_resyncs: u32 = 0;
603
604 let mut send_buf = Vec::with_capacity(64);
605 let mut pipeline: Option<ReplicaPipelineHandles<A, BufferedWriter<A::Event>>> = None;
606
607 loop {
609 primary_link_up.store(false, Ordering::Release);
612
613 if let Some(p) = pipeline.as_ref() {
614 if let Some(ref lock) = p.chain_hash_lock {
623 let fsync_state = lock.load();
624 last_sequence = fsync_state.journal_seq.get();
625 chain_hash = fsync_state.chain_hash;
626 } else {
627 last_sequence = p.last_seq.load().get();
628 }
629 }
630
631 if shutdown.load(Ordering::Relaxed) {
636 if let Some(p) = pipeline.take() {
637 let _ = teardown_replica_pipeline::<A, BufferedWriter<A::Event>>(p);
638 }
639 return Ok(None);
640 }
641 if promote.is_requested() {
642 info!("promotion triggered while disconnected");
643 return take_pipeline_for_promotion(&mut pipeline, &mut exchange, &mut journal_writer);
644 }
645
646 info!(primary = %primary_addr, "connecting to primary as replica");
648
649 let stream = match TcpStream::connect(primary_addr) {
650 Ok(s) => s,
651 Err(e) => {
652 warn!(
653 error = %e,
654 backoff_secs = backoff.as_secs(),
655 "failed to connect to primary — retrying"
656 );
657 sleep_then_double_backoff(&mut backoff, shutdown, promote);
658 continue;
659 }
660 };
661 if let Err(e) = stream.set_nodelay(true) {
662 warn!(error = %e, "failed to set TCP_NODELAY on replica receive socket");
663 }
664 {
665 use std::os::unix::io::AsRawFd;
666 let busy_poll: libc::c_int = 50;
667 let rc = unsafe {
668 libc::setsockopt(
669 stream.as_raw_fd(),
670 libc::SOL_SOCKET,
671 libc::SO_BUSY_POLL,
672 &busy_poll as *const libc::c_int as *const libc::c_void,
673 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
674 )
675 };
676 if rc != 0 {
677 let err = std::io::Error::last_os_error();
678 warn!(error = %err, "failed to set SO_BUSY_POLL on replica receive socket");
679 }
680 }
681 stream.set_read_timeout(Some(std::time::Duration::from_secs(5)))?;
682
683 let mut reader = stream.try_clone()?;
684 let mut tcp_writer = stream;
685
686 if let Err(e) = authenticate_with_primary(&mut reader, signing_key) {
689 if matches!(
696 e.kind(),
697 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
698 ) {
699 warn!(error = %e, "no auth challenge from primary (not serving yet?) — retrying");
700 } else {
701 warn!(error = %e, "authentication failed — retrying");
702 }
703 drop(reader);
708 drop(tcp_writer);
709 sleep_then_double_backoff(&mut backoff, shutdown, promote);
712 continue;
713 }
714 info!("authenticated with primary");
715 primary_link_up.store(true, Ordering::Release);
718
719 let handshake = Handshake {
724 last_sequence,
725 chain_hash,
726 epoch: fence_state.epoch(),
727 };
728 send_buf.clear();
729 encode_handshake(&handshake, &mut send_buf);
730 tcp_writer.write_all(&send_buf)?;
731 tcp_writer.flush()?;
732 send_buf.clear();
733
734 let response_frame = read_frame(&mut reader, MAX_CONTROL_FRAME)?;
741 let response = decode_primary_message(&response_frame)?;
742 let (stream_lineage, session_start) = match response {
743 PrimaryMessage::StreamStart {
744 start_sequence,
745 segment_start_sequence,
746 anchor_hash,
747 epoch,
748 ack_policy,
749 } => {
750 let our_epoch = fence_state.epoch();
758 if fence_state.refuses_primary(epoch) {
759 warn!(
760 primary_epoch = epoch,
761 our_epoch,
762 "primary is behind our fencing epoch — refusing to follow stale primary"
763 );
764 drop(reader);
767 drop(tcp_writer);
768 sleep_then_double_backoff(&mut backoff, shutdown, promote);
769 continue;
770 }
771 fence_state.observe_epoch(epoch);
775 primary_ack_policy.store(ack_policy, Ordering::Release);
776 info!(start_sequence, epoch, ack_policy, "streaming started");
777 ((segment_start_sequence, anchor_hash), last_sequence)
778 }
779 ref resync @ (PrimaryMessage::NeedSnapshot | PrimaryMessage::HashMismatch) => {
780 let divergent = matches!(resync, PrimaryMessage::HashMismatch);
781 let decision = handle_resync_verdict(
785 divergent,
786 &mut TcpFrameSource {
787 reader: &mut reader,
788 },
789 &mut pipeline,
790 &mut exchange,
791 &mut journal_writer,
792 journal_path,
793 &snapshot_path,
794 &fence_state,
795 control,
796 &mut last_sequence,
797 &mut chain_hash,
798 )
799 .map_err(|e| -> Box<dyn std::error::Error> { e })?;
800 match decision {
801 ResyncDecision::Ready {
802 segment_start_sequence,
803 anchor_hash,
804 resume_sequence,
805 } => ((segment_start_sequence, anchor_hash), resume_sequence),
806 ResyncDecision::Retry => {
807 sleep_then_double_backoff(&mut backoff, shutdown, promote);
808 continue;
809 }
810 }
811 }
812 _ => {
813 return Err(format!("unexpected response: {response:?}").into());
814 }
815 };
816
817 if pipeline.is_none() && journal_writer.is_none() {
825 let (lineage_start, lineage_anchor) = stream_lineage;
826 let writer =
827 BufferedWriter::create_continuing(journal_path, lineage_start, lineage_anchor)?;
828 let mut fresh = factory.empty();
829 factory.apply_operator_policy(&mut fresh);
830 exchange = Some(fresh);
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 )?);
849 }
850
851 let result = {
853 let p = pipeline.as_mut().expect("pipeline must exist by here");
854 let input_producer = &mut p.input_producer;
855 let journal_cursor = p.journal_cursor.as_ref();
856 let stream_marks = &p.stream_marks;
857 let journal_failed = &p.journal_failed;
858 std::thread::scope(|s| {
859 let handle = std::thread::Builder::new()
860 .name("replica-receiver".into())
861 .spawn_scoped(s, || {
862 melin_app::affinity::pin_thread("replica-receiver", cores.reader.core);
863 let mut transport = match UringTransport::new(&tcp_writer) {
864 Ok(t) => t,
865 Err(e) => {
866 tracing::error!(error = %e, "UringTransport init failed");
867 return super::receiver_transport::StreamingResult {
873 exit: SessionExit::Disconnected,
874 heard_from_primary: false,
875 };
876 }
877 };
878 streaming_loop::<UringTransport, A::Event>(
879 &mut transport,
880 input_producer,
881 journal_cursor,
882 shutdown,
883 control,
884 pipeline_depth,
885 cores.reader.wait,
886 session_start,
887 Vec::with_capacity(MAX_DATA_FRAME + 4),
888 None,
889 stream_marks,
890 journal_failed,
891 )
892 })
893 .expect("spawn replica-receiver thread");
894 handle.join().expect("replica-receiver thread panicked")
895 })
896 };
897
898 primary_link_up.store(false, Ordering::Release);
899 match handle_session_exit(
900 result,
901 &mut pipeline,
902 &mut divergence_resyncs,
903 &mut backoff,
904 last_sequence,
905 journal_path,
906 &snapshot_path,
907 factory.as_ref(),
908 &fence_state,
909 shutdown,
910 promote,
911 || {},
915 ) {
916 AfterSession::Return(r) => return r,
917 AfterSession::Resync {
918 exchange: ex,
919 journal_writer: wr,
920 last_sequence: seq,
921 chain_hash: hash,
922 } => {
923 exchange = ex;
924 journal_writer = wr;
925 last_sequence = seq;
926 chain_hash = hash;
927 continue;
928 }
929 AfterSession::Reconnect => {}
930 }
931 }
932}
933
934#[cfg(test)]
939mod tests {
940 use super::*;
941 use crate::uring_teardown::DRAIN_TIMEOUT;
942 use std::io::Read;
943 use std::net::TcpListener;
944 use std::time::{Duration, Instant};
945
946 use melin_transport_core::replication::protocol::{ReplicaMessage, decode_replica_message};
947
948 fn socket_pair() -> (TcpStream, TcpStream) {
950 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
951 let addr = listener.local_addr().unwrap();
952 let client = TcpStream::connect(addr).unwrap();
953 let (server, _) = listener.accept().unwrap();
954 (client, server)
955 }
956
957 fn ack(seq: u64) -> Ack {
958 Ack {
959 acked_sequence: seq,
960 in_memory_sequence: seq,
961 }
962 }
963
964 fn flush_acks(transport: &mut UringTransport) {
966 let mut recv_buf = Vec::new();
967 let deadline = Instant::now() + Duration::from_secs(5);
968 while transport.ack_in_flight() {
969 transport.poll_recv(&mut recv_buf).unwrap();
970 assert!(Instant::now() < deadline, "ack send never completed");
971 std::hint::spin_loop();
972 }
973 }
974
975 fn read_acks(peer: &mut TcpStream, expect: usize) -> Vec<Ack> {
977 peer.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
978 let mut buf = Vec::new();
979 let mut chunk = [0u8; 256];
980 let mut acks = Vec::new();
981 while acks.len() < expect {
982 let n = peer.read(&mut chunk).expect("peer read");
983 assert!(n > 0, "peer closed before all acks arrived");
984 buf.extend_from_slice(&chunk[..n]);
985 loop {
987 if buf.len() < 4 {
988 break;
989 }
990 let len = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
991 if buf.len() < 4 + len {
992 break;
993 }
994 match decode_replica_message(&buf[4..4 + len]).expect("decodable frame") {
995 ReplicaMessage::Ack(a) => acks.push(a),
996 other => panic!("expected Ack frame, got {other:?}"),
997 }
998 buf.drain(..4 + len);
999 }
1000 }
1001 assert!(buf.is_empty(), "trailing bytes after expected acks");
1002 acks
1003 }
1004
1005 #[test]
1006 fn coalesces_to_latest_ack_while_send_in_flight() {
1007 let (client, mut peer) = socket_pair();
1008 let mut transport = UringTransport::new(&client).unwrap();
1009
1010 assert!(transport.send_ack(&ack(1)).unwrap());
1015 assert!(transport.ack_in_flight());
1016 assert!(transport.send_ack(&ack(2)).unwrap());
1017 assert!(transport.send_ack(&ack(3)).unwrap());
1018
1019 flush_acks(&mut transport);
1020
1021 let acks = read_acks(&mut peer, 2);
1023 let seqs: Vec<u64> = acks.iter().map(|a| a.acked_sequence).collect();
1024 assert_eq!(seqs, vec![1, 3]);
1025 }
1026
1027 #[test]
1028 fn sequential_acks_all_reach_the_wire_in_order() {
1029 let (client, mut peer) = socket_pair();
1030 let mut transport = UringTransport::new(&client).unwrap();
1031
1032 for seq in 1..=3 {
1033 assert!(transport.send_ack(&ack(seq)).unwrap());
1034 flush_acks(&mut transport);
1035 }
1036
1037 let acks = read_acks(&mut peer, 3);
1038 let seqs: Vec<u64> = acks.iter().map(|a| a.acked_sequence).collect();
1039 assert_eq!(seqs, vec![1, 2, 3]);
1040 }
1041
1042 #[test]
1050 fn drop_quiesces_the_multishot_recv() {
1051 let (client, _peer) = socket_pair();
1052 let mut transport = UringTransport::new(&client).unwrap();
1053 let mut recv_buf = Vec::new();
1056 transport.poll_recv(&mut recv_buf).unwrap();
1057 assert!(transport.multishot_active);
1058
1059 let started = Instant::now();
1060 drop(transport);
1061 let elapsed = started.elapsed();
1062 assert!(
1063 elapsed < DRAIN_TIMEOUT / 4,
1064 "teardown took {elapsed:?}, close to the {DRAIN_TIMEOUT:?} deadline"
1065 );
1066 }
1067
1068 #[test]
1075 fn abandoned_cqes_still_settle_kernel_ownership() {
1076 let (client, _peer) = socket_pair();
1077 let mut transport = UringTransport::new(&client).unwrap();
1078 let mut recv_buf = Vec::new();
1079 transport.poll_recv(&mut recv_buf).unwrap();
1080
1081 transport.ack_in_flight = true;
1084 transport.settle_unprocessed(&[(TOKEN_SEND, 13, 0)]);
1085 assert!(
1086 !transport.ack_in_flight,
1087 "an abandoned SEND completion must release `ack_buf`"
1088 );
1089
1090 transport.settle_unprocessed(&[(TOKEN_RECV, 128, IORING_CQE_F_MORE)]);
1093 assert!(
1094 transport.multishot_active,
1095 "F_MORE must leave the multishot armed"
1096 );
1097 transport.settle_unprocessed(&[(TOKEN_RECV, 0, 0)]);
1099 assert!(!transport.multishot_active);
1100
1101 transport.multishot_active = true;
1105 }
1106
1107 #[test]
1111 fn drop_quiesces_a_pending_ack_send() {
1112 let (client, _peer) = socket_pair();
1113 let mut transport = UringTransport::new(&client).unwrap();
1114 assert!(transport.send_ack(&ack(7)).unwrap());
1115 assert!(transport.ack_in_flight());
1116
1117 let started = Instant::now();
1118 drop(transport);
1119 let elapsed = started.elapsed();
1120 assert!(
1121 elapsed < DRAIN_TIMEOUT / 4,
1122 "teardown took {elapsed:?}, close to the {DRAIN_TIMEOUT:?} deadline"
1123 );
1124 }
1125
1126 #[cfg(not(feature = "no-persist"))]
1131 mod scripted {
1132 use super::*;
1133 use melin_app::app_factory::AppFactory;
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 pub(super) struct App;
1168
1169 impl Application for App {
1170 type Event = EvtAdd;
1171 type Report = Rpt;
1172 type QueryResponse = Rpt;
1173 const APP_VERSION: u16 = 1;
1174
1175 fn apply(&mut self, _e: EvtAdd, _ctx: &ApplyCtx, _out: &mut Vec<Rpt>) -> Option<Rpt> {
1176 None
1177 }
1178 fn tick(&mut self, _now_ns: u64, _out: &mut Vec<Rpt>) {}
1179 fn check_request_seq(&mut self, _key_hash: u64, _seq: u64) -> bool {
1180 true
1181 }
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) struct Factory;
1194
1195 impl AppFactory for Factory {
1196 type App = App;
1197 fn empty(&self) -> App {
1198 App
1199 }
1200 fn prefault(&self, _app: &mut App) {}
1201 }
1202
1203 pub(super) const REPLICA_KEY: [u8; 32] = [0xFC; 32];
1206
1207 pub(super) fn replica_auth() -> melin_app::auth::AuthorizedKeys {
1210 let key = ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY);
1211 let pub_b64 = base64::Engine::encode(
1212 &base64::engine::general_purpose::STANDARD,
1213 key.verifying_key().to_bytes(),
1214 );
1215 melin_app::auth::AuthorizedKeys::parse(&format!("replication {pub_b64} test-replica\n"))
1216 .expect("parse keys")
1217 }
1218
1219 pub(super) fn read_replica_msg(stream: &mut TcpStream) -> ReplicaMessage {
1221 let mut len_buf = [0u8; 4];
1222 stream.read_exact(&mut len_buf).expect("frame length");
1223 let len = u32::from_le_bytes(len_buf) as usize;
1224 let mut payload = vec![0u8; len];
1225 stream.read_exact(&mut payload).expect("frame payload");
1226 decode_replica_message(&payload).expect("decodable replica frame")
1227 }
1228
1229 pub(super) fn wait_for_ack(stream: &mut TcpStream, seq: u64) {
1231 loop {
1232 if let ReplicaMessage::Ack(a) = read_replica_msg(stream)
1233 && a.acked_sequence >= seq
1234 {
1235 return;
1236 }
1237 }
1238 }
1239
1240 pub(super) fn accept_within(listener: &std::net::TcpListener, secs: u64) -> TcpStream {
1243 let deadline = Instant::now() + Duration::from_secs(secs);
1244 loop {
1245 match listener.accept() {
1246 Ok((s, _)) => {
1247 s.set_read_timeout(Some(Duration::from_secs(10)))
1248 .expect("read timeout");
1249 return s;
1250 }
1251 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
1252 assert!(Instant::now() < deadline, "replica never connected");
1253 std::thread::sleep(Duration::from_millis(10));
1254 }
1255 Err(e) => panic!("accept failed: {e}"),
1256 }
1257 }
1258 }
1259 }
1260
1261 #[cfg(all(feature = "hash-chain", not(feature = "no-persist")))]
1267 mod divergence_resync {
1268 use super::super::super::auth::authenticate_replica;
1269 use super::scripted::*;
1270 use super::*;
1271 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
1272 use melin_transport_core::cursors::WireSeq;
1273 use melin_transport_core::replication::catchup::{lineage_origin, snapshot_transfer_with};
1274 use melin_transport_core::replication::protocol::{
1275 encode_hash_mismatch, encode_rotate, encode_stream_start,
1276 };
1277 use std::sync::Arc;
1278 use std::sync::atomic::AtomicBool;
1279
1280 #[test]
1289 fn mid_stream_divergence_resyncs_in_process() {
1290 let dir = tempfile::tempdir().expect("tempdir");
1291
1292 let primary_journal = dir.path().join("primary.journal");
1296 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1297 let mut chain_at_4 = [0u8; 32];
1298 for v in 1..=5u64 {
1299 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1300 if v == 2 {
1301 w.rotate_segment().expect("rotate");
1302 }
1303 if v == 4 {
1304 chain_at_4 = w.chain_hash().expect("chain");
1305 }
1306 }
1307 drop(w);
1308 melin_transport_core::snapshot::save::<App>(
1309 &App,
1310 WireSeq::new(4),
1311 chain_at_4,
1312 0,
1313 &primary_journal.with_extension("snapshot"),
1314 )
1315 .expect("save snapshot");
1316 let (lineage_start, lineage_anchor) =
1317 lineage_origin(&primary_journal).expect("lineage");
1318
1319 let authorized_keys = replica_auth();
1321
1322 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1323 listener.set_nonblocking(true).expect("nonblocking");
1324 let addr = listener.local_addr().expect("addr");
1325
1326 let replica_journal = dir.path().join("replica.journal");
1328 let replica_snapshot = dir.path().join("replica.snapshot");
1329 let shutdown = Arc::new(AtomicBool::new(false));
1330 let control = crate::replication::ReplicaControlPlane::new();
1331 let cores = crate::layout::PipelineCores::unpinned();
1332 let replica = {
1333 let journal = replica_journal.clone();
1334 let shutdown = Arc::clone(&shutdown);
1335 let control = control.clone();
1336 std::thread::spawn(move || -> Result<bool, String> {
1337 run_receiver::<App>(
1338 addr,
1339 &journal,
1340 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1341 &shutdown,
1342 &control,
1343 3_600_000,
1344 replica_snapshot,
1345 cores,
1346 melin_journal::StagingMode::ZeroFill,
1347 Duration::ZERO,
1348 64,
1349 Arc::new(Factory),
1350 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1351 )
1352 .map(|state| state.is_none())
1354 .map_err(|e| e.to_string())
1355 })
1356 };
1357
1358 let mut s1 = accept_within(&listener, 30);
1361 let mut s1r = s1.try_clone().expect("clone");
1362 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1363 match read_replica_msg(&mut s1) {
1364 ReplicaMessage::Handshake(h) => {
1365 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
1366 }
1367 other => panic!("expected Handshake, got {other:?}"),
1368 }
1369 let mut buf = Vec::new();
1370 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
1371 s1.write_all(&buf).expect("StreamStart 1");
1372 buf.clear();
1373 melin_transport_core::replication_wire::encode_input_batch(
1374 &[InputSlot::<EvtAdd> {
1375 connection_id: 0,
1376 key_hash: 0,
1377 request_seq: 0,
1378 sequence: 1,
1379 timestamp_ns: 1,
1380 event: JournalEvent::App(EvtAdd(1)),
1381 publish_ts: Default::default(),
1383 recv_ts: Default::default(),
1384 }],
1385 &mut buf,
1386 );
1387 s1.write_all(&buf).expect("InputBatch");
1388 wait_for_ack(&mut s1, 1);
1389
1390 buf.clear();
1393 encode_rotate(1, &[0xEE; 32], &mut buf);
1394 s1.write_all(&buf).expect("Rotate");
1395
1396 let mut s2 = accept_within(&listener, 30);
1399 let mut s2r = s2.try_clone().expect("clone");
1400 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1401 match read_replica_msg(&mut s2) {
1402 ReplicaMessage::Handshake(h) => assert_eq!(
1403 h.last_sequence, 1,
1404 "post-divergence handshake must carry the recovered journal position"
1405 ),
1406 other => panic!("expected Handshake, got {other:?}"),
1407 }
1408 assert!(
1414 !control.pipeline_healthy.load(Ordering::Acquire),
1415 "the divergence must have latched the health mirror false"
1416 );
1417 buf.clear();
1418 encode_hash_mismatch(&mut buf);
1419 s2.write_all(&buf).expect("HashMismatch");
1420 let transfer_shutdown = AtomicBool::new(false);
1421 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1422 s2.write_all(b)?;
1423 s2.flush()
1424 };
1425 let end = snapshot_transfer_with::<EvtAdd>(
1428 &primary_journal,
1429 &mut publish,
1430 &transfer_shutdown,
1431 1,
1432 )
1433 .expect("snapshot transfer");
1434 assert_eq!(
1435 end,
1436 melin_transport_core::replication::catchup::CatchUpResult::Ok(5)
1437 );
1438 let mut s2_acks = s2.try_clone().expect("clone");
1439 wait_for_ack(&mut s2_acks, 5);
1440
1441 assert!(
1444 control.pipeline_healthy.load(Ordering::Acquire),
1445 "the resync rebuild must reset the health mirror true"
1446 );
1447
1448 shutdown.store(true, Ordering::Relaxed);
1451 let result = replica.join().expect("replica thread panicked");
1452 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
1453
1454 assert!(
1457 dir.path().join("replica.journal.divergent.0").exists(),
1458 "divergent lineage must be archived"
1459 );
1460 assert!(replica_journal.exists(), "re-seeded live journal");
1461 }
1462
1463 #[test]
1477 fn resync_transfer_failure_rebuilds_clean_not_over_stale_writer() {
1478 let dir = tempfile::tempdir().expect("tempdir");
1479
1480 let primary_journal = dir.path().join("primary.journal");
1484 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1485 for v in 1..=2u64 {
1486 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1487 }
1488 drop(w);
1489 let (lineage_start, lineage_anchor) =
1490 lineage_origin(&primary_journal).expect("lineage");
1491
1492 let authorized_keys = replica_auth();
1493
1494 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1495 listener.set_nonblocking(true).expect("nonblocking");
1496 let addr = listener.local_addr().expect("addr");
1497
1498 let replica_journal = dir.path().join("replica.journal");
1499 let replica_snapshot = dir.path().join("replica.snapshot");
1500 let shutdown = Arc::new(AtomicBool::new(false));
1501 let control = crate::replication::ReplicaControlPlane::new();
1502 let cores = crate::layout::PipelineCores::unpinned();
1503 let replica = {
1504 let journal = replica_journal.clone();
1505 let shutdown = Arc::clone(&shutdown);
1506 let control = control.clone();
1507 std::thread::spawn(move || -> Result<bool, String> {
1508 run_receiver::<App>(
1509 addr,
1510 &journal,
1511 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1512 &shutdown,
1513 &control,
1514 3_600_000,
1515 replica_snapshot,
1516 cores,
1517 melin_journal::StagingMode::ZeroFill,
1518 Duration::ZERO,
1519 64,
1520 Arc::new(Factory),
1521 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1522 )
1523 .map(|state| state.is_none())
1524 .map_err(|e| e.to_string())
1525 })
1526 };
1527
1528 let stream_one = |s: &mut TcpStream, buf: &mut Vec<u8>| {
1531 buf.clear();
1532 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, buf);
1533 s.write_all(buf).expect("StreamStart");
1534 buf.clear();
1535 melin_transport_core::replication_wire::encode_input_batch(
1536 &[InputSlot::<EvtAdd> {
1537 connection_id: 0,
1538 key_hash: 0,
1539 request_seq: 0,
1540 sequence: 1,
1541 timestamp_ns: 1,
1542 event: JournalEvent::App(EvtAdd(1)),
1543 publish_ts: Default::default(),
1544 recv_ts: Default::default(),
1545 }],
1546 buf,
1547 );
1548 s.write_all(buf).expect("InputBatch");
1549 };
1550
1551 let mut buf = Vec::new();
1556 let mut s1 = accept_within(&listener, 30);
1557 let mut s1r = s1.try_clone().expect("clone");
1558 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1559 match read_replica_msg(&mut s1) {
1560 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0, "fresh"),
1561 other => panic!("expected Handshake, got {other:?}"),
1562 }
1563 stream_one(&mut s1, &mut buf);
1564 wait_for_ack(&mut s1, 1);
1565 buf.clear();
1566 encode_rotate(1, &[0xEE; 32], &mut buf);
1567 s1.write_all(&buf).expect("Rotate");
1568
1569 let mut s2 = accept_within(&listener, 30);
1574 let mut s2r = s2.try_clone().expect("clone");
1575 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1576 match read_replica_msg(&mut s2) {
1577 ReplicaMessage::Handshake(h) => assert_eq!(
1578 h.last_sequence, 1,
1579 "reconnect carries the recovered forked position"
1580 ),
1581 other => panic!("expected Handshake, got {other:?}"),
1582 }
1583 buf.clear();
1584 encode_hash_mismatch(&mut buf);
1585 s2.write_all(&buf).expect("HashMismatch");
1586 s2.flush().expect("flush");
1587 drop(s2);
1590 drop(s2r);
1591
1592 let mut s3 = accept_within(&listener, 30);
1597 let mut s3r = s3.try_clone().expect("clone");
1598 authenticate_replica(&mut s3r, &authorized_keys).expect("auth 3");
1599 match read_replica_msg(&mut s3) {
1600 ReplicaMessage::Handshake(h) => assert_eq!(
1601 h.last_sequence, 0,
1602 "after a failed transfer the retry handshakes as fresh"
1603 ),
1604 other => panic!("expected Handshake, got {other:?}"),
1605 }
1606 stream_one(&mut s3, &mut buf);
1607 wait_for_ack(&mut s3, 1);
1608
1609 shutdown.store(true, Ordering::Relaxed);
1610 let result = replica.join().expect("replica thread panicked");
1611 assert_eq!(result, Ok(true), "receiver exits cleanly via shutdown");
1612
1613 assert!(
1618 dir.path().join("replica.journal.divergent.0").exists(),
1619 "forked lineage archived, never deleted"
1620 );
1621 assert!(
1622 replica_journal.exists(),
1623 "fresh live journal recreated after the failed transfer"
1624 );
1625 }
1626
1627 #[test]
1634 fn second_mid_stream_divergence_exits_hard() {
1635 let dir = tempfile::tempdir().expect("tempdir");
1636
1637 let primary_journal = dir.path().join("primary.journal");
1638 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1639 let mut chain_at_4 = [0u8; 32];
1640 for v in 1..=5u64 {
1641 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1642 if v == 2 {
1643 w.rotate_segment().expect("rotate");
1644 }
1645 if v == 4 {
1646 chain_at_4 = w.chain_hash().expect("chain");
1647 }
1648 }
1649 drop(w);
1650 melin_transport_core::snapshot::save::<App>(
1651 &App,
1652 WireSeq::new(4),
1653 chain_at_4,
1654 0,
1655 &primary_journal.with_extension("snapshot"),
1656 )
1657 .expect("save snapshot");
1658 let (lineage_start, lineage_anchor) =
1659 lineage_origin(&primary_journal).expect("lineage");
1660
1661 let authorized_keys = replica_auth();
1662
1663 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1664 listener.set_nonblocking(true).expect("nonblocking");
1665 let addr = listener.local_addr().expect("addr");
1666
1667 let replica_journal = dir.path().join("replica.journal");
1668 let replica_snapshot = dir.path().join("replica.snapshot");
1669 let shutdown = Arc::new(AtomicBool::new(false));
1670 let control = crate::replication::ReplicaControlPlane::new();
1671 let cores = crate::layout::PipelineCores::unpinned();
1672 let replica = {
1673 let journal = replica_journal.clone();
1674 let shutdown = Arc::clone(&shutdown);
1675 let control = control.clone();
1676 std::thread::spawn(move || -> Result<bool, String> {
1677 run_receiver::<App>(
1678 addr,
1679 &journal,
1680 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1681 &shutdown,
1682 &control,
1683 3_600_000,
1684 replica_snapshot,
1685 cores,
1686 melin_journal::StagingMode::ZeroFill,
1687 Duration::ZERO,
1688 64,
1689 Arc::new(Factory),
1690 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1691 )
1692 .map(|state| state.is_none())
1693 .map_err(|e| e.to_string())
1694 })
1695 };
1696
1697 let mut s1 = accept_within(&listener, 30);
1699 let mut s1r = s1.try_clone().expect("clone");
1700 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1701 match read_replica_msg(&mut s1) {
1702 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0),
1703 other => panic!("expected Handshake, got {other:?}"),
1704 }
1705 let mut buf = Vec::new();
1706 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
1707 s1.write_all(&buf).expect("StreamStart 1");
1708 buf.clear();
1709 melin_transport_core::replication_wire::encode_input_batch(
1710 &[InputSlot::<EvtAdd> {
1711 connection_id: 0,
1712 key_hash: 0,
1713 request_seq: 0,
1714 sequence: 1,
1715 timestamp_ns: 1,
1716 event: JournalEvent::App(EvtAdd(1)),
1717 publish_ts: Default::default(),
1718 recv_ts: Default::default(),
1719 }],
1720 &mut buf,
1721 );
1722 s1.write_all(&buf).expect("InputBatch");
1723 wait_for_ack(&mut s1, 1);
1724 buf.clear();
1725 encode_rotate(1, &[0xEE; 32], &mut buf);
1726 s1.write_all(&buf).expect("Rotate 1");
1727
1728 let mut s2 = accept_within(&listener, 30);
1731 let mut s2r = s2.try_clone().expect("clone");
1732 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1733 match read_replica_msg(&mut s2) {
1734 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 1),
1735 other => panic!("expected Handshake, got {other:?}"),
1736 }
1737 buf.clear();
1738 encode_hash_mismatch(&mut buf);
1739 s2.write_all(&buf).expect("HashMismatch");
1740 let transfer_shutdown = AtomicBool::new(false);
1741 {
1742 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1743 s2.write_all(b)?;
1744 s2.flush()
1745 };
1746 snapshot_transfer_with::<EvtAdd>(
1747 &primary_journal,
1748 &mut publish,
1749 &transfer_shutdown,
1750 1,
1751 )
1752 .expect("snapshot transfer");
1753 }
1754 buf.clear();
1755 melin_transport_core::replication_wire::encode_input_batch(
1756 &[InputSlot::<EvtAdd> {
1757 connection_id: 0,
1758 key_hash: 0,
1759 request_seq: 0,
1760 sequence: 6,
1761 timestamp_ns: 6,
1762 event: JournalEvent::App(EvtAdd(6)),
1763 publish_ts: Default::default(),
1764 recv_ts: Default::default(),
1765 }],
1766 &mut buf,
1767 );
1768 s2.write_all(&buf).expect("InputBatch 6");
1769 let mut s2_acks = s2.try_clone().expect("clone");
1770 wait_for_ack(&mut s2_acks, 6);
1771 buf.clear();
1772 encode_rotate(6, &[0xEE; 32], &mut buf);
1773 s2.write_all(&buf).expect("Rotate 2");
1774
1775 let result = replica.join().expect("replica thread panicked");
1778 let err = result.expect_err("second divergence must exit hard");
1779 assert!(
1780 err.contains("divergence recurred"),
1781 "error must name the recurrence: {err}"
1782 );
1783 }
1784
1785 type PromoteHandles =
1794 crate::replication::ReplicaPipelineHandles<App, BufferedWriter<EvtAdd>>;
1795
1796 #[test]
1797 fn promotion_hands_back_local_state_when_present() {
1798 let dir = tempfile::tempdir().unwrap();
1799 let mut pipeline: Option<PromoteHandles> = None;
1800 let mut exchange = Some(App);
1801 let mut journal_writer =
1802 Some(BufferedWriter::<EvtAdd>::create(&dir.path().join("p.journal")).unwrap());
1803
1804 let result = crate::replication::take_pipeline_for_promotion(
1805 &mut pipeline,
1806 &mut exchange,
1807 &mut journal_writer,
1808 );
1809
1810 assert!(matches!(result, Ok(Some(_))), "warm state must be promoted");
1811 assert!(
1812 exchange.is_none() && journal_writer.is_none(),
1813 "state must be moved into the result"
1814 );
1815 }
1816
1817 #[test]
1818 fn promotion_errs_when_no_local_state() {
1819 let mut pipeline: Option<PromoteHandles> = None;
1820 let mut exchange: Option<App> = None;
1821 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
1822
1823 let result = crate::replication::take_pipeline_for_promotion(
1824 &mut pipeline,
1825 &mut exchange,
1826 &mut journal_writer,
1827 );
1828
1829 let err = match result {
1831 Err(e) => e,
1832 Ok(_) => panic!("a promote with nothing to promote must error"),
1833 };
1834 assert!(
1835 err.to_string().contains("no local state available"),
1836 "{err}"
1837 );
1838 }
1839
1840 #[test]
1841 fn promotion_errs_on_partial_state() {
1842 let mut pipeline: Option<PromoteHandles> = None;
1844 let mut exchange = Some(App);
1845 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
1846
1847 let result = crate::replication::take_pipeline_for_promotion(
1848 &mut pipeline,
1849 &mut exchange,
1850 &mut journal_writer,
1851 );
1852
1853 assert!(result.is_err(), "partial state is not a valid promotion");
1854 }
1855 }
1856
1857 #[cfg(not(feature = "no-persist"))]
1863 mod backoff_shutdown {
1864 use super::super::super::auth::authenticate_replica;
1865 use super::scripted::*;
1866 use super::*;
1867 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
1868 use melin_transport_core::replication::catchup::lineage_origin;
1869 use melin_transport_core::replication::protocol::encode_stream_start;
1870 use std::sync::Arc;
1871 use std::sync::atomic::AtomicBool;
1872
1873 fn live_thread_names() -> Vec<String> {
1876 std::fs::read_dir("/proc/self/task")
1877 .expect("read /proc/self/task")
1878 .filter_map(|entry| {
1879 let entry = entry.ok()?;
1882 let name = std::fs::read_to_string(entry.path().join("comm")).ok()?;
1883 Some(name.trim().to_string())
1884 })
1885 .collect()
1886 }
1887
1888 #[test]
1894 fn shutdown_during_reconnect_backoff_joins_pipeline_threads() {
1895 let dir = tempfile::tempdir().expect("tempdir");
1896
1897 let primary_journal = dir.path().join("primary.journal");
1900 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1901 w.append(&JournalEvent::App(EvtAdd(1))).expect("append");
1902 drop(w);
1903 let (lineage_start, lineage_anchor) =
1904 lineage_origin(&primary_journal).expect("lineage");
1905
1906 let authorized_keys = replica_auth();
1907 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1908 listener.set_nonblocking(true).expect("nonblocking");
1909 let addr = listener.local_addr().expect("addr");
1910
1911 let shutdown = Arc::new(AtomicBool::new(false));
1912 let control = crate::replication::ReplicaControlPlane::new();
1913 let replica = {
1914 let journal = dir.path().join("replica.journal");
1915 let snapshot = dir.path().join("replica.snapshot");
1916 let shutdown = Arc::clone(&shutdown);
1917 let control = control.clone();
1918 std::thread::spawn(move || -> Result<bool, String> {
1919 run_receiver::<App>(
1920 addr,
1921 &journal,
1922 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1923 &shutdown,
1924 &control,
1925 3_600_000,
1926 snapshot,
1927 crate::layout::PipelineCores::unpinned(),
1928 melin_journal::StagingMode::ZeroFill,
1929 Duration::ZERO,
1930 64,
1931 Arc::new(Factory),
1932 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1933 )
1934 .map(|state| state.is_none())
1936 .map_err(|e| e.to_string())
1937 })
1938 };
1939
1940 let mut s1 = accept_within(&listener, 30);
1944 let mut s1r = s1.try_clone().expect("clone");
1945 authenticate_replica(&mut s1r, &authorized_keys).expect("auth");
1946 match read_replica_msg(&mut s1) {
1947 ReplicaMessage::Handshake(h) => {
1948 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
1949 }
1950 other => panic!("expected Handshake, got {other:?}"),
1951 }
1952 let mut buf = Vec::new();
1953 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
1954 s1.write_all(&buf).expect("StreamStart");
1955 buf.clear();
1956 melin_transport_core::replication_wire::encode_input_batch(
1957 &[InputSlot::<EvtAdd> {
1958 connection_id: 0,
1959 key_hash: 0,
1960 request_seq: 0,
1961 sequence: 1,
1962 timestamp_ns: 1,
1963 event: JournalEvent::App(EvtAdd(1)),
1964 publish_ts: Default::default(),
1965 recv_ts: Default::default(),
1966 }],
1967 &mut buf,
1968 );
1969 s1.write_all(&buf).expect("InputBatch");
1970 wait_for_ack(&mut s1, 1);
1971
1972 drop(s1);
1976 drop(s1r);
1977 drop(listener);
1978 std::thread::sleep(Duration::from_millis(1500));
1983
1984 shutdown.store(true, Ordering::Relaxed);
1985 let deadline = Instant::now() + Duration::from_secs(10);
1986 while !replica.is_finished() {
1987 assert!(
1988 Instant::now() < deadline,
1989 "receiver did not exit after shutdown"
1990 );
1991 std::thread::sleep(Duration::from_millis(20));
1992 }
1993 let result = replica.join().expect("replica thread panicked");
1994 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
1995
1996 let deadline = Instant::now() + Duration::from_secs(10);
2002 loop {
2003 let names = live_thread_names();
2004 if !names
2005 .iter()
2006 .any(|n| n == "journal-seq" || n == "matching" || n == "drain")
2007 {
2008 break;
2009 }
2010 assert!(
2011 Instant::now() < deadline,
2012 "pipeline threads still alive after shutdown: {names:?}"
2013 );
2014 std::thread::sleep(Duration::from_millis(50));
2015 }
2016 }
2017 }
2018
2019 #[cfg(all(feature = "hash-chain", not(feature = "no-persist")))]
2026 mod rotation_adoption {
2027 use super::super::super::auth::authenticate_replica;
2028 use super::scripted::*;
2029 use super::*;
2030 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
2031 use melin_transport_core::replication::catchup::lineage_origin;
2032 use melin_transport_core::replication::protocol::{encode_rotate, encode_stream_start};
2033 use std::sync::Arc;
2034 use std::sync::atomic::AtomicBool;
2035
2036 fn payloads(path: &std::path::Path) -> Vec<u64> {
2038 let mut reader = melin_journal::JournalReader::<EvtAdd>::open(path).expect("open");
2039 let mut out = Vec::new();
2040 while let Some(entry) = reader.next_entry().expect("read entry") {
2041 if let JournalEvent::App(EvtAdd(v)) = entry.event {
2042 out.push(v);
2043 }
2044 }
2045 out
2046 }
2047
2048 fn boundary_adoption_rotates_and_streams_on(mode: melin_journal::StagingMode) {
2059 let _prealloc_guard =
2064 melin_journal::test_utils::PreallocOverrideGuard::new(1024 * 1024);
2065 let dir = tempfile::tempdir().expect("tempdir");
2066
2067 let primary_journal = dir.path().join("primary.journal");
2073 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
2074 for v in 1..=2u64 {
2075 w.batch_append_with_ts(&JournalEvent::App(EvtAdd(v)), v, 0, 0)
2076 .expect("append");
2077 }
2078 w.flush_batch_sync().expect("flush");
2079 let chain_at_2 = w.chain_hash().expect("chain");
2080 drop(w);
2081 let (lineage_start, lineage_anchor) =
2082 lineage_origin(&primary_journal).expect("lineage");
2083
2084 let authorized_keys = replica_auth();
2085 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
2086 listener.set_nonblocking(true).expect("nonblocking");
2087 let addr = listener.local_addr().expect("addr");
2088
2089 let replica_journal = dir.path().join("replica.journal");
2090 let shutdown = Arc::new(AtomicBool::new(false));
2091 let control = crate::replication::ReplicaControlPlane::new();
2092 let replica = {
2093 let journal = replica_journal.clone();
2094 let snapshot = dir.path().join("replica.snapshot");
2095 let shutdown = Arc::clone(&shutdown);
2096 let control = control.clone();
2097 std::thread::spawn(move || -> Result<bool, String> {
2098 run_receiver::<App>(
2099 addr,
2100 &journal,
2101 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
2102 &shutdown,
2103 &control,
2104 3_600_000,
2105 snapshot,
2106 crate::layout::PipelineCores::unpinned(),
2107 mode,
2108 Duration::ZERO,
2109 64,
2110 Arc::new(Factory),
2111 Arc::new(melin_transport_core::fence::FenceState::new(0)),
2112 )
2113 .map(|state| state.is_none())
2115 .map_err(|e| e.to_string())
2116 })
2117 };
2118
2119 let mut s1 = accept_within(&listener, 30);
2120 let mut s1r = s1.try_clone().expect("clone");
2121 authenticate_replica(&mut s1r, &authorized_keys).expect("auth");
2122 match read_replica_msg(&mut s1) {
2123 ReplicaMessage::Handshake(h) => {
2124 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
2125 }
2126 other => panic!("expected Handshake, got {other:?}"),
2127 }
2128 let mut buf = Vec::new();
2129 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
2130 s1.write_all(&buf).expect("StreamStart");
2131
2132 let send_events = |s: &mut TcpStream, range: std::ops::RangeInclusive<u64>| {
2133 let slots: Vec<InputSlot<EvtAdd>> = range
2134 .map(|v| InputSlot {
2135 connection_id: 0,
2136 key_hash: 0,
2137 request_seq: 0,
2138 sequence: v,
2139 timestamp_ns: v,
2140 event: JournalEvent::App(EvtAdd(v)),
2141 publish_ts: Default::default(),
2143 recv_ts: Default::default(),
2144 })
2145 .collect();
2146 let mut buf = Vec::new();
2147 melin_transport_core::replication_wire::encode_input_batch(&slots, &mut buf);
2148 s.write_all(&buf).expect("InputBatch");
2149 };
2150
2151 send_events(&mut s1, 1..=2);
2152 wait_for_ack(&mut s1, 2);
2153
2154 let staging = {
2161 let mut s = replica_journal.clone().into_os_string();
2165 s.push(".next-staging");
2166 std::path::PathBuf::from(s)
2167 };
2168 let staged_len: u64 = (1 + 8) * 1024 * 1024;
2169 let deadline = Instant::now() + Duration::from_secs(10);
2170 loop {
2171 if std::fs::metadata(&staging).is_ok_and(|m| m.len() == staged_len) {
2172 break;
2173 }
2174 assert!(
2175 Instant::now() < deadline,
2176 "preparer produced no full-size staged segment in {mode:?} mode"
2177 );
2178 std::thread::sleep(Duration::from_millis(10));
2179 }
2180 std::thread::sleep(Duration::from_millis(300));
2188
2189 buf.clear();
2192 encode_rotate(2, &chain_at_2, &mut buf);
2193 s1.write_all(&buf).expect("Rotate");
2194
2195 send_events(&mut s1, 3..=4);
2199 wait_for_ack(&mut s1, 4);
2200
2201 shutdown.store(true, Ordering::Relaxed);
2202 let result = replica.join().expect("replica thread panicked");
2203 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
2204
2205 let archived = dir.path().join("replica.journal.000001");
2209 assert!(archived.exists(), "sealed segment must be archived");
2210 assert_eq!(payloads(&archived), vec![1, 2]);
2211 assert_eq!(payloads(&replica_journal), vec![3, 4]);
2212 }
2213
2214 #[test]
2215 fn boundary_adoption_zero_fill() {
2216 boundary_adoption_rotates_and_streams_on(melin_journal::StagingMode::ZeroFill);
2217 }
2218
2219 #[test]
2220 fn boundary_adoption_allocate() {
2221 boundary_adoption_rotates_and_streams_on(melin_journal::StagingMode::Allocate);
2222 }
2223 }
2224}