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::JournalWrite;
19use melin_transport_core::pipeline::{JournalStage, JournalStageRun};
20
21use super::auth::authenticate_with_primary;
22use super::receiver_transport::{
23 ControlFrameSource, ReceiverTransport, SessionExit, streaming_loop,
24};
25use super::{
26 AfterSession, ReplicaPipelineHandles, ResyncDecision, build_replica_pipeline_with_threads,
27 handle_resync_verdict, handle_session_exit, recover_replica_state, sleep_then_double_backoff,
28 take_pipeline_for_promotion, teardown_replica_pipeline,
29};
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]
40fn arm_tcp_quickack(fd: RawFd) {
41 let on: libc::c_int = 1;
42 let rc = unsafe {
46 libc::setsockopt(
47 fd,
48 libc::IPPROTO_TCP,
49 libc::TCP_QUICKACK,
50 &on as *const libc::c_int as *const libc::c_void,
51 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
52 )
53 };
54 if rc != 0 {
55 let _ = rc;
56 }
57}
58
59const TOKEN_RECV: u64 = 0;
64const TOKEN_SEND: u64 = 1;
65const TOKEN_PROVIDE: u64 = 2;
66
67const NUM_RECV_BUFFERS: u16 = 16;
69const RECV_BUF_SIZE: usize = MAX_DATA_FRAME;
70const RECV_BUF_GROUP_ID: u16 = 0;
71
72const IORING_CQE_F_BUFFER: u32 = 1 << 0;
74const IORING_CQE_F_MORE: u32 = 1 << 1;
75const IORING_CQE_BUFFER_SHIFT: u32 = 16;
76
77struct UringTransport {
83 ring: io_uring::IoUring,
84 tcp_fd: RawFd,
85 _recv_pool: Vec<u8>,
89 pool_ptr: *mut u8,
90 multishot_active: bool,
91 connected: bool,
92 ack_buf: Vec<u8>,
93 ack_offset: usize,
94 ack_in_flight: bool,
95 pending_ack: Option<Ack>,
105}
106
107impl UringTransport {
108 fn new(tcp_stream: &TcpStream) -> io::Result<Self> {
109 use io_uring::{IoUring, opcode, types};
110 use std::os::unix::io::AsRawFd;
111
112 let tcp_fd = tcp_stream.as_raw_fd();
113
114 let mut ring: IoUring = IoUring::builder()
115 .setup_single_issuer()
116 .build(64)
117 .map_err(|e| io::Error::other(format!("io_uring init failed: {e}")))?;
118
119 ring.submitter()
120 .register_files(&[tcp_fd])
121 .map_err(|e| io::Error::other(format!("io_uring register_files failed: {e}")))?;
122
123 arm_tcp_quickack(tcp_fd);
124
125 {
127 let mut cpuset: libc::cpu_set_t = unsafe { std::mem::zeroed() };
128 unsafe { libc::CPU_SET(0, &mut cpuset) };
129 let _ = ring.submitter().register_iowq_aff(&cpuset);
130 }
131
132 let mut recv_pool: Vec<u8> = vec![0u8; NUM_RECV_BUFFERS as usize * RECV_BUF_SIZE];
133 let pool_ptr = recv_pool.as_mut_ptr();
134
135 {
137 let sqe = opcode::ProvideBuffers::new(
138 pool_ptr,
139 RECV_BUF_SIZE as i32,
140 NUM_RECV_BUFFERS,
141 RECV_BUF_GROUP_ID,
142 0,
143 )
144 .build()
145 .user_data(TOKEN_PROVIDE);
146 unsafe { ring.submission().push(&sqe).expect("SQ full") };
147 ring.submit_and_wait(1)?;
148 let cqe = ring
149 .completion()
150 .next()
151 .ok_or_else(|| io::Error::other("no cqe after ProvideBuffers"))?;
152 if cqe.result() < 0 {
153 return Err(io::Error::other(format!(
154 "ProvideBuffers failed: {}",
155 cqe.result()
156 )));
157 }
158 }
159
160 let sqe = opcode::RecvMulti::new(types::Fd(tcp_fd), RECV_BUF_GROUP_ID)
162 .build()
163 .user_data(TOKEN_RECV);
164 unsafe { ring.submission().push(&sqe).expect("SQ full") };
165
166 Ok(UringTransport {
167 ring,
168 tcp_fd,
169 _recv_pool: recv_pool,
170 pool_ptr,
171 multishot_active: true,
172 connected: true,
173 ack_buf: Vec::with_capacity(64),
174 ack_offset: 0,
175 ack_in_flight: false,
176 pending_ack: None,
177 })
178 }
179
180 fn push_ack_send(&mut self, ack: &Ack) {
186 use io_uring::{opcode, types};
187
188 debug_assert!(!self.ack_in_flight);
189 self.ack_buf.clear();
190 encode_ack(ack, &mut self.ack_buf);
191 let sqe = opcode::Send::new(
192 types::Fixed(0),
193 self.ack_buf.as_ptr(),
194 self.ack_buf.len() as u32,
195 )
196 .build()
197 .user_data(TOKEN_SEND);
198 unsafe { self.ring.submission().push(&sqe).expect("SQ full") };
202 self.ack_in_flight = true;
203 self.ack_offset = 0;
204 }
205}
206
207impl ReceiverTransport for UringTransport {
208 fn poll_recv(&mut self, recv_buf: &mut Vec<u8>) -> io::Result<bool> {
209 use io_uring::{opcode, types};
210
211 if !self.connected {
212 return Err(io::Error::other("not connected"));
213 }
214
215 let pending = self.ring.submission().len();
218 if pending > 0 {
219 self.ring.submit()?;
220 }
221
222 let mut cqes: [(u64, i32, u32); 16] = [(0, 0, 0); 16];
224 let mut cqe_count = 0;
225 {
226 let mut cq = self.ring.completion();
227 while cqe_count < cqes.len() {
228 match cq.next() {
229 Some(cqe) => {
230 cqes[cqe_count] = (cqe.user_data(), cqe.result(), cqe.flags());
231 cqe_count += 1;
232 }
233 None => break,
234 }
235 }
236 }
237
238 let mut any_recv = false;
239 let mut submit_chained_ack = false;
243
244 for &(token, result, flags) in &cqes[..cqe_count] {
245 match token {
246 TOKEN_RECV => {
247 if (flags & IORING_CQE_F_MORE) == 0 {
248 self.multishot_active = false;
249 }
250 if result < 0 {
251 if result == -libc::ENOBUFS {
252 tracing::debug!("recv multishot ENOBUFS — pool exhausted");
253 continue;
254 }
255 self.connected = false;
256 return Err(io::Error::other(format!(
257 "primary disconnected (recv returned {result})"
258 )));
259 }
260 if result == 0 {
261 self.connected = false;
262 return Err(io::Error::other("primary disconnected (recv returned 0)"));
263 }
264 if (flags & IORING_CQE_F_BUFFER) == 0 {
265 self.connected = false;
266 return Err(io::Error::other("recv cqe missing F_BUFFER flag"));
267 }
268
269 arm_tcp_quickack(self.tcp_fd);
270 let n = result as usize;
271 let buf_id = (flags >> IORING_CQE_BUFFER_SHIFT) as usize;
272 let buf_ptr = unsafe { self.pool_ptr.add(buf_id * RECV_BUF_SIZE) };
275 let slice = unsafe { std::slice::from_raw_parts(buf_ptr, n) };
276 recv_buf.extend_from_slice(slice);
277 any_recv = true;
278
279 let provide_sqe = opcode::ProvideBuffers::new(
281 buf_ptr,
282 RECV_BUF_SIZE as i32,
283 1,
284 RECV_BUF_GROUP_ID,
285 buf_id as u16,
286 )
287 .build()
288 .user_data(TOKEN_PROVIDE);
289 unsafe { self.ring.submission().push(&provide_sqe).expect("SQ full") };
290 }
291
292 TOKEN_PROVIDE if result < 0 => {
293 tracing::debug!("ProvideBuffers re-provision failed: {result}");
294 }
295
296 TOKEN_SEND => {
297 if result < 0 {
298 self.connected = false;
299 return Err(io::Error::other(format!(
300 "ack send error (returned {result})"
301 )));
302 }
303 let sent = result as usize;
304 self.ack_offset += sent;
305 if self.ack_offset >= self.ack_buf.len() {
306 self.ack_buf.clear();
307 self.ack_offset = 0;
308 self.ack_in_flight = false;
309 if let Some(ack) = self.pending_ack.take() {
316 self.push_ack_send(&ack);
317 submit_chained_ack = true;
318 }
319 } else {
320 let sqe = opcode::Send::new(
322 types::Fixed(0),
323 self.ack_buf[self.ack_offset..].as_ptr(),
324 (self.ack_buf.len() - self.ack_offset) as u32,
325 )
326 .build()
327 .user_data(TOKEN_SEND);
328 unsafe { self.ring.submission().push(&sqe).expect("SQ full") };
329 }
330 }
331
332 _ => {}
333 }
334 }
335
336 if !self.multishot_active {
338 let sqe = opcode::RecvMulti::new(types::Fd(self.tcp_fd), RECV_BUF_GROUP_ID)
339 .build()
340 .user_data(TOKEN_RECV);
341 unsafe { self.ring.submission().push(&sqe).expect("SQ full") };
342 self.multishot_active = true;
343 }
344
345 if submit_chained_ack {
348 self.ring.submit()?;
349 }
350
351 Ok(any_recv)
352 }
353
354 fn send_ack(&mut self, ack: &Ack) -> io::Result<bool> {
355 if self.ack_in_flight {
356 self.pending_ack = Some(*ack);
360 return Ok(true);
361 }
362 self.push_ack_send(ack);
363 Ok(true)
364 }
365
366 fn ack_in_flight(&self) -> bool {
367 self.ack_in_flight || self.pending_ack.is_some()
372 }
373
374 fn is_connected(&mut self) -> bool {
375 self.connected
376 }
377}
378
379struct TcpFrameSource<'a> {
387 reader: &'a mut TcpStream,
388}
389
390impl ControlFrameSource for TcpFrameSource<'_> {
391 fn next_frame(
392 &mut self,
393 max_size: usize,
394 ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
395 Ok(read_frame(self.reader, max_size)?)
396 }
397}
398
399pub type ReceiverResult<A, W> = Result<Option<(A, W)>, Box<dyn std::error::Error>>;
401
402#[allow(clippy::too_many_arguments)]
403pub fn run_receiver<A, W>(
404 primary_addr: SocketAddr,
405 journal_path: &std::path::Path,
406 signing_key: &ed25519_dalek::SigningKey,
407 shutdown: &AtomicBool,
408 control: &super::ReplicaControlPlane,
412 snapshot_interval_ms: u64,
413 snapshot_path: std::path::PathBuf,
414 cores: crate::server::PipelineCores,
415 group_commit_delay: std::time::Duration,
416 pipeline_depth: usize,
417 busy_spin: bool,
418 factory: std::sync::Arc<dyn melin_app::app_factory::AppFactory<App = A>>,
419 fence_state: std::sync::Arc<melin_transport_core::fence::FenceState>,
420) -> ReceiverResult<A, W>
421where
422 A: Application + Send + 'static,
423 A::Event: Send + Sync + 'static,
424 A::Report: Send + 'static,
425 A::QueryResponse: Send + 'static,
426 W: JournalWrite<A::Event> + Send + 'static,
427 JournalStage<A::Event, W>: JournalStageRun<A::Event, Writer = W>,
428{
429 let super::ReplicaControlPlane {
430 promote,
431 tip_ready,
432 journal_tip,
433 primary_link_up,
434 primary_acking_mode,
435 pipeline_healthy,
436 } = control;
437 let (mut exchange, mut journal_writer, mut last_sequence, mut chain_hash) =
441 recover_replica_state::<A, W>(
442 journal_path,
443 &snapshot_path,
444 factory.as_ref(),
445 &fence_state,
446 )?;
447 journal_tip.advance(melin_transport_core::WireSeq::new(last_sequence));
453 tip_ready.store(true, Ordering::Release);
454
455 let mut backoff = std::time::Duration::from_secs(1);
456
457 let mut divergence_resyncs: u32 = 0;
460
461 let mut send_buf = Vec::with_capacity(64);
462 let mut pipeline: Option<ReplicaPipelineHandles<A, W>> = None;
463
464 loop {
466 primary_link_up.store(false, Ordering::Release);
469
470 if let Some(p) = pipeline.as_ref() {
471 if let Some(ref lock) = p.chain_hash_lock {
480 let fsync_state = lock.load();
481 last_sequence = fsync_state.journal_seq.get();
482 chain_hash = fsync_state.chain_hash;
483 } else {
484 last_sequence = p.last_seq.load().get();
485 }
486 }
487
488 if shutdown.load(Ordering::Relaxed) {
493 if let Some(p) = pipeline.take() {
494 let _ = teardown_replica_pipeline::<A, W>(p);
495 }
496 return Ok(None);
497 }
498 if promote.is_requested() {
499 info!("promotion triggered while disconnected");
500 return take_pipeline_for_promotion(&mut pipeline, &mut exchange, &mut journal_writer);
501 }
502
503 info!(primary = %primary_addr, "connecting to primary as replica");
505
506 let stream = match TcpStream::connect(primary_addr) {
507 Ok(s) => s,
508 Err(e) => {
509 warn!(
510 error = %e,
511 backoff_secs = backoff.as_secs(),
512 "failed to connect to primary — retrying"
513 );
514 sleep_then_double_backoff(&mut backoff, shutdown, promote);
515 continue;
516 }
517 };
518 if let Err(e) = stream.set_nodelay(true) {
519 warn!(error = %e, "failed to set TCP_NODELAY on replica receive socket");
520 }
521 {
522 use std::os::unix::io::AsRawFd;
523 let busy_poll: libc::c_int = 50;
524 let rc = unsafe {
525 libc::setsockopt(
526 stream.as_raw_fd(),
527 libc::SOL_SOCKET,
528 libc::SO_BUSY_POLL,
529 &busy_poll as *const libc::c_int as *const libc::c_void,
530 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
531 )
532 };
533 if rc != 0 {
534 let err = std::io::Error::last_os_error();
535 warn!(error = %err, "failed to set SO_BUSY_POLL on replica receive socket");
536 }
537 }
538 stream.set_read_timeout(Some(std::time::Duration::from_secs(5)))?;
539
540 let mut reader = stream.try_clone()?;
541 let mut tcp_writer = stream;
542
543 if let Err(e) = authenticate_with_primary(&mut reader, signing_key) {
546 if matches!(
553 e.kind(),
554 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
555 ) {
556 warn!(error = %e, "no auth challenge from primary (not serving yet?) — retrying");
557 } else {
558 warn!(error = %e, "authentication failed — retrying");
559 }
560 drop(reader);
565 drop(tcp_writer);
566 sleep_then_double_backoff(&mut backoff, shutdown, promote);
569 continue;
570 }
571 info!("authenticated with primary");
572 primary_link_up.store(true, Ordering::Release);
575
576 let handshake = Handshake {
581 last_sequence,
582 chain_hash,
583 epoch: fence_state.epoch(),
584 };
585 send_buf.clear();
586 encode_handshake(&handshake, &mut send_buf);
587 tcp_writer.write_all(&send_buf)?;
588 tcp_writer.flush()?;
589 send_buf.clear();
590
591 let response_frame = read_frame(&mut reader, MAX_CONTROL_FRAME)?;
598 let response = decode_primary_message(&response_frame)?;
599 let (stream_lineage, session_start) = match response {
600 PrimaryMessage::StreamStart {
601 start_sequence,
602 segment_start_sequence,
603 anchor_hash,
604 epoch,
605 durability_mode,
606 } => {
607 let our_epoch = fence_state.epoch();
615 if fence_state.refuses_primary(epoch) {
616 warn!(
617 primary_epoch = epoch,
618 our_epoch,
619 "primary is behind our fencing epoch — refusing to follow stale primary"
620 );
621 drop(reader);
624 drop(tcp_writer);
625 sleep_then_double_backoff(&mut backoff, shutdown, promote);
626 continue;
627 }
628 fence_state.observe_epoch(epoch);
632 primary_acking_mode.store(durability_mode, Ordering::Release);
633 info!(start_sequence, epoch, durability_mode, "streaming started");
634 ((segment_start_sequence, anchor_hash), last_sequence)
635 }
636 ref resync @ (PrimaryMessage::NeedSnapshot | PrimaryMessage::HashMismatch) => {
637 let divergent = matches!(resync, PrimaryMessage::HashMismatch);
638 let decision = handle_resync_verdict(
642 divergent,
643 &mut TcpFrameSource {
644 reader: &mut reader,
645 },
646 &mut pipeline,
647 &mut exchange,
648 &mut journal_writer,
649 journal_path,
650 &snapshot_path,
651 &fence_state,
652 control,
653 &mut last_sequence,
654 &mut chain_hash,
655 )
656 .map_err(|e| -> Box<dyn std::error::Error> { e })?;
657 match decision {
658 ResyncDecision::Ready {
659 segment_start_sequence,
660 anchor_hash,
661 resume_sequence,
662 } => ((segment_start_sequence, anchor_hash), resume_sequence),
663 ResyncDecision::Retry => {
664 sleep_then_double_backoff(&mut backoff, shutdown, promote);
665 continue;
666 }
667 }
668 }
669 _ => {
670 return Err(format!("unexpected response: {response:?}").into());
671 }
672 };
673
674 if pipeline.is_none() && journal_writer.is_none() {
682 let (lineage_start, lineage_anchor) = stream_lineage;
683 let writer = W::create_continuing(journal_path, lineage_start, lineage_anchor)?;
684 let mut fresh = factory.empty();
685 factory.apply_operator_policy(&mut fresh);
686 exchange = Some(fresh);
687 journal_writer = Some(writer);
688 }
689
690 if pipeline.is_none() {
692 let cur_exchange = exchange.take().expect("exchange initialized");
693 let cur_writer = journal_writer.take().expect("journal_writer initialized");
694 pipeline = Some(build_replica_pipeline_with_threads::<A, W>(
695 cur_exchange,
696 cur_writer,
697 cores,
698 snapshot_interval_ms,
699 snapshot_path.clone(),
700 group_commit_delay,
701 busy_spin,
702 Arc::clone(&fence_state),
703 Arc::clone(pipeline_healthy),
704 )?);
705 }
706
707 let result = {
709 let p = pipeline.as_mut().expect("pipeline must exist by here");
710 let input_producer = &mut p.input_producer;
711 let journal_cursor = p.journal_cursor.as_ref();
712 let stream_marks = &p.stream_marks;
713 let journal_failed = &p.journal_failed;
714 std::thread::scope(|s| {
715 let handle = std::thread::Builder::new()
716 .name("replica-receiver".into())
717 .spawn_scoped(s, || {
718 melin_app::affinity::pin_thread("replica-receiver", cores.reader);
719 let mut transport = match UringTransport::new(&tcp_writer) {
720 Ok(t) => t,
721 Err(e) => {
722 tracing::error!(error = %e, "UringTransport init failed");
723 return super::receiver_transport::StreamingResult {
729 exit: SessionExit::Disconnected,
730 heard_from_primary: false,
731 };
732 }
733 };
734 streaming_loop::<UringTransport, A::Event>(
735 &mut transport,
736 input_producer,
737 journal_cursor,
738 shutdown,
739 control,
740 pipeline_depth,
741 busy_spin,
742 session_start,
743 Vec::with_capacity(MAX_DATA_FRAME + 4),
744 None,
745 stream_marks,
746 journal_failed,
747 )
748 })
749 .expect("spawn replica-receiver thread");
750 handle.join().expect("replica-receiver thread panicked")
751 })
752 };
753
754 primary_link_up.store(false, Ordering::Release);
755 match handle_session_exit(
756 result,
757 &mut pipeline,
758 &mut divergence_resyncs,
759 &mut backoff,
760 last_sequence,
761 journal_path,
762 &snapshot_path,
763 factory.as_ref(),
764 &fence_state,
765 shutdown,
766 promote,
767 || {},
771 ) {
772 AfterSession::Return(r) => return r,
773 AfterSession::Resync {
774 exchange: ex,
775 journal_writer: wr,
776 last_sequence: seq,
777 chain_hash: hash,
778 } => {
779 exchange = ex;
780 journal_writer = wr;
781 last_sequence = seq;
782 chain_hash = hash;
783 continue;
784 }
785 AfterSession::Reconnect => {}
786 }
787 }
788}
789
790#[cfg(test)]
795mod tests {
796 use super::*;
797 use std::io::Read;
798 use std::net::TcpListener;
799 use std::time::{Duration, Instant};
800
801 use melin_transport_core::replication::protocol::{ReplicaMessage, decode_replica_message};
802
803 fn socket_pair() -> (TcpStream, TcpStream) {
805 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
806 let addr = listener.local_addr().unwrap();
807 let client = TcpStream::connect(addr).unwrap();
808 let (server, _) = listener.accept().unwrap();
809 (client, server)
810 }
811
812 fn ack(seq: u64) -> Ack {
813 Ack {
814 acked_sequence: seq,
815 in_memory_sequence: seq,
816 }
817 }
818
819 fn flush_acks(transport: &mut UringTransport) {
821 let mut recv_buf = Vec::new();
822 let deadline = Instant::now() + Duration::from_secs(5);
823 while transport.ack_in_flight() {
824 transport.poll_recv(&mut recv_buf).unwrap();
825 assert!(Instant::now() < deadline, "ack send never completed");
826 std::hint::spin_loop();
827 }
828 }
829
830 fn read_acks(peer: &mut TcpStream, expect: usize) -> Vec<Ack> {
832 peer.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
833 let mut buf = Vec::new();
834 let mut chunk = [0u8; 256];
835 let mut acks = Vec::new();
836 while acks.len() < expect {
837 let n = peer.read(&mut chunk).expect("peer read");
838 assert!(n > 0, "peer closed before all acks arrived");
839 buf.extend_from_slice(&chunk[..n]);
840 loop {
842 if buf.len() < 4 {
843 break;
844 }
845 let len = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
846 if buf.len() < 4 + len {
847 break;
848 }
849 match decode_replica_message(&buf[4..4 + len]).expect("decodable frame") {
850 ReplicaMessage::Ack(a) => acks.push(a),
851 other => panic!("expected Ack frame, got {other:?}"),
852 }
853 buf.drain(..4 + len);
854 }
855 }
856 assert!(buf.is_empty(), "trailing bytes after expected acks");
857 acks
858 }
859
860 #[test]
861 fn coalesces_to_latest_ack_while_send_in_flight() {
862 let (client, mut peer) = socket_pair();
863 let mut transport = UringTransport::new(&client).unwrap();
864
865 assert!(transport.send_ack(&ack(1)).unwrap());
870 assert!(transport.ack_in_flight());
871 assert!(transport.send_ack(&ack(2)).unwrap());
872 assert!(transport.send_ack(&ack(3)).unwrap());
873
874 flush_acks(&mut transport);
875
876 let acks = read_acks(&mut peer, 2);
878 let seqs: Vec<u64> = acks.iter().map(|a| a.acked_sequence).collect();
879 assert_eq!(seqs, vec![1, 3]);
880 }
881
882 #[test]
883 fn sequential_acks_all_reach_the_wire_in_order() {
884 let (client, mut peer) = socket_pair();
885 let mut transport = UringTransport::new(&client).unwrap();
886
887 for seq in 1..=3 {
888 assert!(transport.send_ack(&ack(seq)).unwrap());
889 flush_acks(&mut transport);
890 }
891
892 let acks = read_acks(&mut peer, 3);
893 let seqs: Vec<u64> = acks.iter().map(|a| a.acked_sequence).collect();
894 assert_eq!(seqs, vec![1, 2, 3]);
895 }
896
897 #[cfg(not(feature = "no-persist"))]
902 mod scripted {
903 use super::*;
904 use melin_app::app_factory::AppFactory;
905 use melin_app::{AppEvent, Application, ApplyCtx, CodecError, RejectReason};
906
907 pub(super) use melin_transport_core::pipeline::InputSlot;
910
911 #[derive(Debug, Clone, Copy, PartialEq)]
912 pub(super) struct EvtAdd(pub(super) u64);
913
914 impl AppEvent for EvtAdd {
915 fn encoded_size(&self) -> usize {
916 8
917 }
918 fn encode(&self, buf: &mut [u8]) -> usize {
919 buf[..8].copy_from_slice(&self.0.to_le_bytes());
920 8
921 }
922 fn decode(buf: &[u8]) -> Result<Self, CodecError> {
923 if buf.len() < 8 {
924 return Err(CodecError::Truncated);
925 }
926 Ok(EvtAdd(u64::from_le_bytes(buf[..8].try_into().expect("8"))))
927 }
928 fn is_query(&self) -> bool {
929 false
930 }
931 }
932
933 #[derive(Debug, Clone, Copy)]
934 pub(super) struct Rpt;
935
936 pub(super) struct App;
937
938 impl Application for App {
939 type Event = EvtAdd;
940 type Report = Rpt;
941 type QueryResponse = Rpt;
942 const APP_VERSION: u16 = 1;
943
944 fn apply(&mut self, _e: EvtAdd, _ctx: &ApplyCtx, _out: &mut Vec<Rpt>) -> Option<Rpt> {
945 None
946 }
947 fn tick(&mut self, _now_ns: u64, _out: &mut Vec<Rpt>) {}
948 fn check_request_seq(&mut self, _key_hash: u64, _seq: u64) -> bool {
949 true
950 }
951 fn build_reject(_e: &EvtAdd, _r: RejectReason) -> Rpt {
952 Rpt
953 }
954 fn snapshot<W: std::io::Write>(&self, _w: &mut W) -> std::io::Result<()> {
955 Ok(())
956 }
957 fn restore<R: std::io::Read>(_r: &mut R) -> std::io::Result<Self> {
958 Ok(App)
959 }
960 }
961
962 pub(super) struct Factory;
963
964 impl AppFactory for Factory {
965 type App = App;
966 fn empty(&self) -> App {
967 App
968 }
969 fn prefault(&self, _app: &mut App) {}
970 }
971
972 pub(super) const REPLICA_KEY: [u8; 32] = [0xFC; 32];
975
976 pub(super) fn replica_auth() -> melin_app::auth::AuthorizedKeys {
979 let key = ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY);
980 let pub_b64 = base64::Engine::encode(
981 &base64::engine::general_purpose::STANDARD,
982 key.verifying_key().to_bytes(),
983 );
984 melin_app::auth::AuthorizedKeys::parse(&format!("replication {pub_b64} test-replica\n"))
985 .expect("parse keys")
986 }
987
988 pub(super) fn unpinned_cores() -> crate::server::PipelineCores {
990 crate::server::PipelineCores {
991 journal: 0,
992 matching: 0,
993 response: 0,
994 reader: 0,
995 repl_sender: 0,
996 event_publisher: 0,
997 shadow: 0,
998 repl_handler_0: 0,
999 repl_handler_1: 0,
1000 journal_prep: 0,
1001 }
1002 }
1003
1004 pub(super) fn read_replica_msg(stream: &mut TcpStream) -> ReplicaMessage {
1006 let mut len_buf = [0u8; 4];
1007 stream.read_exact(&mut len_buf).expect("frame length");
1008 let len = u32::from_le_bytes(len_buf) as usize;
1009 let mut payload = vec![0u8; len];
1010 stream.read_exact(&mut payload).expect("frame payload");
1011 decode_replica_message(&payload).expect("decodable replica frame")
1012 }
1013
1014 pub(super) fn wait_for_ack(stream: &mut TcpStream, seq: u64) {
1016 loop {
1017 if let ReplicaMessage::Ack(a) = read_replica_msg(stream)
1018 && a.acked_sequence >= seq
1019 {
1020 return;
1021 }
1022 }
1023 }
1024
1025 pub(super) fn accept_within(listener: &std::net::TcpListener, secs: u64) -> TcpStream {
1028 let deadline = Instant::now() + Duration::from_secs(secs);
1029 loop {
1030 match listener.accept() {
1031 Ok((s, _)) => {
1032 s.set_read_timeout(Some(Duration::from_secs(10)))
1033 .expect("read timeout");
1034 return s;
1035 }
1036 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
1037 assert!(Instant::now() < deadline, "replica never connected");
1038 std::thread::sleep(Duration::from_millis(10));
1039 }
1040 Err(e) => panic!("accept failed: {e}"),
1041 }
1042 }
1043 }
1044 }
1045
1046 #[cfg(all(feature = "hash-chain", not(feature = "no-persist")))]
1052 mod divergence_resync {
1053 use super::super::super::auth::authenticate_replica;
1054 use super::scripted::*;
1055 use super::*;
1056 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
1057 use melin_transport_core::cursors::WireSeq;
1058 use melin_transport_core::replication::catchup::{lineage_origin, snapshot_transfer_with};
1059 use melin_transport_core::replication::protocol::{
1060 encode_hash_mismatch, encode_rotate, encode_stream_start,
1061 };
1062 use std::sync::Arc;
1063 use std::sync::atomic::AtomicBool;
1064
1065 #[test]
1074 fn mid_stream_divergence_resyncs_in_process() {
1075 let dir = tempfile::tempdir().expect("tempdir");
1076
1077 let primary_journal = dir.path().join("primary.journal");
1081 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1082 let mut chain_at_4 = [0u8; 32];
1083 for v in 1..=5u64 {
1084 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1085 if v == 2 {
1086 w.rotate_segment().expect("rotate");
1087 }
1088 if v == 4 {
1089 chain_at_4 = w.chain_hash().expect("chain");
1090 }
1091 }
1092 drop(w);
1093 melin_transport_core::snapshot::save::<App>(
1094 &App,
1095 WireSeq::new(4),
1096 chain_at_4,
1097 0,
1098 &primary_journal.with_extension("snapshot"),
1099 )
1100 .expect("save snapshot");
1101 let (lineage_start, lineage_anchor) =
1102 lineage_origin(&primary_journal).expect("lineage");
1103
1104 let authorized_keys = replica_auth();
1106
1107 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1108 listener.set_nonblocking(true).expect("nonblocking");
1109 let addr = listener.local_addr().expect("addr");
1110
1111 let replica_journal = dir.path().join("replica.journal");
1113 let replica_snapshot = dir.path().join("replica.snapshot");
1114 let shutdown = Arc::new(AtomicBool::new(false));
1115 let control = crate::replication::ReplicaControlPlane::new();
1116 let cores = unpinned_cores();
1117 let replica = {
1118 let journal = replica_journal.clone();
1119 let shutdown = Arc::clone(&shutdown);
1120 let control = control.clone();
1121 std::thread::spawn(move || -> Result<bool, String> {
1122 run_receiver::<App, BufferedWriter<EvtAdd>>(
1123 addr,
1124 &journal,
1125 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1126 &shutdown,
1127 &control,
1128 3_600_000,
1129 replica_snapshot,
1130 cores,
1131 Duration::ZERO,
1132 64,
1133 false,
1134 Arc::new(Factory),
1135 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1136 )
1137 .map(|state| state.is_none())
1139 .map_err(|e| e.to_string())
1140 })
1141 };
1142
1143 let mut s1 = accept_within(&listener, 30);
1146 let mut s1r = s1.try_clone().expect("clone");
1147 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1148 match read_replica_msg(&mut s1) {
1149 ReplicaMessage::Handshake(h) => {
1150 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
1151 }
1152 other => panic!("expected Handshake, got {other:?}"),
1153 }
1154 let mut buf = Vec::new();
1155 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
1156 s1.write_all(&buf).expect("StreamStart 1");
1157 buf.clear();
1158 melin_transport_core::replication_wire::encode_input_batch(
1159 &[InputSlot::<EvtAdd> {
1160 connection_id: 0,
1161 key_hash: 0,
1162 request_seq: 0,
1163 sequence: 1,
1164 timestamp_ns: 1,
1165 event: JournalEvent::App(EvtAdd(1)),
1166 publish_ts: Default::default(),
1168 recv_ts: Default::default(),
1169 }],
1170 &mut buf,
1171 );
1172 s1.write_all(&buf).expect("InputBatch");
1173 wait_for_ack(&mut s1, 1);
1174
1175 buf.clear();
1178 encode_rotate(1, &[0xEE; 32], &mut buf);
1179 s1.write_all(&buf).expect("Rotate");
1180
1181 let mut s2 = accept_within(&listener, 30);
1184 let mut s2r = s2.try_clone().expect("clone");
1185 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1186 match read_replica_msg(&mut s2) {
1187 ReplicaMessage::Handshake(h) => assert_eq!(
1188 h.last_sequence, 1,
1189 "post-divergence handshake must carry the recovered journal position"
1190 ),
1191 other => panic!("expected Handshake, got {other:?}"),
1192 }
1193 assert!(
1199 !control.pipeline_healthy.load(Ordering::Acquire),
1200 "the divergence must have latched the health mirror false"
1201 );
1202 buf.clear();
1203 encode_hash_mismatch(&mut buf);
1204 s2.write_all(&buf).expect("HashMismatch");
1205 let transfer_shutdown = AtomicBool::new(false);
1206 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1207 s2.write_all(b)?;
1208 s2.flush()
1209 };
1210 let end = snapshot_transfer_with::<EvtAdd>(
1213 &primary_journal,
1214 &mut publish,
1215 &transfer_shutdown,
1216 1,
1217 )
1218 .expect("snapshot transfer");
1219 assert_eq!(
1220 end,
1221 melin_transport_core::replication::catchup::CatchUpResult::Ok(5)
1222 );
1223 let mut s2_acks = s2.try_clone().expect("clone");
1224 wait_for_ack(&mut s2_acks, 5);
1225
1226 assert!(
1229 control.pipeline_healthy.load(Ordering::Acquire),
1230 "the resync rebuild must reset the health mirror true"
1231 );
1232
1233 shutdown.store(true, Ordering::Relaxed);
1236 let result = replica.join().expect("replica thread panicked");
1237 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
1238
1239 assert!(
1242 dir.path().join("replica.journal.divergent.0").exists(),
1243 "divergent lineage must be archived"
1244 );
1245 assert!(replica_journal.exists(), "re-seeded live journal");
1246 }
1247
1248 #[test]
1262 fn resync_transfer_failure_rebuilds_clean_not_over_stale_writer() {
1263 let dir = tempfile::tempdir().expect("tempdir");
1264
1265 let primary_journal = dir.path().join("primary.journal");
1269 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1270 for v in 1..=2u64 {
1271 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1272 }
1273 drop(w);
1274 let (lineage_start, lineage_anchor) =
1275 lineage_origin(&primary_journal).expect("lineage");
1276
1277 let authorized_keys = replica_auth();
1278
1279 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1280 listener.set_nonblocking(true).expect("nonblocking");
1281 let addr = listener.local_addr().expect("addr");
1282
1283 let replica_journal = dir.path().join("replica.journal");
1284 let replica_snapshot = dir.path().join("replica.snapshot");
1285 let shutdown = Arc::new(AtomicBool::new(false));
1286 let control = crate::replication::ReplicaControlPlane::new();
1287 let cores = unpinned_cores();
1288 let replica = {
1289 let journal = replica_journal.clone();
1290 let shutdown = Arc::clone(&shutdown);
1291 let control = control.clone();
1292 std::thread::spawn(move || -> Result<bool, String> {
1293 run_receiver::<App, BufferedWriter<EvtAdd>>(
1294 addr,
1295 &journal,
1296 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1297 &shutdown,
1298 &control,
1299 3_600_000,
1300 replica_snapshot,
1301 cores,
1302 Duration::ZERO,
1303 64,
1304 false,
1305 Arc::new(Factory),
1306 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1307 )
1308 .map(|state| state.is_none())
1309 .map_err(|e| e.to_string())
1310 })
1311 };
1312
1313 let stream_one = |s: &mut TcpStream, buf: &mut Vec<u8>| {
1316 buf.clear();
1317 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, buf);
1318 s.write_all(buf).expect("StreamStart");
1319 buf.clear();
1320 melin_transport_core::replication_wire::encode_input_batch(
1321 &[InputSlot::<EvtAdd> {
1322 connection_id: 0,
1323 key_hash: 0,
1324 request_seq: 0,
1325 sequence: 1,
1326 timestamp_ns: 1,
1327 event: JournalEvent::App(EvtAdd(1)),
1328 publish_ts: Default::default(),
1329 recv_ts: Default::default(),
1330 }],
1331 buf,
1332 );
1333 s.write_all(buf).expect("InputBatch");
1334 };
1335
1336 let mut buf = Vec::new();
1341 let mut s1 = accept_within(&listener, 30);
1342 let mut s1r = s1.try_clone().expect("clone");
1343 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1344 match read_replica_msg(&mut s1) {
1345 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0, "fresh"),
1346 other => panic!("expected Handshake, got {other:?}"),
1347 }
1348 stream_one(&mut s1, &mut buf);
1349 wait_for_ack(&mut s1, 1);
1350 buf.clear();
1351 encode_rotate(1, &[0xEE; 32], &mut buf);
1352 s1.write_all(&buf).expect("Rotate");
1353
1354 let mut s2 = accept_within(&listener, 30);
1359 let mut s2r = s2.try_clone().expect("clone");
1360 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1361 match read_replica_msg(&mut s2) {
1362 ReplicaMessage::Handshake(h) => assert_eq!(
1363 h.last_sequence, 1,
1364 "reconnect carries the recovered forked position"
1365 ),
1366 other => panic!("expected Handshake, got {other:?}"),
1367 }
1368 buf.clear();
1369 encode_hash_mismatch(&mut buf);
1370 s2.write_all(&buf).expect("HashMismatch");
1371 s2.flush().expect("flush");
1372 drop(s2);
1375 drop(s2r);
1376
1377 let mut s3 = accept_within(&listener, 30);
1382 let mut s3r = s3.try_clone().expect("clone");
1383 authenticate_replica(&mut s3r, &authorized_keys).expect("auth 3");
1384 match read_replica_msg(&mut s3) {
1385 ReplicaMessage::Handshake(h) => assert_eq!(
1386 h.last_sequence, 0,
1387 "after a failed transfer the retry handshakes as fresh"
1388 ),
1389 other => panic!("expected Handshake, got {other:?}"),
1390 }
1391 stream_one(&mut s3, &mut buf);
1392 wait_for_ack(&mut s3, 1);
1393
1394 shutdown.store(true, Ordering::Relaxed);
1395 let result = replica.join().expect("replica thread panicked");
1396 assert_eq!(result, Ok(true), "receiver exits cleanly via shutdown");
1397
1398 assert!(
1403 dir.path().join("replica.journal.divergent.0").exists(),
1404 "forked lineage archived, never deleted"
1405 );
1406 assert!(
1407 replica_journal.exists(),
1408 "fresh live journal recreated after the failed transfer"
1409 );
1410 }
1411
1412 #[test]
1419 fn second_mid_stream_divergence_exits_hard() {
1420 let dir = tempfile::tempdir().expect("tempdir");
1421
1422 let primary_journal = dir.path().join("primary.journal");
1423 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1424 let mut chain_at_4 = [0u8; 32];
1425 for v in 1..=5u64 {
1426 w.append(&JournalEvent::App(EvtAdd(v))).expect("append");
1427 if v == 2 {
1428 w.rotate_segment().expect("rotate");
1429 }
1430 if v == 4 {
1431 chain_at_4 = w.chain_hash().expect("chain");
1432 }
1433 }
1434 drop(w);
1435 melin_transport_core::snapshot::save::<App>(
1436 &App,
1437 WireSeq::new(4),
1438 chain_at_4,
1439 0,
1440 &primary_journal.with_extension("snapshot"),
1441 )
1442 .expect("save snapshot");
1443 let (lineage_start, lineage_anchor) =
1444 lineage_origin(&primary_journal).expect("lineage");
1445
1446 let authorized_keys = replica_auth();
1447
1448 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1449 listener.set_nonblocking(true).expect("nonblocking");
1450 let addr = listener.local_addr().expect("addr");
1451
1452 let replica_journal = dir.path().join("replica.journal");
1453 let replica_snapshot = dir.path().join("replica.snapshot");
1454 let shutdown = Arc::new(AtomicBool::new(false));
1455 let control = crate::replication::ReplicaControlPlane::new();
1456 let cores = unpinned_cores();
1457 let replica = {
1458 let journal = replica_journal.clone();
1459 let shutdown = Arc::clone(&shutdown);
1460 let control = control.clone();
1461 std::thread::spawn(move || -> Result<bool, String> {
1462 run_receiver::<App, BufferedWriter<EvtAdd>>(
1463 addr,
1464 &journal,
1465 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1466 &shutdown,
1467 &control,
1468 3_600_000,
1469 replica_snapshot,
1470 cores,
1471 Duration::ZERO,
1472 64,
1473 false,
1474 Arc::new(Factory),
1475 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1476 )
1477 .map(|state| state.is_none())
1478 .map_err(|e| e.to_string())
1479 })
1480 };
1481
1482 let mut s1 = accept_within(&listener, 30);
1484 let mut s1r = s1.try_clone().expect("clone");
1485 authenticate_replica(&mut s1r, &authorized_keys).expect("auth 1");
1486 match read_replica_msg(&mut s1) {
1487 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 0),
1488 other => panic!("expected Handshake, got {other:?}"),
1489 }
1490 let mut buf = Vec::new();
1491 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
1492 s1.write_all(&buf).expect("StreamStart 1");
1493 buf.clear();
1494 melin_transport_core::replication_wire::encode_input_batch(
1495 &[InputSlot::<EvtAdd> {
1496 connection_id: 0,
1497 key_hash: 0,
1498 request_seq: 0,
1499 sequence: 1,
1500 timestamp_ns: 1,
1501 event: JournalEvent::App(EvtAdd(1)),
1502 publish_ts: Default::default(),
1503 recv_ts: Default::default(),
1504 }],
1505 &mut buf,
1506 );
1507 s1.write_all(&buf).expect("InputBatch");
1508 wait_for_ack(&mut s1, 1);
1509 buf.clear();
1510 encode_rotate(1, &[0xEE; 32], &mut buf);
1511 s1.write_all(&buf).expect("Rotate 1");
1512
1513 let mut s2 = accept_within(&listener, 30);
1516 let mut s2r = s2.try_clone().expect("clone");
1517 authenticate_replica(&mut s2r, &authorized_keys).expect("auth 2");
1518 match read_replica_msg(&mut s2) {
1519 ReplicaMessage::Handshake(h) => assert_eq!(h.last_sequence, 1),
1520 other => panic!("expected Handshake, got {other:?}"),
1521 }
1522 buf.clear();
1523 encode_hash_mismatch(&mut buf);
1524 s2.write_all(&buf).expect("HashMismatch");
1525 let transfer_shutdown = AtomicBool::new(false);
1526 {
1527 let mut publish = |b: &[u8]| -> std::io::Result<()> {
1528 s2.write_all(b)?;
1529 s2.flush()
1530 };
1531 snapshot_transfer_with::<EvtAdd>(
1532 &primary_journal,
1533 &mut publish,
1534 &transfer_shutdown,
1535 1,
1536 )
1537 .expect("snapshot transfer");
1538 }
1539 buf.clear();
1540 melin_transport_core::replication_wire::encode_input_batch(
1541 &[InputSlot::<EvtAdd> {
1542 connection_id: 0,
1543 key_hash: 0,
1544 request_seq: 0,
1545 sequence: 6,
1546 timestamp_ns: 6,
1547 event: JournalEvent::App(EvtAdd(6)),
1548 publish_ts: Default::default(),
1549 recv_ts: Default::default(),
1550 }],
1551 &mut buf,
1552 );
1553 s2.write_all(&buf).expect("InputBatch 6");
1554 let mut s2_acks = s2.try_clone().expect("clone");
1555 wait_for_ack(&mut s2_acks, 6);
1556 buf.clear();
1557 encode_rotate(6, &[0xEE; 32], &mut buf);
1558 s2.write_all(&buf).expect("Rotate 2");
1559
1560 let result = replica.join().expect("replica thread panicked");
1563 let err = result.expect_err("second divergence must exit hard");
1564 assert!(
1565 err.contains("divergence recurred"),
1566 "error must name the recurrence: {err}"
1567 );
1568 }
1569
1570 type PromoteHandles =
1579 crate::replication::ReplicaPipelineHandles<App, BufferedWriter<EvtAdd>>;
1580
1581 #[test]
1582 fn promotion_hands_back_local_state_when_present() {
1583 let dir = tempfile::tempdir().unwrap();
1584 let mut pipeline: Option<PromoteHandles> = None;
1585 let mut exchange = Some(App);
1586 let mut journal_writer =
1587 Some(BufferedWriter::<EvtAdd>::create(&dir.path().join("p.journal")).unwrap());
1588
1589 let result = crate::replication::take_pipeline_for_promotion(
1590 &mut pipeline,
1591 &mut exchange,
1592 &mut journal_writer,
1593 );
1594
1595 assert!(matches!(result, Ok(Some(_))), "warm state must be promoted");
1596 assert!(
1597 exchange.is_none() && journal_writer.is_none(),
1598 "state must be moved into the result"
1599 );
1600 }
1601
1602 #[test]
1603 fn promotion_errs_when_no_local_state() {
1604 let mut pipeline: Option<PromoteHandles> = None;
1605 let mut exchange: Option<App> = None;
1606 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
1607
1608 let result = crate::replication::take_pipeline_for_promotion(
1609 &mut pipeline,
1610 &mut exchange,
1611 &mut journal_writer,
1612 );
1613
1614 let err = match result {
1616 Err(e) => e,
1617 Ok(_) => panic!("a promote with nothing to promote must error"),
1618 };
1619 assert!(
1620 err.to_string().contains("no local state available"),
1621 "{err}"
1622 );
1623 }
1624
1625 #[test]
1626 fn promotion_errs_on_partial_state() {
1627 let mut pipeline: Option<PromoteHandles> = None;
1629 let mut exchange = Some(App);
1630 let mut journal_writer: Option<BufferedWriter<EvtAdd>> = None;
1631
1632 let result = crate::replication::take_pipeline_for_promotion(
1633 &mut pipeline,
1634 &mut exchange,
1635 &mut journal_writer,
1636 );
1637
1638 assert!(result.is_err(), "partial state is not a valid promotion");
1639 }
1640 }
1641
1642 #[cfg(not(feature = "no-persist"))]
1648 mod backoff_shutdown {
1649 use super::super::super::auth::authenticate_replica;
1650 use super::scripted::*;
1651 use super::*;
1652 use melin_journal::{BufferedWriter, JournalEvent, JournalWrite};
1653 use melin_transport_core::replication::catchup::lineage_origin;
1654 use melin_transport_core::replication::protocol::encode_stream_start;
1655 use std::sync::Arc;
1656 use std::sync::atomic::AtomicBool;
1657
1658 fn live_thread_names() -> Vec<String> {
1661 std::fs::read_dir("/proc/self/task")
1662 .expect("read /proc/self/task")
1663 .filter_map(|entry| {
1664 let entry = entry.ok()?;
1667 let name = std::fs::read_to_string(entry.path().join("comm")).ok()?;
1668 Some(name.trim().to_string())
1669 })
1670 .collect()
1671 }
1672
1673 #[test]
1679 fn shutdown_during_reconnect_backoff_joins_pipeline_threads() {
1680 let dir = tempfile::tempdir().expect("tempdir");
1681
1682 let primary_journal = dir.path().join("primary.journal");
1685 let mut w = BufferedWriter::<EvtAdd>::create(&primary_journal).expect("create");
1686 w.append(&JournalEvent::App(EvtAdd(1))).expect("append");
1687 drop(w);
1688 let (lineage_start, lineage_anchor) =
1689 lineage_origin(&primary_journal).expect("lineage");
1690
1691 let authorized_keys = replica_auth();
1692 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
1693 listener.set_nonblocking(true).expect("nonblocking");
1694 let addr = listener.local_addr().expect("addr");
1695
1696 let shutdown = Arc::new(AtomicBool::new(false));
1697 let control = crate::replication::ReplicaControlPlane::new();
1698 let replica = {
1699 let journal = dir.path().join("replica.journal");
1700 let snapshot = dir.path().join("replica.snapshot");
1701 let shutdown = Arc::clone(&shutdown);
1702 let control = control.clone();
1703 std::thread::spawn(move || -> Result<bool, String> {
1704 run_receiver::<App, BufferedWriter<EvtAdd>>(
1705 addr,
1706 &journal,
1707 &ed25519_dalek::SigningKey::from_bytes(&REPLICA_KEY),
1708 &shutdown,
1709 &control,
1710 3_600_000,
1711 snapshot,
1712 unpinned_cores(),
1713 Duration::ZERO,
1714 64,
1715 false,
1716 Arc::new(Factory),
1717 Arc::new(melin_transport_core::fence::FenceState::new(0)),
1718 )
1719 .map(|state| state.is_none())
1721 .map_err(|e| e.to_string())
1722 })
1723 };
1724
1725 let mut s1 = accept_within(&listener, 30);
1729 let mut s1r = s1.try_clone().expect("clone");
1730 authenticate_replica(&mut s1r, &authorized_keys).expect("auth");
1731 match read_replica_msg(&mut s1) {
1732 ReplicaMessage::Handshake(h) => {
1733 assert_eq!(h.last_sequence, 0, "fresh replica handshake")
1734 }
1735 other => panic!("expected Handshake, got {other:?}"),
1736 }
1737 let mut buf = Vec::new();
1738 encode_stream_start(0, lineage_start, lineage_anchor, 0, 1, &mut buf);
1739 s1.write_all(&buf).expect("StreamStart");
1740 buf.clear();
1741 melin_transport_core::replication_wire::encode_input_batch(
1742 &[InputSlot::<EvtAdd> {
1743 connection_id: 0,
1744 key_hash: 0,
1745 request_seq: 0,
1746 sequence: 1,
1747 timestamp_ns: 1,
1748 event: JournalEvent::App(EvtAdd(1)),
1749 publish_ts: Default::default(),
1750 recv_ts: Default::default(),
1751 }],
1752 &mut buf,
1753 );
1754 s1.write_all(&buf).expect("InputBatch");
1755 wait_for_ack(&mut s1, 1);
1756
1757 drop(s1);
1761 drop(s1r);
1762 drop(listener);
1763 std::thread::sleep(Duration::from_millis(1500));
1768
1769 shutdown.store(true, Ordering::Relaxed);
1770 let deadline = Instant::now() + Duration::from_secs(10);
1771 while !replica.is_finished() {
1772 assert!(
1773 Instant::now() < deadline,
1774 "receiver did not exit after shutdown"
1775 );
1776 std::thread::sleep(Duration::from_millis(20));
1777 }
1778 let result = replica.join().expect("replica thread panicked");
1779 assert_eq!(result, Ok(true), "receiver must exit cleanly via shutdown");
1780
1781 let deadline = Instant::now() + Duration::from_secs(10);
1787 loop {
1788 let names = live_thread_names();
1789 if !names
1790 .iter()
1791 .any(|n| n == "journal" || n == "matching" || n == "drain")
1792 {
1793 break;
1794 }
1795 assert!(
1796 Instant::now() < deadline,
1797 "pipeline threads still alive after shutdown: {names:?}"
1798 );
1799 std::thread::sleep(Duration::from_millis(50));
1800 }
1801 }
1802 }
1803}