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::server::PipelineCores,
558 group_commit_delay: std::time::Duration,
559 pipeline_depth: usize,
560 busy_spin: bool,
561 factory: std::sync::Arc<dyn melin_app::app_factory::AppFactory<App = A>>,
562 fence_state: std::sync::Arc<melin_transport_core::fence::FenceState>,
563) -> ReceiverResult<A, BufferedWriter<A::Event>>
564where
565 A: Application + Send + 'static,
566 A::Event: Send + Sync + 'static,
567 A::Report: Send + 'static,
568 A::QueryResponse: Send + 'static,
569{
570 let super::ReplicaControlPlane {
571 promote,
572 tip_ready,
573 journal_tip,
574 primary_link_up,
575 primary_acking_mode,
576 pipeline_healthy,
577 } = control;
578 let (mut exchange, mut journal_writer, mut last_sequence, mut chain_hash) =
582 recover_replica_state::<A, BufferedWriter<A::Event>>(
583 journal_path,
584 &snapshot_path,
585 factory.as_ref(),
586 &fence_state,
587 )?;
588 journal_tip.advance(melin_transport_core::WireSeq::new(last_sequence));
594 tip_ready.store(true, Ordering::Release);
595
596 let mut backoff = std::time::Duration::from_secs(1);
597
598 let mut divergence_resyncs: u32 = 0;
601
602 let mut send_buf = Vec::with_capacity(64);
603 let mut pipeline: Option<ReplicaPipelineHandles<A, BufferedWriter<A::Event>>> = None;
604
605 loop {
607 primary_link_up.store(false, Ordering::Release);
610
611 if let Some(p) = pipeline.as_ref() {
612 if let Some(ref lock) = p.chain_hash_lock {
621 let fsync_state = lock.load();
622 last_sequence = fsync_state.journal_seq.get();
623 chain_hash = fsync_state.chain_hash;
624 } else {
625 last_sequence = p.last_seq.load().get();
626 }
627 }
628
629 if shutdown.load(Ordering::Relaxed) {
634 if let Some(p) = pipeline.take() {
635 let _ = teardown_replica_pipeline::<A, BufferedWriter<A::Event>>(p);
636 }
637 return Ok(None);
638 }
639 if promote.is_requested() {
640 info!("promotion triggered while disconnected");
641 return take_pipeline_for_promotion(&mut pipeline, &mut exchange, &mut journal_writer);
642 }
643
644 info!(primary = %primary_addr, "connecting to primary as replica");
646
647 let stream = match TcpStream::connect(primary_addr) {
648 Ok(s) => s,
649 Err(e) => {
650 warn!(
651 error = %e,
652 backoff_secs = backoff.as_secs(),
653 "failed to connect to primary — retrying"
654 );
655 sleep_then_double_backoff(&mut backoff, shutdown, promote);
656 continue;
657 }
658 };
659 if let Err(e) = stream.set_nodelay(true) {
660 warn!(error = %e, "failed to set TCP_NODELAY on replica receive socket");
661 }
662 {
663 use std::os::unix::io::AsRawFd;
664 let busy_poll: libc::c_int = 50;
665 let rc = unsafe {
666 libc::setsockopt(
667 stream.as_raw_fd(),
668 libc::SOL_SOCKET,
669 libc::SO_BUSY_POLL,
670 &busy_poll as *const libc::c_int as *const libc::c_void,
671 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
672 )
673 };
674 if rc != 0 {
675 let err = std::io::Error::last_os_error();
676 warn!(error = %err, "failed to set SO_BUSY_POLL on replica receive socket");
677 }
678 }
679 stream.set_read_timeout(Some(std::time::Duration::from_secs(5)))?;
680
681 let mut reader = stream.try_clone()?;
682 let mut tcp_writer = stream;
683
684 if let Err(e) = authenticate_with_primary(&mut reader, signing_key) {
687 if matches!(
694 e.kind(),
695 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
696 ) {
697 warn!(error = %e, "no auth challenge from primary (not serving yet?) — retrying");
698 } else {
699 warn!(error = %e, "authentication failed — retrying");
700 }
701 drop(reader);
706 drop(tcp_writer);
707 sleep_then_double_backoff(&mut backoff, shutdown, promote);
710 continue;
711 }
712 info!("authenticated with primary");
713 primary_link_up.store(true, Ordering::Release);
716
717 let handshake = Handshake {
722 last_sequence,
723 chain_hash,
724 epoch: fence_state.epoch(),
725 };
726 send_buf.clear();
727 encode_handshake(&handshake, &mut send_buf);
728 tcp_writer.write_all(&send_buf)?;
729 tcp_writer.flush()?;
730 send_buf.clear();
731
732 let response_frame = read_frame(&mut reader, MAX_CONTROL_FRAME)?;
739 let response = decode_primary_message(&response_frame)?;
740 let (stream_lineage, session_start) = match response {
741 PrimaryMessage::StreamStart {
742 start_sequence,
743 segment_start_sequence,
744 anchor_hash,
745 epoch,
746 durability_mode,
747 } => {
748 let our_epoch = fence_state.epoch();
756 if fence_state.refuses_primary(epoch) {
757 warn!(
758 primary_epoch = epoch,
759 our_epoch,
760 "primary is behind our fencing epoch — refusing to follow stale primary"
761 );
762 drop(reader);
765 drop(tcp_writer);
766 sleep_then_double_backoff(&mut backoff, shutdown, promote);
767 continue;
768 }
769 fence_state.observe_epoch(epoch);
773 primary_acking_mode.store(durability_mode, Ordering::Release);
774 info!(start_sequence, epoch, durability_mode, "streaming started");
775 ((segment_start_sequence, anchor_hash), last_sequence)
776 }
777 ref resync @ (PrimaryMessage::NeedSnapshot | PrimaryMessage::HashMismatch) => {
778 let divergent = matches!(resync, PrimaryMessage::HashMismatch);
779 let decision = handle_resync_verdict(
783 divergent,
784 &mut TcpFrameSource {
785 reader: &mut reader,
786 },
787 &mut pipeline,
788 &mut exchange,
789 &mut journal_writer,
790 journal_path,
791 &snapshot_path,
792 &fence_state,
793 control,
794 &mut last_sequence,
795 &mut chain_hash,
796 )
797 .map_err(|e| -> Box<dyn std::error::Error> { e })?;
798 match decision {
799 ResyncDecision::Ready {
800 segment_start_sequence,
801 anchor_hash,
802 resume_sequence,
803 } => ((segment_start_sequence, anchor_hash), resume_sequence),
804 ResyncDecision::Retry => {
805 sleep_then_double_backoff(&mut backoff, shutdown, promote);
806 continue;
807 }
808 }
809 }
810 _ => {
811 return Err(format!("unexpected response: {response:?}").into());
812 }
813 };
814
815 if pipeline.is_none() && journal_writer.is_none() {
823 let (lineage_start, lineage_anchor) = stream_lineage;
824 let writer =
825 BufferedWriter::create_continuing(journal_path, lineage_start, lineage_anchor)?;
826 let mut fresh = factory.empty();
827 factory.apply_operator_policy(&mut fresh);
828 exchange = Some(fresh);
829 journal_writer = Some(writer);
830 }
831
832 if pipeline.is_none() {
834 let cur_exchange = exchange.take().expect("exchange initialized");
835 let cur_writer = journal_writer.take().expect("journal_writer initialized");
836 pipeline = Some(build_replica_pipeline_with_threads::<A>(
837 cur_exchange,
838 cur_writer,
839 cores,
840 snapshot_interval_ms,
841 snapshot_path.clone(),
842 group_commit_delay,
843 busy_spin,
844 Arc::clone(&fence_state),
845 Arc::clone(pipeline_healthy),
846 )?);
847 }
848
849 let result = {
851 let p = pipeline.as_mut().expect("pipeline must exist by here");
852 let input_producer = &mut p.input_producer;
853 let journal_cursor = p.journal_cursor.as_ref();
854 let stream_marks = &p.stream_marks;
855 let journal_failed = &p.journal_failed;
856 std::thread::scope(|s| {
857 let handle = std::thread::Builder::new()
858 .name("replica-receiver".into())
859 .spawn_scoped(s, || {
860 melin_app::affinity::pin_thread("replica-receiver", cores.reader);
861 let mut transport = match UringTransport::new(&tcp_writer) {
862 Ok(t) => t,
863 Err(e) => {
864 tracing::error!(error = %e, "UringTransport init failed");
865 return super::receiver_transport::StreamingResult {
871 exit: SessionExit::Disconnected,
872 heard_from_primary: false,
873 };
874 }
875 };
876 streaming_loop::<UringTransport, A::Event>(
877 &mut transport,
878 input_producer,
879 journal_cursor,
880 shutdown,
881 control,
882 pipeline_depth,
883 busy_spin,
884 session_start,
885 Vec::with_capacity(MAX_DATA_FRAME + 4),
886 None,
887 stream_marks,
888 journal_failed,
889 )
890 })
891 .expect("spawn replica-receiver thread");
892 handle.join().expect("replica-receiver thread panicked")
893 })
894 };
895
896 primary_link_up.store(false, Ordering::Release);
897 match handle_session_exit(
898 result,
899 &mut pipeline,
900 &mut divergence_resyncs,
901 &mut backoff,
902 last_sequence,
903 journal_path,
904 &snapshot_path,
905 factory.as_ref(),
906 &fence_state,
907 shutdown,
908 promote,
909 || {},
913 ) {
914 AfterSession::Return(r) => return r,
915 AfterSession::Resync {
916 exchange: ex,
917 journal_writer: wr,
918 last_sequence: seq,
919 chain_hash: hash,
920 } => {
921 exchange = ex;
922 journal_writer = wr;
923 last_sequence = seq;
924 chain_hash = hash;
925 continue;
926 }
927 AfterSession::Reconnect => {}
928 }
929 }
930}
931
932#[cfg(test)]
937mod tests {
938 use super::*;
939 use crate::uring_teardown::DRAIN_TIMEOUT;
940 use std::io::Read;
941 use std::net::TcpListener;
942 use std::time::{Duration, Instant};
943
944 use melin_transport_core::replication::protocol::{ReplicaMessage, decode_replica_message};
945
946 fn socket_pair() -> (TcpStream, TcpStream) {
948 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
949 let addr = listener.local_addr().unwrap();
950 let client = TcpStream::connect(addr).unwrap();
951 let (server, _) = listener.accept().unwrap();
952 (client, server)
953 }
954
955 fn ack(seq: u64) -> Ack {
956 Ack {
957 acked_sequence: seq,
958 in_memory_sequence: seq,
959 }
960 }
961
962 fn flush_acks(transport: &mut UringTransport) {
964 let mut recv_buf = Vec::new();
965 let deadline = Instant::now() + Duration::from_secs(5);
966 while transport.ack_in_flight() {
967 transport.poll_recv(&mut recv_buf).unwrap();
968 assert!(Instant::now() < deadline, "ack send never completed");
969 std::hint::spin_loop();
970 }
971 }
972
973 fn read_acks(peer: &mut TcpStream, expect: usize) -> Vec<Ack> {
975 peer.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
976 let mut buf = Vec::new();
977 let mut chunk = [0u8; 256];
978 let mut acks = Vec::new();
979 while acks.len() < expect {
980 let n = peer.read(&mut chunk).expect("peer read");
981 assert!(n > 0, "peer closed before all acks arrived");
982 buf.extend_from_slice(&chunk[..n]);
983 loop {
985 if buf.len() < 4 {
986 break;
987 }
988 let len = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
989 if buf.len() < 4 + len {
990 break;
991 }
992 match decode_replica_message(&buf[4..4 + len]).expect("decodable frame") {
993 ReplicaMessage::Ack(a) => acks.push(a),
994 other => panic!("expected Ack frame, got {other:?}"),
995 }
996 buf.drain(..4 + len);
997 }
998 }
999 assert!(buf.is_empty(), "trailing bytes after expected acks");
1000 acks
1001 }
1002
1003 #[test]
1004 fn coalesces_to_latest_ack_while_send_in_flight() {
1005 let (client, mut peer) = socket_pair();
1006 let mut transport = UringTransport::new(&client).unwrap();
1007
1008 assert!(transport.send_ack(&ack(1)).unwrap());
1013 assert!(transport.ack_in_flight());
1014 assert!(transport.send_ack(&ack(2)).unwrap());
1015 assert!(transport.send_ack(&ack(3)).unwrap());
1016
1017 flush_acks(&mut transport);
1018
1019 let acks = read_acks(&mut peer, 2);
1021 let seqs: Vec<u64> = acks.iter().map(|a| a.acked_sequence).collect();
1022 assert_eq!(seqs, vec![1, 3]);
1023 }
1024
1025 #[test]
1026 fn sequential_acks_all_reach_the_wire_in_order() {
1027 let (client, mut peer) = socket_pair();
1028 let mut transport = UringTransport::new(&client).unwrap();
1029
1030 for seq in 1..=3 {
1031 assert!(transport.send_ack(&ack(seq)).unwrap());
1032 flush_acks(&mut transport);
1033 }
1034
1035 let acks = read_acks(&mut peer, 3);
1036 let seqs: Vec<u64> = acks.iter().map(|a| a.acked_sequence).collect();
1037 assert_eq!(seqs, vec![1, 2, 3]);
1038 }
1039
1040 #[test]
1048 fn drop_quiesces_the_multishot_recv() {
1049 let (client, _peer) = socket_pair();
1050 let mut transport = UringTransport::new(&client).unwrap();
1051 let mut recv_buf = Vec::new();
1054 transport.poll_recv(&mut recv_buf).unwrap();
1055 assert!(transport.multishot_active);
1056
1057 let started = Instant::now();
1058 drop(transport);
1059 let elapsed = started.elapsed();
1060 assert!(
1061 elapsed < DRAIN_TIMEOUT / 4,
1062 "teardown took {elapsed:?}, close to the {DRAIN_TIMEOUT:?} deadline"
1063 );
1064 }
1065
1066 #[test]
1073 fn abandoned_cqes_still_settle_kernel_ownership() {
1074 let (client, _peer) = socket_pair();
1075 let mut transport = UringTransport::new(&client).unwrap();
1076 let mut recv_buf = Vec::new();
1077 transport.poll_recv(&mut recv_buf).unwrap();
1078
1079 transport.ack_in_flight = true;
1082 transport.settle_unprocessed(&[(TOKEN_SEND, 13, 0)]);
1083 assert!(
1084 !transport.ack_in_flight,
1085 "an abandoned SEND completion must release `ack_buf`"
1086 );
1087
1088 transport.settle_unprocessed(&[(TOKEN_RECV, 128, IORING_CQE_F_MORE)]);
1091 assert!(
1092 transport.multishot_active,
1093 "F_MORE must leave the multishot armed"
1094 );
1095 transport.settle_unprocessed(&[(TOKEN_RECV, 0, 0)]);
1097 assert!(!transport.multishot_active);
1098
1099 transport.multishot_active = true;
1103 }
1104
1105 #[test]
1109 fn drop_quiesces_a_pending_ack_send() {
1110 let (client, _peer) = socket_pair();
1111 let mut transport = UringTransport::new(&client).unwrap();
1112 assert!(transport.send_ack(&ack(7)).unwrap());
1113 assert!(transport.ack_in_flight());
1114
1115 let started = Instant::now();
1116 drop(transport);
1117 let elapsed = started.elapsed();
1118 assert!(
1119 elapsed < DRAIN_TIMEOUT / 4,
1120 "teardown took {elapsed:?}, close to the {DRAIN_TIMEOUT:?} deadline"
1121 );
1122 }
1123
1124 #[cfg(not(feature = "no-persist"))]
1129 mod scripted {
1130 use super::*;
1131 use melin_app::app_factory::AppFactory;
1132 use melin_app::{AppEvent, Application, ApplyCtx, CodecError, RejectReason};
1133
1134 pub(super) use melin_transport_core::pipeline::InputSlot;
1137
1138 #[derive(Debug, Clone, Copy, PartialEq)]
1139 pub(super) struct EvtAdd(pub(super) u64);
1140
1141 impl AppEvent for EvtAdd {
1142 fn encoded_size(&self) -> usize {
1143 8
1144 }
1145 fn encode(&self, buf: &mut [u8]) -> usize {
1146 buf[..8].copy_from_slice(&self.0.to_le_bytes());
1147 8
1148 }
1149 fn decode(buf: &[u8]) -> Result<Self, CodecError> {
1150 if buf.len() < 8 {
1151 return Err(CodecError::Truncated);
1152 }
1153 Ok(EvtAdd(u64::from_le_bytes(buf[..8].try_into().expect("8"))))
1154 }
1155 fn is_query(&self) -> bool {
1156 false
1157 }
1158 }
1159
1160 #[derive(Debug, Clone, Copy)]
1161 pub(super) struct Rpt;
1162
1163 pub(super) struct App;
1164
1165 impl Application for App {
1166 type Event = EvtAdd;
1167 type Report = Rpt;
1168 type QueryResponse = Rpt;
1169 const APP_VERSION: u16 = 1;
1170
1171 fn apply(&mut self, _e: EvtAdd, _ctx: &ApplyCtx, _out: &mut Vec<Rpt>) -> Option<Rpt> {
1172 None
1173 }
1174 fn tick(&mut self, _now_ns: u64, _out: &mut Vec<Rpt>) {}
1175 fn check_request_seq(&mut self, _key_hash: u64, _seq: u64) -> bool {
1176 true
1177 }
1178 fn build_reject(_e: &EvtAdd, _r: RejectReason) -> Rpt {
1179 Rpt
1180 }
1181 fn snapshot<W: std::io::Write>(&self, _w: &mut W) -> std::io::Result<()> {
1182 Ok(())
1183 }
1184 fn restore<R: std::io::Read>(_r: &mut R) -> std::io::Result<Self> {
1185 Ok(App)
1186 }
1187 }
1188
1189 pub(super) struct Factory;
1190
1191 impl AppFactory for Factory {
1192 type App = App;
1193 fn empty(&self) -> App {
1194 App
1195 }
1196 fn prefault(&self, _app: &mut App) {}
1197 }
1198
1199 pub(super) const REPLICA_KEY: [u8; 32] = [0xFC; 32];
1202
1203 pub(super) fn replica_auth() -> melin_app::auth::AuthorizedKeys {
1206 let key = ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY);
1207 let pub_b64 = base64::Engine::encode(
1208 &base64::engine::general_purpose::STANDARD,
1209 key.verifying_key().to_bytes(),
1210 );
1211 melin_app::auth::AuthorizedKeys::parse(&format!("replication {pub_b64} test-replica\n"))
1212 .expect("parse keys")
1213 }
1214
1215 pub(super) fn unpinned_cores() -> crate::server::PipelineCores {
1217 crate::server::PipelineCores {
1218 journal: 0,
1219 matching: 0,
1220 response: 0,
1221 reader: 0,
1222 repl_sender: 0,
1223 event_publisher: 0,
1224 shadow: 0,
1225 repl_handler_0: 0,
1226 repl_handler_1: 0,
1227 journal_prep: 0,
1228 journal_disk: 0,
1229 }
1230 }
1231
1232 pub(super) fn read_replica_msg(stream: &mut TcpStream) -> ReplicaMessage {
1234 let mut len_buf = [0u8; 4];
1235 stream.read_exact(&mut len_buf).expect("frame length");
1236 let len = u32::from_le_bytes(len_buf) as usize;
1237 let mut payload = vec![0u8; len];
1238 stream.read_exact(&mut payload).expect("frame payload");
1239 decode_replica_message(&payload).expect("decodable replica frame")
1240 }
1241
1242 pub(super) fn wait_for_ack(stream: &mut TcpStream, seq: u64) {
1244 loop {
1245 if let ReplicaMessage::Ack(a) = read_replica_msg(stream)
1246 && a.acked_sequence >= seq
1247 {
1248 return;
1249 }
1250 }
1251 }
1252
1253 pub(super) fn accept_within(listener: &std::net::TcpListener, secs: u64) -> TcpStream {
1256 let deadline = Instant::now() + Duration::from_secs(secs);
1257 loop {
1258 match listener.accept() {
1259 Ok((s, _)) => {
1260 s.set_read_timeout(Some(Duration::from_secs(10)))
1261 .expect("read timeout");
1262 return s;
1263 }
1264 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
1265 assert!(Instant::now() < deadline, "replica never connected");
1266 std::thread::sleep(Duration::from_millis(10));
1267 }
1268 Err(e) => panic!("accept failed: {e}"),
1269 }
1270 }
1271 }
1272 }
1273
1274 #[cfg(all(feature = "hash-chain", not(feature = "no-persist")))]
1280 mod divergence_resync {
1281 use super::super::super::auth::authenticate_replica;
1282 use super::scripted::*;
1283 use super::*;
1284 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
1285 use melin_transport_core::cursors::WireSeq;
1286 use melin_transport_core::replication::catchup::{lineage_origin, snapshot_transfer_with};
1287 use melin_transport_core::replication::protocol::{
1288 encode_hash_mismatch, encode_rotate, encode_stream_start,
1289 };
1290 use std::sync::Arc;
1291 use std::sync::atomic::AtomicBool;
1292
1293 #[test]
1302 fn mid_stream_divergence_resyncs_in_process() {
1303 let dir = tempfile::tempdir().expect("tempdir");
1304
1305 let primary_journal = dir.path().join("primary.journal");
1309 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1310 let mut chain_at_4 = [0u8; 32];
1311 for v in 1..=5u64 {
1312 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1313 if v == 2 {
1314 w.rotate_segment().expect("rotate");
1315 }
1316 if v == 4 {
1317 chain_at_4 = w.chain_hash().expect("chain");
1318 }
1319 }
1320 drop(w);
1321 melin_transport_core::snapshot::save::<App>(
1322 &App,
1323 WireSeq::new(4),
1324 chain_at_4,
1325 0,
1326 &primary_journal.with_extension("snapshot"),
1327 )
1328 .expect("save snapshot");
1329 let (lineage_start, lineage_anchor) =
1330 lineage_origin(&primary_journal).expect("lineage");
1331
1332 let authorized_keys = replica_auth();
1334
1335 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1336 listener.set_nonblocking(true).expect("nonblocking");
1337 let addr = listener.local_addr().expect("addr");
1338
1339 let replica_journal = dir.path().join("replica.journal");
1341 let replica_snapshot = dir.path().join("replica.snapshot");
1342 let shutdown = Arc::new(AtomicBool::new(false));
1343 let control = crate::replication::ReplicaControlPlane::new();
1344 let cores = unpinned_cores();
1345 let replica = {
1346 let journal = replica_journal.clone();
1347 let shutdown = Arc::clone(&shutdown);
1348 let control = control.clone();
1349 std::thread::spawn(move || -> Result<bool, String> {
1350 run_receiver::<App>(
1351 addr,
1352 &journal,
1353 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1354 &shutdown,
1355 &control,
1356 3_600_000,
1357 replica_snapshot,
1358 cores,
1359 Duration::ZERO,
1360 64,
1361 false,
1362 Arc::new(Factory),
1363 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1364 )
1365 .map(|state| state.is_none())
1367 .map_err(|e| e.to_string())
1368 })
1369 };
1370
1371 let mut s1 = accept_within(&listener, 30);
1374 let mut s1r = s1.try_clone().expect("clone");
1375 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1376 match read_replica_msg(&mut s1) {
1377 ReplicaMessage::Handshake(h) => {
1378 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
1379 }
1380 other => panic!("expected Handshake, got {other:?}"),
1381 }
1382 let mut buf = Vec::new();
1383 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
1384 s1.write_all(&buf).expect("StreamStart 1");
1385 buf.clear();
1386 melin_transport_core::replication_wire::encode_input_batch(
1387 &[InputSlot::<EvtAdd> {
1388 connection_id: 0,
1389 key_hash: 0,
1390 request_seq: 0,
1391 sequence: 1,
1392 timestamp_ns: 1,
1393 event: JournalEvent::App(EvtAdd(1)),
1394 publish_ts: Default::default(),
1396 recv_ts: Default::default(),
1397 }],
1398 &mut buf,
1399 );
1400 s1.write_all(&buf).expect("InputBatch");
1401 wait_for_ack(&mut s1, 1);
1402
1403 buf.clear();
1406 encode_rotate(1, &[0xEE; 32], &mut buf);
1407 s1.write_all(&buf).expect("Rotate");
1408
1409 let mut s2 = accept_within(&listener, 30);
1412 let mut s2r = s2.try_clone().expect("clone");
1413 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1414 match read_replica_msg(&mut s2) {
1415 ReplicaMessage::Handshake(h) => assert_eq!(
1416 h.last_sequence, 1,
1417 "post-divergence handshake must carry the recovered journal position"
1418 ),
1419 other => panic!("expected Handshake, got {other:?}"),
1420 }
1421 assert!(
1427 !control.pipeline_healthy.load(Ordering::Acquire),
1428 "the divergence must have latched the health mirror false"
1429 );
1430 buf.clear();
1431 encode_hash_mismatch(&mut buf);
1432 s2.write_all(&buf).expect("HashMismatch");
1433 let transfer_shutdown = AtomicBool::new(false);
1434 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1435 s2.write_all(b)?;
1436 s2.flush()
1437 };
1438 let end = snapshot_transfer_with::<EvtAdd>(
1441 &primary_journal,
1442 &mut publish,
1443 &transfer_shutdown,
1444 1,
1445 )
1446 .expect("snapshot transfer");
1447 assert_eq!(
1448 end,
1449 melin_transport_core::replication::catchup::CatchUpResult::Ok(5)
1450 );
1451 let mut s2_acks = s2.try_clone().expect("clone");
1452 wait_for_ack(&mut s2_acks, 5);
1453
1454 assert!(
1457 control.pipeline_healthy.load(Ordering::Acquire),
1458 "the resync rebuild must reset the health mirror true"
1459 );
1460
1461 shutdown.store(true, Ordering::Relaxed);
1464 let result = replica.join().expect("replica thread panicked");
1465 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
1466
1467 assert!(
1470 dir.path().join("replica.journal.divergent.0").exists(),
1471 "divergent lineage must be archived"
1472 );
1473 assert!(replica_journal.exists(), "re-seeded live journal");
1474 }
1475
1476 #[test]
1490 fn resync_transfer_failure_rebuilds_clean_not_over_stale_writer() {
1491 let dir = tempfile::tempdir().expect("tempdir");
1492
1493 let primary_journal = dir.path().join("primary.journal");
1497 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1498 for v in 1..=2u64 {
1499 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1500 }
1501 drop(w);
1502 let (lineage_start, lineage_anchor) =
1503 lineage_origin(&primary_journal).expect("lineage");
1504
1505 let authorized_keys = replica_auth();
1506
1507 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1508 listener.set_nonblocking(true).expect("nonblocking");
1509 let addr = listener.local_addr().expect("addr");
1510
1511 let replica_journal = dir.path().join("replica.journal");
1512 let replica_snapshot = dir.path().join("replica.snapshot");
1513 let shutdown = Arc::new(AtomicBool::new(false));
1514 let control = crate::replication::ReplicaControlPlane::new();
1515 let cores = unpinned_cores();
1516 let replica = {
1517 let journal = replica_journal.clone();
1518 let shutdown = Arc::clone(&shutdown);
1519 let control = control.clone();
1520 std::thread::spawn(move || -> Result<bool, String> {
1521 run_receiver::<App>(
1522 addr,
1523 &journal,
1524 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1525 &shutdown,
1526 &control,
1527 3_600_000,
1528 replica_snapshot,
1529 cores,
1530 Duration::ZERO,
1531 64,
1532 false,
1533 Arc::new(Factory),
1534 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1535 )
1536 .map(|state| state.is_none())
1537 .map_err(|e| e.to_string())
1538 })
1539 };
1540
1541 let stream_one = |s: &mut TcpStream, buf: &mut Vec<u8>| {
1544 buf.clear();
1545 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, buf);
1546 s.write_all(buf).expect("StreamStart");
1547 buf.clear();
1548 melin_transport_core::replication_wire::encode_input_batch(
1549 &[InputSlot::<EvtAdd> {
1550 connection_id: 0,
1551 key_hash: 0,
1552 request_seq: 0,
1553 sequence: 1,
1554 timestamp_ns: 1,
1555 event: JournalEvent::App(EvtAdd(1)),
1556 publish_ts: Default::default(),
1557 recv_ts: Default::default(),
1558 }],
1559 buf,
1560 );
1561 s.write_all(buf).expect("InputBatch");
1562 };
1563
1564 let mut buf = Vec::new();
1569 let mut s1 = accept_within(&listener, 30);
1570 let mut s1r = s1.try_clone().expect("clone");
1571 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1572 match read_replica_msg(&mut s1) {
1573 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0, "fresh"),
1574 other => panic!("expected Handshake, got {other:?}"),
1575 }
1576 stream_one(&mut s1, &mut buf);
1577 wait_for_ack(&mut s1, 1);
1578 buf.clear();
1579 encode_rotate(1, &[0xEE; 32], &mut buf);
1580 s1.write_all(&buf).expect("Rotate");
1581
1582 let mut s2 = accept_within(&listener, 30);
1587 let mut s2r = s2.try_clone().expect("clone");
1588 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1589 match read_replica_msg(&mut s2) {
1590 ReplicaMessage::Handshake(h) => assert_eq!(
1591 h.last_sequence, 1,
1592 "reconnect carries the recovered forked position"
1593 ),
1594 other => panic!("expected Handshake, got {other:?}"),
1595 }
1596 buf.clear();
1597 encode_hash_mismatch(&mut buf);
1598 s2.write_all(&buf).expect("HashMismatch");
1599 s2.flush().expect("flush");
1600 drop(s2);
1603 drop(s2r);
1604
1605 let mut s3 = accept_within(&listener, 30);
1610 let mut s3r = s3.try_clone().expect("clone");
1611 authenticate_replica(&mut s3r, &authorized_keys).expect("auth 3");
1612 match read_replica_msg(&mut s3) {
1613 ReplicaMessage::Handshake(h) => assert_eq!(
1614 h.last_sequence, 0,
1615 "after a failed transfer the retry handshakes as fresh"
1616 ),
1617 other => panic!("expected Handshake, got {other:?}"),
1618 }
1619 stream_one(&mut s3, &mut buf);
1620 wait_for_ack(&mut s3, 1);
1621
1622 shutdown.store(true, Ordering::Relaxed);
1623 let result = replica.join().expect("replica thread panicked");
1624 assert_eq!(result, Ok(true), "receiver exits cleanly via shutdown");
1625
1626 assert!(
1631 dir.path().join("replica.journal.divergent.0").exists(),
1632 "forked lineage archived, never deleted"
1633 );
1634 assert!(
1635 replica_journal.exists(),
1636 "fresh live journal recreated after the failed transfer"
1637 );
1638 }
1639
1640 #[test]
1647 fn second_mid_stream_divergence_exits_hard() {
1648 let dir = tempfile::tempdir().expect("tempdir");
1649
1650 let primary_journal = dir.path().join("primary.journal");
1651 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1652 let mut chain_at_4 = [0u8; 32];
1653 for v in 1..=5u64 {
1654 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1655 if v == 2 {
1656 w.rotate_segment().expect("rotate");
1657 }
1658 if v == 4 {
1659 chain_at_4 = w.chain_hash().expect("chain");
1660 }
1661 }
1662 drop(w);
1663 melin_transport_core::snapshot::save::<App>(
1664 &App,
1665 WireSeq::new(4),
1666 chain_at_4,
1667 0,
1668 &primary_journal.with_extension("snapshot"),
1669 )
1670 .expect("save snapshot");
1671 let (lineage_start, lineage_anchor) =
1672 lineage_origin(&primary_journal).expect("lineage");
1673
1674 let authorized_keys = replica_auth();
1675
1676 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1677 listener.set_nonblocking(true).expect("nonblocking");
1678 let addr = listener.local_addr().expect("addr");
1679
1680 let replica_journal = dir.path().join("replica.journal");
1681 let replica_snapshot = dir.path().join("replica.snapshot");
1682 let shutdown = Arc::new(AtomicBool::new(false));
1683 let control = crate::replication::ReplicaControlPlane::new();
1684 let cores = unpinned_cores();
1685 let replica = {
1686 let journal = replica_journal.clone();
1687 let shutdown = Arc::clone(&shutdown);
1688 let control = control.clone();
1689 std::thread::spawn(move || -> Result<bool, String> {
1690 run_receiver::<App>(
1691 addr,
1692 &journal,
1693 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1694 &shutdown,
1695 &control,
1696 3_600_000,
1697 replica_snapshot,
1698 cores,
1699 Duration::ZERO,
1700 64,
1701 false,
1702 Arc::new(Factory),
1703 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1704 )
1705 .map(|state| state.is_none())
1706 .map_err(|e| e.to_string())
1707 })
1708 };
1709
1710 let mut s1 = accept_within(&listener, 30);
1712 let mut s1r = s1.try_clone().expect("clone");
1713 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1714 match read_replica_msg(&mut s1) {
1715 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0),
1716 other => panic!("expected Handshake, got {other:?}"),
1717 }
1718 let mut buf = Vec::new();
1719 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
1720 s1.write_all(&buf).expect("StreamStart 1");
1721 buf.clear();
1722 melin_transport_core::replication_wire::encode_input_batch(
1723 &[InputSlot::<EvtAdd> {
1724 connection_id: 0,
1725 key_hash: 0,
1726 request_seq: 0,
1727 sequence: 1,
1728 timestamp_ns: 1,
1729 event: JournalEvent::App(EvtAdd(1)),
1730 publish_ts: Default::default(),
1731 recv_ts: Default::default(),
1732 }],
1733 &mut buf,
1734 );
1735 s1.write_all(&buf).expect("InputBatch");
1736 wait_for_ack(&mut s1, 1);
1737 buf.clear();
1738 encode_rotate(1, &[0xEE; 32], &mut buf);
1739 s1.write_all(&buf).expect("Rotate 1");
1740
1741 let mut s2 = accept_within(&listener, 30);
1744 let mut s2r = s2.try_clone().expect("clone");
1745 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1746 match read_replica_msg(&mut s2) {
1747 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 1),
1748 other => panic!("expected Handshake, got {other:?}"),
1749 }
1750 buf.clear();
1751 encode_hash_mismatch(&mut buf);
1752 s2.write_all(&buf).expect("HashMismatch");
1753 let transfer_shutdown = AtomicBool::new(false);
1754 {
1755 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1756 s2.write_all(b)?;
1757 s2.flush()
1758 };
1759 snapshot_transfer_with::<EvtAdd>(
1760 &primary_journal,
1761 &mut publish,
1762 &transfer_shutdown,
1763 1,
1764 )
1765 .expect("snapshot transfer");
1766 }
1767 buf.clear();
1768 melin_transport_core::replication_wire::encode_input_batch(
1769 &[InputSlot::<EvtAdd> {
1770 connection_id: 0,
1771 key_hash: 0,
1772 request_seq: 0,
1773 sequence: 6,
1774 timestamp_ns: 6,
1775 event: JournalEvent::App(EvtAdd(6)),
1776 publish_ts: Default::default(),
1777 recv_ts: Default::default(),
1778 }],
1779 &mut buf,
1780 );
1781 s2.write_all(&buf).expect("InputBatch 6");
1782 let mut s2_acks = s2.try_clone().expect("clone");
1783 wait_for_ack(&mut s2_acks, 6);
1784 buf.clear();
1785 encode_rotate(6, &[0xEE; 32], &mut buf);
1786 s2.write_all(&buf).expect("Rotate 2");
1787
1788 let result = replica.join().expect("replica thread panicked");
1791 let err = result.expect_err("second divergence must exit hard");
1792 assert!(
1793 err.contains("divergence recurred"),
1794 "error must name the recurrence: {err}"
1795 );
1796 }
1797
1798 type PromoteHandles =
1807 crate::replication::ReplicaPipelineHandles<App, BufferedWriter<EvtAdd>>;
1808
1809 #[test]
1810 fn promotion_hands_back_local_state_when_present() {
1811 let dir = tempfile::tempdir().unwrap();
1812 let mut pipeline: Option<PromoteHandles> = None;
1813 let mut exchange = Some(App);
1814 let mut journal_writer =
1815 Some(BufferedWriter::<EvtAdd>::create(&dir.path().join("p.journal")).unwrap());
1816
1817 let result = crate::replication::take_pipeline_for_promotion(
1818 &mut pipeline,
1819 &mut exchange,
1820 &mut journal_writer,
1821 );
1822
1823 assert!(matches!(result, Ok(Some(_))), "warm state must be promoted");
1824 assert!(
1825 exchange.is_none() && journal_writer.is_none(),
1826 "state must be moved into the result"
1827 );
1828 }
1829
1830 #[test]
1831 fn promotion_errs_when_no_local_state() {
1832 let mut pipeline: Option<PromoteHandles> = None;
1833 let mut exchange: Option<App> = None;
1834 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
1835
1836 let result = crate::replication::take_pipeline_for_promotion(
1837 &mut pipeline,
1838 &mut exchange,
1839 &mut journal_writer,
1840 );
1841
1842 let err = match result {
1844 Err(e) => e,
1845 Ok(_) => panic!("a promote with nothing to promote must error"),
1846 };
1847 assert!(
1848 err.to_string().contains("no local state available"),
1849 "{err}"
1850 );
1851 }
1852
1853 #[test]
1854 fn promotion_errs_on_partial_state() {
1855 let mut pipeline: Option<PromoteHandles> = None;
1857 let mut exchange = Some(App);
1858 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
1859
1860 let result = crate::replication::take_pipeline_for_promotion(
1861 &mut pipeline,
1862 &mut exchange,
1863 &mut journal_writer,
1864 );
1865
1866 assert!(result.is_err(), "partial state is not a valid promotion");
1867 }
1868 }
1869
1870 #[cfg(not(feature = "no-persist"))]
1876 mod backoff_shutdown {
1877 use super::super::super::auth::authenticate_replica;
1878 use super::scripted::*;
1879 use super::*;
1880 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
1881 use melin_transport_core::replication::catchup::lineage_origin;
1882 use melin_transport_core::replication::protocol::encode_stream_start;
1883 use std::sync::Arc;
1884 use std::sync::atomic::AtomicBool;
1885
1886 fn live_thread_names() -> Vec<String> {
1889 std::fs::read_dir("/proc/self/task")
1890 .expect("read /proc/self/task")
1891 .filter_map(|entry| {
1892 let entry = entry.ok()?;
1895 let name = std::fs::read_to_string(entry.path().join("comm")).ok()?;
1896 Some(name.trim().to_string())
1897 })
1898 .collect()
1899 }
1900
1901 #[test]
1907 fn shutdown_during_reconnect_backoff_joins_pipeline_threads() {
1908 let dir = tempfile::tempdir().expect("tempdir");
1909
1910 let primary_journal = dir.path().join("primary.journal");
1913 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1914 w.append(&JournalEvent::App(EvtAdd(1))).expect("append");
1915 drop(w);
1916 let (lineage_start, lineage_anchor) =
1917 lineage_origin(&primary_journal).expect("lineage");
1918
1919 let authorized_keys = replica_auth();
1920 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1921 listener.set_nonblocking(true).expect("nonblocking");
1922 let addr = listener.local_addr().expect("addr");
1923
1924 let shutdown = Arc::new(AtomicBool::new(false));
1925 let control = crate::replication::ReplicaControlPlane::new();
1926 let replica = {
1927 let journal = dir.path().join("replica.journal");
1928 let snapshot = dir.path().join("replica.snapshot");
1929 let shutdown = Arc::clone(&shutdown);
1930 let control = control.clone();
1931 std::thread::spawn(move || -> Result<bool, String> {
1932 run_receiver::<App>(
1933 addr,
1934 &journal,
1935 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1936 &shutdown,
1937 &control,
1938 3_600_000,
1939 snapshot,
1940 unpinned_cores(),
1941 Duration::ZERO,
1942 64,
1943 false,
1944 Arc::new(Factory),
1945 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1946 )
1947 .map(|state| state.is_none())
1949 .map_err(|e| e.to_string())
1950 })
1951 };
1952
1953 let mut s1 = accept_within(&listener, 30);
1957 let mut s1r = s1.try_clone().expect("clone");
1958 authenticate_replica(&mut s1r, &authorized_keys).expect("auth");
1959 match read_replica_msg(&mut s1) {
1960 ReplicaMessage::Handshake(h) => {
1961 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
1962 }
1963 other => panic!("expected Handshake, got {other:?}"),
1964 }
1965 let mut buf = Vec::new();
1966 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
1967 s1.write_all(&buf).expect("StreamStart");
1968 buf.clear();
1969 melin_transport_core::replication_wire::encode_input_batch(
1970 &[InputSlot::<EvtAdd> {
1971 connection_id: 0,
1972 key_hash: 0,
1973 request_seq: 0,
1974 sequence: 1,
1975 timestamp_ns: 1,
1976 event: JournalEvent::App(EvtAdd(1)),
1977 publish_ts: Default::default(),
1978 recv_ts: Default::default(),
1979 }],
1980 &mut buf,
1981 );
1982 s1.write_all(&buf).expect("InputBatch");
1983 wait_for_ack(&mut s1, 1);
1984
1985 drop(s1);
1989 drop(s1r);
1990 drop(listener);
1991 std::thread::sleep(Duration::from_millis(1500));
1996
1997 shutdown.store(true, Ordering::Relaxed);
1998 let deadline = Instant::now() + Duration::from_secs(10);
1999 while !replica.is_finished() {
2000 assert!(
2001 Instant::now() < deadline,
2002 "receiver did not exit after shutdown"
2003 );
2004 std::thread::sleep(Duration::from_millis(20));
2005 }
2006 let result = replica.join().expect("replica thread panicked");
2007 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
2008
2009 let deadline = Instant::now() + Duration::from_secs(10);
2015 loop {
2016 let names = live_thread_names();
2017 if !names
2018 .iter()
2019 .any(|n| n == "journal" || n == "matching" || n == "drain")
2020 {
2021 break;
2022 }
2023 assert!(
2024 Instant::now() < deadline,
2025 "pipeline threads still alive after shutdown: {names:?}"
2026 );
2027 std::thread::sleep(Duration::from_millis(50));
2028 }
2029 }
2030 }
2031}